springboot單元測試


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

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

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

相關注解

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

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

@SpringBootTest

SpringBoot提供了一個 @SpringBootTest 注解用於測試 SpringBoot 應用,它可以用作標准 spring-test @ContextConfiguration 注釋的替代方法,其原理是通過 SpringApplication 在測試中創建ApplicationContext。

1 @RunWith(SpringRunner.class)
2 @SpringBootTest
3 public class ApplicationTest {
4 }

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

  • webEnvironment:指定Web應用環境,它可以是以下值
    • MOCK:提供一個模擬的 Servlet 環境,內置的 Servlet 容器沒有啟動,配合可以與@AutoConfigureMockMvc 結合使用,用於基於 MockMvc 的應用程序測試。
    • RANDOM_PORT:加載一個 EmbeddedWebApplicationContext 並提供一個真正嵌入式的 Servlet 環境,隨機端口。
    • DEFINED_PORT:加載一個 EmbeddedWebApplicationContext 並提供一個真正嵌入式的 Servlet 環境,默認端口 8080 或由配置文件指定。
    • NONE:使用 SpringApplication 加載 ApplicationContext,但不提供任何 servlet 環境。
  • classes:指定應用啟動類,通常情況下無需設置,因為 SpringBoot 會自動搜索,直到找到 @SpringBootApplication 或 @SpringBootConfiguration 注解。

單元測試回滾

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

但是如果使用 RANDOM_PORT 或 DEFINED_PORT 這種真正的 Servlet 環境,HTTP 客戶端和服務器將在不同的線程中運行,從而分離事務。 在這種情況下,在服務器上啟動的任何事務都不會回滾。

斷言

JUnit4 結合 Hamcrest 提供了一個全新的斷言語法——assertThat,結合 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));

// 數值匹配符
double d = new C().div(10, 3);
// closeTo:浮點型變量的值在3.0±0.5范圍內,測試通過
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:字符串變量以指定字符串結尾時,測試通過
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"))

基本的單元測試例子

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

1 @Service
2 public class UserService {
3 
4     public String getName() {
5         return "lyTongXue";
6     }
7     
8 }
 1 @RunWith(SpringRunner.class)
 2 @SpringBootTest
 3 public class UserServiceTest {
 4 
 5     @Autowired
 6     private UserService service;
 7 
 8     @Test
 9     public void getName() {
10         String name = service.getName();
11         assertThat(name,is("lyTongXue"));
12     }
13 
14 }

Controller 測試

Spring 提供了 MockMVC 用於支持 RESTful 風格的 Spring MVC 測試,使用 MockMvcBuilder 來構造MockMvc 實例。MockMvc 有兩個實現:

  • StandaloneMockMvcBuilder:指定 WebApplicationContext,它將會從該上下文獲取相應的控制器並得到相應的 MockMvc

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

    1 @RunWith(SpringRunner.class)
    2 public class UserControllerTest  {
    3     private MockMvc mockMvc;
    4     @Before
    5     public void setUp() throws Exception {
    6         mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    7     } 
    8 } 

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

 1 @RestController
 2 @RequestMapping("v1/users")
 3 public class UserController {
 4 
 5     @GetMapping("/{id}")
 6     public User get(@PathVariable("id") String id) {
 7         return new User(1, "lyTongXue");
 8     }
 9 
10     @Data
11     @AllArgsConstructor
12     public class User {
13         private Integer id;
14         private String name;
15     }
16 
17 }
 1 // ...
 2 import static org.hamcrest.Matchers.containsString;
 3 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 4 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
 5 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 6 
 7 @RunWith(SpringRunner.class)
 8 @SpringBootTest
 9 public class UserControllerTest {
10 
11     @Autowired
12     private WebApplicationContext webApplicationContext;
13     private MockMvc mockMvc;
14 
15     @Before
16     public void setUp() {
17         mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
18     }
19 
20     @Test
21     public void getUser() {
22         mockMvc.perform(get("/v1/users/1")
23                 .accept(MediaType.APPLICATION_JSON_UTF8))
24                 .andExpect(status().isOk())
25            .andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
26     }
27   
28 }
 

方法描述

  • perform:執行一個 RequestBuilder 請求,返回一個 ResultActions 實例對象,可對請求結果進行期望與其它操作

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

  • andExpect:添加 ResultMatcher 驗證規則,驗證請求結果是否正確,驗證規則可查閱→MockMvcResultMatchers 文檔

  • andDo:添加 ResultHandler 結果處理器,比如調試時打印結果到控制台,更多處理器可查閱→MockMvcResultHandlers 文檔

  • andReturn:返回執行請求的結果,該結果是一個恩 MvcResult 實例對象→MvcResult 文檔

Mock 數據

在單元測試中,Service 層的調用往往涉及到對數據庫、中間件等外部依賴。而在單元測試 AIR 原則中,單元測試應該是可以重復執行的,不應受到外界環境的影響的。此時我們可以通過 Mock 一個實現來處理這種情況。

如果不需要對靜態方法,私有方法等特殊進行驗證測試,則僅僅使用 Spring boot 自帶的 Mockito 即可完成相關的測試數據 Mock。若需要則可以使用 PowerMock,簡單實用,結合 Spring 可以使用注解注入。

@MockBean

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

例如下面代碼中, ProjectService 中通過 ProjectMapper 的 selectById 方法進行數據庫查詢操作:

 1 @Service
 2 public class ProjectService {
 3 
 4     @Autowired
 5     private ProjectMapper mapper;
 6 
 7     public ProjectDO detail(String id) {
 8         return mapper.selectById(id);
 9     }
10 
11 }

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

 1 @RunWith(SpringRunner.class)
 2 @SpringBootTest
 3 public class ProjectServiceTest {
 4   
 5     @MockBean
 6     private ProjectMapper mapper;
 7     @Autowired
 8     private ProjectService service;
 9 
10     @Test
11     public void detail() {
12         ProjectDemoDO model = new ProjectDemoDO();
13         model.setId("1");
14         model.setName("dubbo-demo");
15         Mockito.when(mapper.selectById("1")).thenReturn(model);
16         ProjectDemoDO entity = service.detail("1");
17         assertThat(entity.getName(), containsString("dubbo-demo"));
18     }
19 
20 }

Mockito 常用方法

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

mock() 對象
1 List list = mock(List.class);
verify() 驗證互動行為
1 @Test
2 public void mockTest() {
3     List list = mock(List.class);
4   list.add(1);
5   // 驗證 add(1) 互動行為是否發生
6   Mockito.verify(list).add(1);
7 }
when() 模擬期望結果
1 @Test
2 public void mockTest() {
3   List list = mock(List.class);
4   when(mock.get(0)).thenReturn("hello");
5   assertThat(mock.get(0),is("hello"));
6 }
doThrow() 模擬拋出異常
1 @Test(expected = RuntimeException.class)
2 public void mockTest(){
3   List list = mock(List.class);
4   doThrow(new RuntimeException()).when(list).add(1);
5   list.add(1);
6 }
@Mock 注解

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

 1 // @RunWith(MockitoJUnitRunner.class) 
 2 public class MockitoTest {
 3     @Mock
 4     private List list;
 5 
 6     public MockitoTest(){
 7           // 初始化 @Mock 注解
 8         MockitoAnnotations.initMocks(this);
 9     }
10 
11     @Test
12     public void shorthand(){
13         list.add(1);
14         verify(list).add(1);
15     }
16 }
when() 參數匹配
 1 @Test
 2 public void mockTest(){
 3     Comparable comparable = mock(Comparable.class);
 4   //預設根據不同的參數返回不同的結果
 5   when(comparable.compareTo("Test")).thenReturn(1);
 6   when(comparable.compareTo("Omg")).thenReturn(2);
 7   assertThat(comparable.compareTo("Test"),is(1));
 8   assertThat(comparable.compareTo("Omg"),is(2));
 9   //對於沒有預設的情況會返回默認值
10    assertThat(list.get(1),is(999));
11    assertThat(comparable.compareTo("Not stub"),is(0));
12 }
Answer 修改對未預設的調用返回默認期望
 1 @Test
 2 public void mockTest(){
 3   //mock對象使用Answer來對未預設的調用返回默認期望值
 4   List list = mock(List.class,new Answer() {
 5     @Override
 6     public Object answer(InvocationOnMock invocation) throws Throwable {
 7       return 999;
 8     }
 9   });
10   //下面的get(1)沒有預設,通常情況下會返回NULL,但是使用了Answer改變了默認期望值
11   assertThat(list.get(1),is(999));
12   //下面的size()沒有預設,通常情況下會返回0,但是使用了Answer改變了默認期望值
13   assertThat(list.size(),is(999));
14 }
spy() 監控真實對象

Mock 不是真實的對象,它只是創建了一個虛擬對象,並可以設置對象行為。而 Spy是一個真實的對象,但它可以設置對象行為。

 1 @Test(expected = IndexOutOfBoundsException.class)
 2 public void mockTest(){
 3   List list = new LinkedList();
 4   List spy = spy(list);
 5   //下面預設的spy.get(0)會報錯,因為會調用真實對象的get(0),所以會拋出越界異常
 6   when(spy.get(0)).thenReturn(3);
 7   //使用doReturn-when可以避免when-thenReturn調用真實對象api
 8   doReturn(999).when(spy).get(999);
 9   //預設size()期望值
10   when(spy.size()).thenReturn(100);
11   //調用真實對象的api
12   spy.add(1);
13   spy.add(2);
14   assertThat(spy.size(),is(100));
15   assertThat(spy.size(),is(1));
16   assertThat(spy.size(),is(2));
17   verify(spy).add(1);
18   verify(spy).add(2);
19   assertThat(spy.get(999),is(999));
20 }
reset() 重置 mock
 1 @Test
 2 public void reset_mock(){
 3   List list = mock(List.class);
 4   when(list.size()).thenReturn(10);
 5   list.add(1);
 6     assertThat(list.size(),is(10));
 7   //重置mock,清除所有的互動和預設
 8   reset(list);
 9   assertThat(list.size(),is(0));
10 }
times() 驗證調用次數
 1 @Test
 2 public void verifying_number_of_invocations(){
 3   List list = mock(List.class);
 4   list.add(1);
 5   list.add(2);
 6   list.add(2);
 7   list.add(3);
 8   list.add(3);
 9   list.add(3);
10   //驗證是否被調用一次,等效於下面的times(1)
11   verify(list).add(1);
12   verify(list,times(1)).add(1);
13   //驗證是否被調用2次
14   verify(list,times(2)).add(2);
15   //驗證是否被調用3次
16   verify(list,times(3)).add(3);
17   //驗證是否從未被調用過
18   verify(list,never()).add(4);
19   //驗證至少調用一次
20   verify(list,atLeastOnce()).add(1);
21   //驗證至少調用2次
22   verify(list,atLeast(2)).add(2);
23   //驗證至多調用3次
24   verify(list,atMost(3)).add(3);
25 }
inOrder() 驗證執行順序
 1 @Test
 2 public void verification_in_order(){
 3   List list = mock(List.class);
 4   List list2 = mock(List.class);
 5   list.add(1);
 6   list2.add("hello");
 7   list.add(2);
 8   list2.add("world");
 9   //將需要排序的mock對象放入InOrder
10   InOrder inOrder = inOrder(list,list2);
11   //下面的代碼不能顛倒順序,驗證執行順序
12   inOrder.verify(list).add(1);
13   inOrder.verify(list2).add("hello");
14   inOrder.verify(list).add(2);
15   inOrder.verify(list2).add("world");
16 }
verifyZeroInteractions() 驗證零互動行為
 1  @Test
 2  public void mockTest(){
 3    List list = mock(List.class);
 4    List list2 = mock(List.class);
 5    List list3 = mock(List.class);
 6    list.add(1);
 7    verify(list).add(1);
 8    verify(list,never()).add(2);
 9    //驗證零互動行為
10    verifyZeroInteractions(list2,list3);
11  }
verifyNoMoreInteractions() 驗證冗余互動行為
 1 @Test(expected = NoInteractionsWanted.class)
 2 public void mockTest(){
 3   List list = mock(List.class);
 4   list.add(1);
 5   list.add(2);
 6   verify(list,times(2)).add(anyInt());
 7   //檢查是否有未被驗證的互動行為,因為add(1)和add(2)都會被上面的anyInt()驗證到,所以下面的代碼會通過
 8   verifyNoMoreInteractions(list);
 9 
10   List list2 = mock(List.class);
11   list2.add(1);
12   list2.add(2);
13   verify(list2).add(1);
14   //檢查是否有未被驗證的互動行為,因為add(2)沒有被驗證,所以下面的代碼會失敗拋出異常
15   verifyNoMoreInteractions(list2);
16 }
 
 注:
1. 如果使用異步的servlet,不能用StandaloneMockMvcBuilder方式進行測試,AsyncContext.getResponse得出的response是一個null,即使加上@SpringTest加載了上下文也是這樣
2.@SpringBootTest會開啟模擬容器來模擬正式運行,所以會加載相關注解(component)和配置
3.可以使用maven-surefire-plugin的<argLine>或者mvn test -DargLine在測試的時候jvm啟動參數或者增加啟動命令,比如我的程序執行需要用javaagent去修改字節碼,則可以使用:
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <argLine>-javaagent:xxxx xxx.jar</argLine>
                </configuration>
            </plugin>

或者

mvn test -DargLine=-javaagent xxxx xxx.jar

4.如果想設置默認跳過單測,可以用maven-surefire-plugin設置skipTests=${skipTests},然后如果想進行單測,則可以直接mvn test -DskipTests=true。skipTests優先級為configuration>命令>properties中配置的。是否跳過單測最終結果為skipTests||maven.test.skip

            <properties>
                 <skipTests>true</skipTests>
            </properties>

           <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <skipTests>${skipTests}</skipTests>
                </configuration>
            </plugin>

5.maven多module項目中千萬不要引入其他模塊的單元測試代碼: 

經過參考一些Maven的資料得知,其工作機制實際上是包的依賴管理。在規定的標准目錄下,能夠在模塊之間引用的代碼只能存在於main目錄下。而單元測試(test目錄下的代碼)模型是建立在“獨立”的思想之上的,目的就是不受其他環境的干擾從而純粹地驗證自身模塊的可用性和正確性。因此單元測試代碼之間是不能被其他模塊引用的

6.如果使用了jacoco,它會依賴maven-surefire-plugin的argLine,所以如果你在該插件中用了argLine,建議按以下方式增加${argLine}

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <argLine>${argLine} -javaagent:xxxx xxx.jar</argLine>
                </configuration>
            </plugin>

 

參考:

https://juejin.im/post/5d62cc3ee51d45620b21c3e9

https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html

http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html

https://blog.csdn.net/chaijunkun/article/details/35796335

https://stackoverflow.com/questions/18107375/getting-skipping-jacoco-execution-due-to-missing-execution-data-file-upon-exec

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM