Spring Boot中編寫單元測試


編寫單元測試可以幫助開發人員編寫高質量的代碼,提升代碼質量,減少Bug,便於重構。Spring Boot提供了一些實用程序和註解,用來幫助我們測試應用程序,在Spring Boot中開啓單元測試只需引入spring-boot-starter-test即可,其包含了一些主流的測試庫。本文主要介紹基於 Service和Controller的單元測試。
引入spring-boot-starter-test:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

運行Maven命令dependency:tree可看到其包含了以下依賴:

[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:1.5.9.RELEASE:test
[INFO] |  +- org.springframework.boot:spring-boot-test:jar:1.5.9.RELEASE:test
[INFO] |  +- org.springframework.boot:spring-boot-test-autoconfigure:jar:1.5.9.RELEASE:test
[INFO] |  +- com.jayway.jsonpath:json-path:jar:2.2.0:test
[INFO] |  |  +- net.minidev:json-smart:jar:2.2.1:test
[INFO] |  |  |  \- net.minidev:accessors-smart:jar:1.1:test
[INFO] |  |  |     \- org.ow2.asm:asm:jar:5.0.3:test
[INFO] |  |  \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] |  +- junit:junit:jar:4.12:test
[INFO] |  +- org.assertj:assertj-core:jar:2.6.0:test
[INFO] |  +- org.mockito:mockito-core:jar:1.10.19:test
[INFO] |  |  \- org.objenesis:objenesis:jar:2.1:test
[INFO] |  +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] |  +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO] |  +- org.skyscreamer:jsonassert:jar:1.4.0:test
[INFO] |  |  \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] |  +- org.springframework:spring-core:jar:4.3.13.RELEASE:compile
[INFO] |  \- org.springframework:spring-test:jar:4.3.13.RELEASE:test

JUnit,標準的單元測試Java應用程序;
Spring Test & Spring Boot Test,對Spring Boot應用程序的單元測試提供支持;
Mockito, Java mocking框架,用於模擬任何Spring管理的Bean,比如在單元測試中模擬一個第三方系統Service接口返回的數據,而不會去真正調用第三方系統;
AssertJ,一個流暢的assertion庫,同時也提供了更多的期望值與測試返回值的比較方式;
Hamcrest,庫的匹配對象(也稱爲約束或謂詞);
JsonPath,提供類似XPath那樣的符號來獲取JSON數據片段;
JSONassert,對JSON對象或者JSON字符串斷言的庫。

一個標準的Spring Boot測試單元應有如下的代碼結構:
@SpringBootTest這個註解所在的文件一定要和註解@SpringBootApplication在同個層級的文件目錄底下,不然會報錯

import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
	
}

知識準備

JUnit4註解
JUnit4中包含了幾個比較重要的註解:@BeforeClass、@AfterClass、@Before、@After和@Test。其中, @BeforeClass和@AfterClass在每個類加載的開始和結束時運行,必須爲靜態方法;而@Before和@After則在每個測試方法開始之前和結束之後運行。見如下例子:

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestApplicationTests {
    @BeforeClass
    public static void beforeClassTest() {
        System.out.println("before class test");
    }
    
    @Before
    public void beforeTest() {
        System.out.println("before test");
    }
    
    @Test
    public void Test1() {
        System.out.println("test 1+1=2");
        Assert.assertEquals(2, 1 + 1);
    }
    
    @Test
    public void Test2() {
        System.out.println("test 2+2=4");
        Assert.assertEquals(4, 2 + 2);
    }
    
    @After
    public void afterTest() {
        System.out.println("after test");
    }
    
    @AfterClass
    public static void afterClassTest() {
        System.out.println("after class test");
    }
}

運行輸出如下:

...
before class test
before test
test 1+1=2
after test
before test
test 2+2=4
after test
after class test
...

從上面的輸出可以看出各個註解的運行時機。

Assert
上面代碼中,我們使用了Assert類提供的assert口方法,下面列出了一些常用的assert方法:

assertEquals(“message”,A,B),判斷A對象和B對象是否相等,這個判斷在比較兩個對象時調用了equals()方法。

assertSame(“message”,A,B),判斷A對象與B對象是否相同,使用的是==操作符。

assertTrue(“message”,A),判斷A條件是否爲真。

assertFalse(“message”,A),判斷A條件是否不爲真。

assertNotNull(“message”,A),判斷A對象是否不爲null。

assertArrayEquals(“message”,A,B),判斷A數組與B數組是否相等。

MockMvc
下文中,對Controller的測試需要用到MockMvc技術。MockMvc,從字面上來看指的是模擬的MVC,即其可以模擬一個MVC環境,向Controller發送請求然後得到響應。

在單元測試中,使用MockMvc前需要進行初始化,如下所示:

private MockMvc mockMvc;
@Autowired
private WebApplicationContext wac;
@Before
public void setupMockMvc(){
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

MockMvc模擬MVC請求

模擬一個get請求:

mockMvc.perform(MockMvcRequestBuilders.get("/hello?name={name}",“mrbird”));
模擬一個post請求:

mockMvc.perform(MockMvcRequestBuilders.post("/user/{id}", 1));
模擬文件上傳:

mockMvc.perform(MockMvcRequestBuilders.fileUpload("/fileupload").file(“file”, “文件內容”.getBytes(“utf-8”)));
模擬請求參數:

// 模擬發送一個message參數,值爲hello
mockMvc.perform(MockMvcRequestBuilders.get("/hello").param(“message”, “hello”));
// 模擬提交一個checkbox值,name爲hobby,值爲sleep和eat
mockMvc.perform(MockMvcRequestBuilders.get("/saveHobby").param(“hobby”, “sleep”, “eat”));
也可以直接使用MultiValueMap構建參數:

MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add("name", "mrbird");
params.add("hobby", "sleep");
params.add("hobby", "eat");
mockMvc.perform(MockMvcRequestBuilders.get("/hobby/save").params(params));

模擬發送JSON參數:

String jsonStr = "{\"username\":\"Dopa\",\"passwd\":\"ac3af72d9f95161a502fd326865c2f15\",\"status\":\"1\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(jsonStr.getBytes()));

實際測試中,要手動編寫這麼長的JSON格式字符串很繁瑣也很容易出錯,可以藉助Spring Boot自帶的Jackson技術來序列化一個Java對象(可參考Spring Boot中的JSON技術),如下所示:

User user = new User();
user.setUsername("Dopa");
user.setPasswd("ac3af72d9f95161a502fd326865c2f15");
user.setStatus("1");
String userJson = mapper.writeValueAsString(user);
mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(userJson.getBytes()));

其中,mapper爲com.fasterxml.jackson.databind.ObjectMapper對象。

模擬Session和Cookie:

mockMvc.perform(MockMvcRequestBuilders.get("/index").sessionAttr(name, value));
mockMvc.perform(MockMvcRequestBuilders.get("/index").cookie(new Cookie(name, value)));

設置請求的Content-Type:

mockMvc.perform(MockMvcRequestBuilders.get("/index").contentType(MediaType.APPLICATION_JSON_UTF8));

設置返回格式爲JSON:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).accept(MediaType.APPLICATION_JSON));

模擬HTTP請求頭:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).header(name, values));

MockMvc處理返回結果

期望成功調用,即HTTP Status爲200:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
    .andExpect(MockMvcResultMatchers.status().isOk());

期望返回內容是application/json:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
    .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));

檢查返回JSON數據中某個值的內容:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
    .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("mrbird"));

這裏使用到了jsonPath,$代表了JSON的根節點。更多關於jsonPath的介紹可參考 https://github.com/json-path/JsonPath。

判斷Controller方法是否返回某視圖:

mockMvc.perform(MockMvcRequestBuilders.post("/index"))
    .andExpect(MockMvcResultMatchers.view().name("index.html"));

比較Model:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
    .andExpect(MockMvcResultMatchers.model().size(1))
    .andExpect(MockMvcResultMatchers.model().attributeExists("password"))
    .andExpect(MockMvcResultMatchers.model().attribute("username", "mrbird"));

比較forward或者redirect:

mockMvc.perform(MockMvcRequestBuilders.get("/index"))
    .andExpect(MockMvcResultMatchers.forwardedUrl("index.html"));

// 或者
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
    .andExpect(MockMvcResultMatchers.redirectedUrl("index.html"));
比較返回內容,使用content():

// 返回內容爲hello
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
    .andExpect(MockMvcResultMatchers.content().string("hello"));
// 返回內容是XML,並且與xmlCotent一樣
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
    .andExpect(MockMvcResultMatchers.content().xml(xmlContent));
// 返回內容是JSON ,並且與jsonContent一樣
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
    .andExpect(MockMvcResultMatchers.content().json(jsonContent));

輸出響應結果:

mockMvc.perform(MockMvcRequestBuilders.get("/index"))
    .andDo(MockMvcResultHandlers.print());

測試Service

現有如下Service:

@Repository("userService")
public class UserServiceImpl extends BaseService<User> implements UserService {
    @Override
    public User findByName(String userName) {
        Example example = new Example(User.class);
        example.createCriteria().andCondition("username=", userName);
        List<User> userList = this.selectByExample(example);
        if (userList.size() != 0)
            return userList.get(0);
        else
            return null;
    }
}

編寫一個該Service的單元測試,測試findByName方法是否有效:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
    @Autowired
    UserService userService;
    @Test
    public void test() {
        User user = this.userService.findByName("scott");
        Assert.assertEquals("用戶名爲scott", "scott", user.getUsername());
    }
}

運行後,JUnit沒有報錯說明測試通過,即UserService的findByName方法可行。

此外,和在Controller中引用Service相比,在測試單元中對Service測試完畢後,數據能自動回滾,只需要在測試方法上加上@Transactional註解,比如:

@Test
@Transactional
public void test() {
    User user = new User();
    user.setId(this.userService.getSequence("seq_user"));
    user.setUsername("JUnit");
    user.setPasswd("123456");
    user.setStatus("1");
    user.setCreateTime(new Date());
    this.userService.save(user);
}

運行,測試通過,查看數據庫發現數據並沒有被插入,這樣很好的避免了不必要的數據污染。

測試Controller

現有如下Controller:

@RestController
public class UserController {
    @Autowired
    UserService userService;
    @GetMapping("user/{userName}")
    public User getUserByName(@PathVariable(value = "userName") String userName) {
        return this.userService.findByName(userName);
    }
    @PostMapping("user/save")
    public void saveUser(@RequestBody User user) {
        this.userService.saveUser(user);
    }
}

現在編寫一個針對於該ControllergetUserByName(@PathVariable(value = “userName”) String userName)方法的測試類:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
    private MockMvc mockMvc;
    
    @Autowired
    private WebApplicationContext wac;
    
    @Before
    public void setupMockMvc(){
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    
    @Test
    public void test() throws Exception {
        mockMvc.perform(
            MockMvcRequestBuilders.get("/user/{userName}", "scott")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("scott"))
        .andDo(MockMvcResultHandlers.print());
    }
}

運行後,JUnit通過,控制檯輸出過程如下所示:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /user/scott
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}
Handler:
             Type = demo.springboot.test.controller.UserController
           Method = public demo.springboot.test.domain.User demo.springboot.test.controller.UserController.getUserByName(java.lang.String)
Async:
    Async started = false
     Async result = null
Resolved Exception:
             Type = null
ModelAndView:
        View name = null
             View = null
            Model = null
FlashMap:
       Attributes = null
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body = {"id":23,"username":"scott","passwd":"ac3af72d9f95161a502fd326865c2f15","createTime":1514535399000,"status":"1"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

繼續編寫一個針對於該ControllersaveUser(@RequestBody User user)方法的測試類:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
    private MockMvc mockMvc;
    
    @Autowired
    private WebApplicationContext wac;
    
    @Autowired
    ObjectMapper mapper;
    
    
    @Before
    public void setupMockMvc(){
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
	
    @Test
    @Transactional
    public void test() throws Exception {
        User user = new User();
        user.setUsername("Dopa");
        user.setPasswd("ac3af72d9f95161a502fd326865c2f15");
        user.setStatus("1");
        
        String userJson = mapper.writeValueAsString(user);
        mockMvc.perform(
            MockMvcRequestBuilders.post("/user/save")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(userJson.getBytes()))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print());
    }
}

運行過程如下所示:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /user/save
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}
Handler:
             Type = demo.springboot.test.controller.UserController
           Method = public void demo.springboot.test.controller.UserController.saveUser(demo.springboot.test.domain.User)
Async:
    Async started = false
     Async result = null
Resolved Exception:
             Type = null
ModelAndView:
        View name = null
             View = null
            Model = null
FlashMap:
       Attributes = null
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

值得注意的是,在一個完整的系統中編寫測試單元時,可能需要模擬一個登錄用戶信息Session,MockMvc也提供瞭解決方案,可在初始化的時候模擬一個HttpSession:

private MockMvc mockMvc;
private MockHttpSession session;
@Autowired
private WebApplicationContext wac;
@Before
public void setupMockMvc(){
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    session = new MockHttpSession();
    User user =new User();
    user.setUsername("Dopa");
    user.setPasswd("ac3af72d9f95161a502fd326865c2f15");
    session.setAttribute("user", user); 
}

源碼鏈接:https://github.com/wuyouzhuguli/Spring-Boot-Demos/tree/master/19.Spring-Boot-Testing

轉載自https://mrbird.cc/Spring-Boot%20TESTing.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章