SpringBoot 單元測試與 Mockito 使用

SpringBoot 單元測試與 Mockito 使用

單元測試應(yīng)遵循 → AIR 原則

SpringBoot 測試支持由兩個模塊提供:

  • spring-boot-test 包含核心項目
  • spring-boot-test-autoconfigure 支持測試的自動配置

通常我們只要引入 spring-boot-starter-test 依賴就行心软,它包含了一些常用的模塊 Junit颗管、Spring Test、AssertJ玫芦、Hamcrest、Mockito 等茂契。

相關(guān)注解

SpringBoot 使用了 Junit4 作為單元測試框架饮亏,所以注解與 Junit4 是一致的。

注解 作用
@Test(excepted==xx.class,timeout=毫秒數(shù)) 修飾一個方法為測試方法忠藤,excepted參數(shù)可以忽略某些異常類
@Before 在每一個測試方法被運行前執(zhí)行一次
@BeforeClass 在所有測試方法執(zhí)行前執(zhí)行
@After 在每一個測試方法運行后執(zhí)行一次
@AfterClass 在所有測試方法執(zhí)行后執(zhí)行
@Ignore 修飾的類或方法會被測試運行器忽略
@RunWith 更改測試運行器

@SpringBootTest

SpringBoot提供了一個 @SpringBootTest 注解用于測試 SpringBoot 應(yīng)用挟伙,它可以用作標準 spring-test @ContextConfiguration 注釋的替代方法,其原理是通過 SpringApplication 在測試中創(chuàng)建ApplicationContext模孩。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
}

該注解提供了兩個屬性用于配置:

  • webEnvironment:指定Web應(yīng)用環(huán)境尖阔,它可以是以下值
    • MOCK:提供一個模擬的 Servlet 環(huán)境,內(nèi)置的 Servlet 容器沒有啟動榨咐,配合可以與@AutoConfigureMockMvc 結(jié)合使用介却,用于基于 MockMvc 的應(yīng)用程序測試。
    • RANDOM_PORT:加載一個 EmbeddedWebApplicationContext 并提供一個真正嵌入式的 Servlet 環(huán)境块茁,隨機端口筷笨。
    • DEFINED_PORT:加載一個 EmbeddedWebApplicationContext 并提供一個真正嵌入式的 Servlet 環(huán)境,默認端口 8080 或由配置文件指定龟劲。
    • NONE:使用 SpringApplication 加載 ApplicationContext胃夏,但不提供任何 servlet 環(huán)境。
  • classes:指定應(yīng)用啟動類昌跌,通常情況下無需設(shè)置仰禀,因為 SpringBoot 會自動搜索,直到找到 @SpringBootApplication 或 @SpringBootConfiguration 注解蚕愤。

單元測試回滾

如果你添加了 @Transactional 注解答恶,它會在每個測試方法結(jié)束時會進行回滾操作。

但是如果使用 RANDOM_PORT 或 DEFINED_PORT 這種真正的 Servlet 環(huán)境萍诱,HTTP 客戶端和服務(wù)器將在不同的線程中運行悬嗓,從而分離事務(wù)。 在這種情況下裕坊,在服務(wù)器上啟動的任何事務(wù)都不會回滾包竹。

斷言

JUnit4 結(jié)合 Hamcrest 提供了一個全新的斷言語法——assertThat,結(jié)合 Hamcrest 提供的匹配符,就可以表達全部的測試思想周瞎。

// 一般匹配符
int s = new C().add(1, 1);
// allOf:所有條件必須都成立苗缩,測試才通過
assertThat(s, allOf(greaterThan(1), lessThan(3)));
// anyOf:只要有一個條件成立,測試就通過
assertThat(s, anyOf(greaterThan(1), lessThan(1)));
// anything:無論什么條件声诸,測試都通過
assertThat(s, anything());
// is:變量的值等于指定值時酱讶,測試通過
assertThat(s, is(2));
// not:和is相反,變量的值不等于指定值時彼乌,測試通過
assertThat(s, not(1));

// 數(shù)值匹配符
double d = new C().div(10, 3);
// closeTo:浮點型變量的值在3.0±0.5范圍內(nèi)泻肯,測試通過
assertThat(d, closeTo(3.0, 0.5));
// greaterThan:變量的值大于指定值時,測試通過
assertThat(d, greaterThan(3.0));
// lessThan:變量的值小于指定值時慰照,測試通過
assertThat(d, lessThan(3.5));
// greaterThanOrEuqalTo:變量的值大于等于指定值時灶挟,測試通過
assertThat(d, greaterThanOrEqualTo(3.3));
// lessThanOrEqualTo:變量的值小于等于指定值時,測試通過
assertThat(d, lessThanOrEqualTo(3.4));

// 字符串匹配符
String n = new C().getName("Magci");
// containsString:字符串變量中包含指定字符串時焚挠,測試通過
assertThat(n, containsString("ci"));
// startsWith:字符串變量以指定字符串開頭時膏萧,測試通過
assertThat(n, startsWith("Ma"));
// endsWith:字符串變量以指定字符串結(jié)尾時,測試通過
assertThat(n, endsWith("i"));
// euqalTo:字符串變量等于指定字符串時蝌衔,測試通過
assertThat(n, equalTo("Magci"));
// equalToIgnoringCase:字符串變量在忽略大小寫的情況下等于指定字符串時榛泛,測試通過
assertThat(n, equalToIgnoringCase("magci"));
// equalToIgnoringWhiteSpace:字符串變量在忽略頭尾任意空格的情況下等于指定字符串時,測試通過
assertThat(n, equalToIgnoringWhiteSpace(" Magci   "));

// 集合匹配符
List<String> l = new C().getList("Magci");
// hasItem:Iterable變量中含有指定元素時噩斟,測試通過
assertThat(l, hasItem("Magci"));

Map<String, String> m = new C().getMap("mgc", "Magci");
// hasEntry:Map變量中含有指定鍵值對時曹锨,測試通過
assertThat(m, hasEntry("mgc", "Magci"));
// hasKey:Map變量中含有指定鍵時,測試通過
assertThat(m, hasKey("mgc"));
// hasValue:Map變量中含有指定值時剃允,測試通過
assertThat(m, hasValue("Magci"));

基本的單元測試例子

下面是一個基本的單元測試例子沛简,對某個方法的返回結(jié)果進行斷言:

@Service
public class UserService {

    public String getName() {
        return "lyTongXue";
    }
    
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService service;

    @Test
    public void getName() {
        String name = service.getName();
        assertThat(name,is("lyTongXue"));
    }

}

Controller 測試

Spring 提供了 MockMVC 用于支持 RESTful 風(fēng)格的 Spring MVC 測試,使用 MockMvcBuilder 來構(gòu)造MockMvc 實例斥废。MockMvc 有兩個實現(xiàn):

  • StandaloneMockMvcBuilder:指定 WebApplicationContext椒楣,它將會從該上下文獲取相應(yīng)的控制器并得到相應(yīng)的 MockMvc

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest  {
        @Autowired
        private WebApplicationContext webApplicationContext;
        private MockMvc mockMvc;
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    } 
    
  • DefaultMockMvcBuilder:通過參數(shù)指定一組控制器,這樣就不需要從上下文獲取了

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest  {
        private MockMvc mockMvc;
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
        } 
    }    
    

下面是一個簡單的用例牡肉,對 UserController 的 /v1/users/{id} 接口進行測試捧灰。

@RestController
@RequestMapping("v1/users")
public class UserController {

    @GetMapping("/{id}")
    public User get(@PathVariable("id") String id) {
        return new User(1, "lyTongXue");
    }

    @Data
    @AllArgsConstructor
    public class User {
        private Integer id;
        private String name;
    }

}
// ...
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getUser() {
        mockMvc.perform(get("/v1/users/1")
                .accept(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk())
           .andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
    }
  
}

方法描述

  • perform:執(zhí)行一個 RequestBuilder 請求,返回一個 ResultActions 實例對象统锤,可對請求結(jié)果進行期望與其它操作

  • get:聲明發(fā)送一個 get 請求的方法毛俏,更多的請求類型可查閱→MockMvcRequestBuilders 文檔

  • andExpect:添加 ResultMatcher 驗證規(guī)則,驗證請求結(jié)果是否正確饲窿,驗證規(guī)則可查閱→MockMvcResultMatchers 文檔

  • andDo:添加 ResultHandler 結(jié)果處理器煌寇,比如調(diào)試時打印結(jié)果到控制臺,更多處理器可查閱→MockMvcResultHandlers 文檔

  • andReturn:返回執(zhí)行請求的結(jié)果逾雄,該結(jié)果是一個恩 MvcResult 實例對象→MvcResult 文檔

Mock 數(shù)據(jù)

在單元測試中阀溶,Service 層的調(diào)用往往涉及到對數(shù)據(jù)庫腻脏、中間件等外部依賴。而在單元測試 AIR 原則中淌哟,單元測試應(yīng)該是可以重復(fù)執(zhí)行的迹卢,不應(yīng)受到外界環(huán)境的影響的辽故。此時我們可以通過 Mock 一個實現(xiàn)來處理這種情況徒仓。

如果不需要對靜態(tài)方法,私有方法等特殊進行驗證測試誊垢,則僅僅使用 Spring boot 自帶的 Mockito 即可完成相關(guān)的測試數(shù)據(jù) Mock掉弛。若需要則可以使用 PowerMock,簡單實用喂走,結(jié)合 Spring 可以使用注解注入殃饿。

@MockBean

SpringBoot 在執(zhí)行單元測試時,會將該注解的 Bean 替換掉 IOC 容器中原生 Bean芋肠。

例如下面代碼中乎芳, ProjectService 中通過 ProjectMapper 的 selectById 方法進行數(shù)據(jù)庫查詢操作:

@Service
public class ProjectService {

    @Autowired
    private ProjectMapper mapper;

    public ProjectDO detail(String id) {
        return mapper.selectById(id);
    }

}

此時我們可以對 Mock 一個 ProjectMapper 對象替換掉 IOC 容器中原生的 Bean,來模擬數(shù)據(jù)庫查詢操作帖池,如:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProjectServiceTest {
  
    @MockBean
    private ProjectMapper mapper;
    @Autowired
    private ProjectService service;

    @Test
    public void detail() {
        ProjectDemoDO model = new ProjectDemoDO();
        model.setId("1");
        model.setName("dubbo-demo");
        Mockito.when(mapper.selectById("1")).thenReturn(model);
        ProjectDemoDO entity = service.detail("1");
        assertThat(entity.getName(), containsString("dubbo-demo"));
    }

}

Mockito 常用方法

Mockito 更多的使用可查看→官方文檔

mock() 對象
List list = mock(List.class);

verify() 驗證互動行為
@Test
public void mockTest() {
    List list = mock(List.class);
  list.add(1);
  // 驗證 add(1) 互動行為是否發(fā)生
  Mockito.verify(list).add(1);
}

when() 模擬期望結(jié)果
@Test
public void mockTest() {
  List list = mock(List.class);
  when(mock.get(0)).thenReturn("hello");
  assertThat(mock.get(0),is("hello"));
}

doThrow() 模擬拋出異常
@Test(expected = RuntimeException.class)
public void mockTest(){
  List list = mock(List.class);
  doThrow(new RuntimeException()).when(list).add(1);
  list.add(1);
}

@Mock 注解

在上面的測試中我們在每個測試方法里都 mock 了一個 List 對象奈惑,為了避免重復(fù)的 mock,使測試類更具有可讀性睡汹,我們可以使用下面的注解方式來快速模擬對象:

// @RunWith(MockitoJUnitRunner.class) 
public class MockitoTest {
    @Mock
    private List list;

    public MockitoTest(){
        // 初始化 @Mock 注解
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shorthand(){
        list.add(1);
        verify(list).add(1);
    }
}

when() 參數(shù)匹配
@Test
public void mockTest(){
    Comparable comparable = mock(Comparable.class);
  //預(yù)設(shè)根據(jù)不同的參數(shù)返回不同的結(jié)果
  when(comparable.compareTo("Test")).thenReturn(1);
  when(comparable.compareTo("Omg")).thenReturn(2);
  assertThat(comparable.compareTo("Test"),is(1));
  assertThat(comparable.compareTo("Omg"),is(2));
  //對于沒有預(yù)設(shè)的情況會返回默認值
   assertThat(list.get(1),is(999));
   assertThat(comparable.compareTo("Not stub"),is(0));
}

Answer 修改對未預(yù)設(shè)的調(diào)用返回默認期望
@Test
public void mockTest(){
  //mock對象使用Answer來對未預(yù)設(shè)的調(diào)用返回默認期望值
  List list = mock(List.class,new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
      return 999;
    }
  });
  //下面的get(1)沒有預(yù)設(shè)肴甸,通常情況下會返回NULL,但是使用了Answer改變了默認期望值
  assertThat(list.get(1),is(999));
  //下面的size()沒有預(yù)設(shè)囚巴,通常情況下會返回0原在,但是使用了Answer改變了默認期望值
  assertThat(list.size(),is(999));
}

spy() 監(jiān)控真實對象

Mock 不是真實的對象,它只是創(chuàng)建了一個虛擬對象彤叉,并可以設(shè)置對象行為庶柿。而 Spy是一個真實的對象,但它可以設(shè)置對象行為秽浇。

@Test(expected = IndexOutOfBoundsException.class)
public void mockTest(){
  List list = new LinkedList();
  List spy = spy(list);
  //下面預(yù)設(shè)的spy.get(0)會報錯浮庐,因為會調(diào)用真實對象的get(0),所以會拋出越界異常
  when(spy.get(0)).thenReturn(3);
  //使用doReturn-when可以避免when-thenReturn調(diào)用真實對象api
  doReturn(999).when(spy).get(999);
  //預(yù)設(shè)size()期望值
  when(spy.size()).thenReturn(100);
  //調(diào)用真實對象的api
  spy.add(1);
  spy.add(2);
  assertThat(spy.size(),is(100));
  assertThat(spy.size(),is(1));
  assertThat(spy.size(),is(2));
  verify(spy).add(1);
  verify(spy).add(2);
  assertThat(spy.get(999),is(999));
}

reset() 重置 mock
@Test
public void reset_mock(){
  List list = mock(List.class);
  when(list.size()).thenReturn(10);
  list.add(1);
    assertThat(list.size(),is(10));
  //重置mock兼呵,清除所有的互動和預(yù)設(shè)
  reset(list);
  assertThat(list.size(),is(0));
}

times() 驗證調(diào)用次數(shù)
@Test
public void verifying_number_of_invocations(){
  List list = mock(List.class);
  list.add(1);
  list.add(2);
  list.add(2);
  list.add(3);
  list.add(3);
  list.add(3);
  //驗證是否被調(diào)用一次兔辅,等效于下面的times(1)
  verify(list).add(1);
  verify(list,times(1)).add(1);
  //驗證是否被調(diào)用2次
  verify(list,times(2)).add(2);
  //驗證是否被調(diào)用3次
  verify(list,times(3)).add(3);
  //驗證是否從未被調(diào)用過
  verify(list,never()).add(4);
  //驗證至少調(diào)用一次
  verify(list,atLeastOnce()).add(1);
  //驗證至少調(diào)用2次
  verify(list,atLeast(2)).add(2);
  //驗證至多調(diào)用3次
  verify(list,atMost(3)).add(3);
}

inOrder() 驗證執(zhí)行順序
@Test
public void verification_in_order(){
  List list = mock(List.class);
  List list2 = mock(List.class);
  list.add(1);
  list2.add("hello");
  list.add(2);
  list2.add("world");
  //將需要排序的mock對象放入InOrder
  InOrder inOrder = inOrder(list,list2);
  //下面的代碼不能顛倒順序,驗證執(zhí)行順序
  inOrder.verify(list).add(1);
  inOrder.verify(list2).add("hello");
  inOrder.verify(list).add(2);
  inOrder.verify(list2).add("world");
}

verifyZeroInteractions() 驗證零互動行為
 @Test
 public void mockTest(){
   List list = mock(List.class);
   List list2 = mock(List.class);
   List list3 = mock(List.class);
   list.add(1);
   verify(list).add(1);
   verify(list,never()).add(2);
   //驗證零互動行為
   verifyZeroInteractions(list2,list3);
 }

verifyNoMoreInteractions() 驗證冗余互動行為
@Test(expected = NoInteractionsWanted.class)
public void mockTest(){
  List list = mock(List.class);
  list.add(1);
  list.add(2);
  verify(list,times(2)).add(anyInt());
  //檢查是否有未被驗證的互動行為击喂,因為add(1)和add(2)都會被上面的anyInt()驗證到维苔,所以下面的代碼會通過
  verifyNoMoreInteractions(list);

  List list2 = mock(List.class);
  list2.add(1);
  list2.add(2);
  verify(list2).add(1);
  //檢查是否有未被驗證的互動行為,因為add(2)沒有被驗證懂昂,所以下面的代碼會失敗拋出異常
  verifyNoMoreInteractions(list2);
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末介时,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沸柔,老刑警劉巖循衰,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異褐澎,居然都是意外死亡,警方通過查閱死者的電腦和手機工三,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來俭正,“玉大人,你說我怎么就攤上這事掸读〈叮” “怎么了儿惫?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長姥闪。 經(jīng)常有香客問我始苇,道長,這世上最難降的妖魔是什么筐喳? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任催式,我火速辦了婚禮,結(jié)果婚禮上避归,老公的妹妹穿的比我還像新娘荣月。我一直安慰自己,他們只是感情好梳毙,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布哺窄。 她就那樣靜靜地躺著,像睡著了一般账锹。 火紅的嫁衣襯著肌膚如雪萌业。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天奸柬,我揣著相機與錄音生年,去河邊找鬼。 笑死廓奕,一個胖子當著我的面吹牛抱婉,可吹牛的內(nèi)容都是我干的档叔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蒸绩,長吁一口氣:“原來是場噩夢啊……” “哼衙四!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起患亿,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤传蹈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后窍育,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卡睦,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡宴胧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年漱抓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恕齐。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡乞娄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出显歧,到底是詐尸還是另有隱情仪或,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布士骤,位于F島的核電站,受9級特大地震影響到旦,放射性物質(zhì)發(fā)生泄漏巨缘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一搁骑、第九天 我趴在偏房一處隱蔽的房頂上張望仲器。 院中可真熱鬧仰冠,春花似錦、人聲如沸沪停。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至郊闯,卻和暖如春蛛株,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欢摄。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工怀挠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留害捕,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓吞滞,卻偏偏與公主長得像裁赠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子组贺,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1.Springboot的測試類庫 SpringBoot 提供了許多實用工具和注解來幫助測試應(yīng)用程序失尖,主要包括以下...
    hadoop_a9bb閱讀 13,783評論 0 9
  • 概述 本文主要介紹單元測試掀潮、集成測試相關(guān)的概念琼富、技術(shù)實現(xiàn)以及最佳實踐。 本文的demo是基于Java語言薯鼠,Spri...
    heyikan閱讀 4,143評論 0 16
  • 單元測試實踐背景 測試環(huán)境定位bug時,需要測試同學(xué)協(xié)助手動發(fā)起相關(guān)業(yè)務(wù)URL請求羞芍,開發(fā)進行遠程調(diào)試問題:1郊艘、遠程...
    Zeng_小洲閱讀 7,671評論 0 4
  • 1 概述 總所周知,測試是軟件開發(fā)中一個非常重要的環(huán)節(jié)畏浆,用來驗證程序運行是否符合預(yù)期(這個預(yù)期包括了程序的正確性狞贱、...
    yeonon閱讀 661評論 0 0
  • 1斥滤、今天殷先生過生日,全家人都情緒大好,不過對于小朋友來說草娜,沒有蛋糕是個遺憾~ 2、親子陪伴:放學(xué)后直奔爺爺奶奶家...
    Emmaluo閱讀 178評論 0 0