【SpringBoot】單元測試

如果餓了就吃,困了就睡,渴了就喝,人生就太無趣了
源碼地址:https://github.com/keer123456789/springbootstudy/tree/master/mybatisdemo


1.單元測試

單元測試是編寫單元測試類,針對類級別的測試。比如使用Junit框架,針對一個類,寫一個測試類,測試目標類的大部分主要方法。
需要注意單元測試的級別是方法。項目當中,類之間的依賴調用是很常見的事,如果你要測試一個類,而這個目標類又調用了另一個類,那麼在測試時就沒有遵守“在一個類範圍內進行測試”,自然算不得單元測試。
如圖1:A、B、C、D類存在依賴關係,如果對A類進行單元測試,就需要採取Mock方式對依賴B類C類進行模擬。

在這裏插入圖片描述

2.項目介紹

此次使用的項目使用springboot+mybatis對數據庫進行增刪改查操作的功能,分別對項目的ControllerService, Dao三層進行單元測試。

2.1 Controller 單元測試

由於controller類相較於其他bean,功能比較特殊,負責接收HTTP請求,返回HTTP消息,但是單元測試不能手動發送HTTP請求,所以使用@WebMvcTest註解對測試類進行註解。然後使用MockMvc模擬請求。

2.1.1 Controller 邏輯

項目中的PeopleController控制器,其中不僅接收了外部HTTP請求,而且對PeopleService存在依賴,不僅要對請求進行mock,還要對PeopleService進行模擬

@RestController
public class PeopleController {
    protected Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    PeopleService peopleService;

    @GetMapping("/getAllPeopleInfo")
    public WebResult getAllPeopleInfo() {
        logger.info("接收到請求:/getAllPeopleInfo");
        return peopleService.getAllPeopleInfo();
    }

    @PostMapping("/addPeopleInfo")
    public WebResult addPeopleInfo(@RequestBody People people) {
        logger.info("接收到請求:/getAllPeopleInfo");
        return peopleService.addPeopleInfo(people);
    }
	
	//other method……
}

2.1.2 Controller Test 邏輯

  1. PeopleService進行Mock,使用Mockito進行模擬。
	@MockBean
    PeopleService peopleService;

    @Before
    public void setup() {
        WebResult webResult = new WebResult();
        webResult.setStatus(WebResult.SUCCESS);

        Mockito.when(peopleService.getAllPeopleInfo()).thenReturn(webResult);
        Mockito.when(peopleService.addPeopleInfo(Mockito.any())).thenReturn(webResult);
        Mockito.when(peopleService.getPeopleInfoByID(Mockito.anyInt())).thenReturn(webResult);
        Mockito.when(peopleService.updatePeopleNameByID(Mockito.anyString(), Mockito.anyInt())).thenReturn(webResult);
        Mockito.when(peopleService.deletePeopleInfoByID(Mockito.anyInt())).thenReturn(webResult);
    }

因爲PeopleService中的方法都是返回WebResult實例。對於返回的WebResult實例,模擬返回status屬性爲0(SUCCESS)

public class WebResult<T> {
    public static final int SUCCESS = 0;
    public static final int ERROR = 1;
    private int status;
    private T data;
    private String message;

    public WebResult() {
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
  1. 編寫測試代碼
    對其中兩個接口進行了測試,一個使用Get請求,一個是Post請求。第一個.andExpect(MockMvcResultMatchers.status().isOk())是判斷請求狀態是否正確。第二個.andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("0")));是對PeopleService中的返回值進行測試判斷。
@RunWith(SpringRunner.class)
@DisplayName("人員API接口測試")
@WebMvcTest(PeopleController.class)
public class PeopleControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    PeopleService peopleService;

    @Before
    public void setup() {
        WebResult webResult = new WebResult();
        webResult.setStatus(WebResult.SUCCESS);

        Mockito.when(peopleService.getAllPeopleInfo()).thenReturn(webResult);
        Mockito.when(peopleService.addPeopleInfo(Mockito.any())).thenReturn(webResult);
        Mockito.when(peopleService.getPeopleInfoByID(Mockito.anyInt())).thenReturn(webResult);
        Mockito.when(peopleService.updatePeopleNameByID(Mockito.anyString(), Mockito.anyInt())).thenReturn(webResult);
        Mockito.when(peopleService.deletePeopleInfoByID(Mockito.anyInt())).thenReturn(webResult);
    }

    @Test
    @DisplayName(value = "測試獲取全部信息接口")
    public void testGetAllPeopleInfo() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/getAllPeopleInfo"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("0")));
    }

    @Test
    @DisplayName(value = "增加人員接口")
    public void testAddPeopleInfo() throws Exception {
        Gson gson = new Gson();
        People people = new People("java", 1, 12, "spring");
        String json = gson.toJson(people);
        mockMvc.perform(MockMvcRequestBuilders
                .post("/addPeopleInfo")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("0")));
    }

	//other method……
}
  1. spring環境問題
  • @WebMvcTest就像@SpringBootTest一樣,默認搜索@SpringBootConfiguration註解的類作爲配置類。一般情況下,基於Spring-Boot的web應用,會創建一個啓動類,並使用@SpringBootApplication,這個註解可看作@SpringBootConfiguration註解的擴展,所以很可能會搜索到這個啓動類作爲配置。
  • 如果項目當中有多個@SpringBootConfiguration配置類,比如有些其他的測試類創建了內部配置類,並且使用了這個註解。如果當前測試類沒有使用內部類,也沒有使用classes屬性指定使用哪個配置類,就會因爲找到了多個配置類而失敗。這種情況下會有明確的錯誤提示信息。
  • 另外一個可能的問題是:如果配置類上添加了其他的註解,比如Mybatis框架的@MapperScan註解,那麼Spring會去嘗試實例化Mapper實例,但是因爲我們使用的是@WebMvcTest註解,Spring不會去實例化Mapper所依賴的sqlSessionFactory等自動配置的組件,最終導致依賴註解失敗,無法構建Spring上下文環境。

針對這種問題:採用是通過使用內部類來自定義配置。內部類只有一個@SpringBootApplication註解,指定了掃描的根路徑,以縮小bean的掃描範圍。

@RunWith(SpringRunner.class)
@DisplayName("人員API接口測試")
@WebMvcTest(PeopleController.class)
public class PeopleControllerTest {
    @SpringBootApplication(scanBasePackages = {"com.keer.mybatisdemo.controller"})
    static class InnerConfig {
    }

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    PeopleService peopleService;
}

2.2 Service 單元測試

2.2.1 Service邏輯

service層的依賴只有Dao層的mapper,所以只需要對底層的PeopleMappermapper進行mock模擬。

@Service
public class PeopleServiceImpl implements PeopleService {
    protected Logger logger = LoggerFactory.getLogger(this.getClass());

    PeopleMapper peopleMapper;

    @Override
    public WebResult getAllPeopleInfo() {
        WebResult webResult = new WebResult();
        List<People> list = peopleMapper.getAllPeopleInfo();
        webResult.setStatus(WebResult.SUCCESS);
        webResult.setMessage("select all people info success ");
        webResult.setData(list);
        logger.info("select all people info success ,data:" + list.toString());
        return webResult;
    }

    @Override
    public WebResult addPeopleInfo(People people) {
        WebResult webResult = new WebResult();
        if (peopleMapper.addPeopleInfo(people) == 1) {
            webResult.setData(1);
            webResult.setMessage("add people info success");
            webResult.setStatus(WebResult.SUCCESS);
            logger.info("add people info success");
        } else {
            webResult.setStatus(WebResult.ERROR);
            webResult.setMessage("add people info fail");
            webResult.setData(0);
            logger.error("add people info fail");
        }
        return webResult;
    }

	//other method……

}

2.2.2 Service Test

因爲沒有啓動spring容器,@Autowird自動注入功能消失,此時採取@InjectMocks進行bean的注入

  • @InjectMocks創建一個PeopleServiceImpl實例,將mock 的bean注入該實例中。
  • @Mock模擬一個bean。
@RunWith(SpringRunner.class)
public class PeopleServiceImplTest {
    @InjectMocks
    private PeopleServiceImpl peopleService;

    @Mock
    private PeopleMapper peopleMapper;

    @Before
    public void setup() {
        People bob = new People("bob", 1, 15, "北京");
        People alex = new People("alex", 2, 20, "天津");
        People john = new People("john", 3, 25, "湖北");
        List<People> allPeople = Arrays.asList(john, bob, alex);

        Mockito.when(peopleMapper.getPeopleInfoByID(alex.getId())).thenReturn(alex);
        Mockito.when(peopleMapper.getPeopleInfoByID(-1)).thenReturn(null);
        Mockito.when(peopleMapper.getAllPeopleInfo()).thenReturn(allPeople);
        Mockito.when(peopleMapper.updatePeopleNameByID("alexChange", alex.getId())).thenReturn(1);
        Mockito.when(peopleMapper.deletePeopleInfoByID(john.getId())).thenReturn(1);
    }

    @Test
    @DisplayName(value = "輸入正確id查看返回結果是否正確")
    public void whenValidId_thenPeopleShouldBeFound() {
        int alexID = 2;
        WebResult webResult = peopleService.getPeopleInfoByID(alexID);
        People people = (People) webResult.getData();
        Assertions.assertThat(people.getId()).isEqualTo(alexID);
        Mockito.verify(peopleMapper, VerificationModeFactory.times(1)).getPeopleInfoByID(Mockito.anyInt());
    }

	@Test
    @DisplayName(value = "插入人員信息")
    public void addPeopleInfo_thenReturnSuccess() {
        People bob = new People("bob", 1, 15, "北京");
        Mockito.when(peopleMapper.addPeopleInfo(bob)).thenReturn(1);
        WebResult webResult = peopleService.addPeopleInfo(bob);
        Assertions.assertThat(webResult.getStatus()).isEqualTo(WebResult.SUCCESS);
        Mockito.verify(peopleMapper, VerificationModeFactory.times(1)).addPeopleInfo(Mockito.any());
    }
	//other mothed……
}

2.3 Dao 單元測試

因爲這一層大多數都是數據庫操作,需要配置數據連接,使用mybatis還需要配置,所以在寫測試時加上註解引入配置

  • @SpringBootTest 註解負責掃描配置來構建測試用的Spring上下文環境
  • @EnableAutoConfiguration自動加載配置到容器中
  • @Transactional數據庫操作的回滾功能,不會在數據庫中產生髒數據。
@RunWith(SpringRunner.class)
@DisplayName("人員接口測試")
@EnableAutoConfiguration
@SpringBootTest
@Transactional
public class PeopleMapperTest {
    @Autowired
    PeopleMapper peopleMapper;

    @Test
    @DisplayName("增加人員信息")
    public void testAddPeopleInfo() {
        People people = new People("keer", 1, 25, "湖北武漢加油!!");
        Assert.assertEquals(1, peopleMapper.addPeopleInfo(people));
    }

 	@Test
    @DisplayName("根據主鍵id查詢人員信息")
    public void testGetPeopleInfoByID() {
        People people = new People("keer", 1, 25, "湖北武漢加油!!");
        Assert.assertEquals(1, peopleMapper.addPeopleInfo(people));
        Assert.assertEquals("keer", peopleMapper.getPeopleInfoByID(1).getName());
    }
	//other method……
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章