如果餓了就吃,困了就睡,渴了就喝,人生就太無趣了
源碼地址:https://github.com/keer123456789/springbootstudy/tree/master/mybatisdemo
1.單元測試
單元測試是編寫單元測試類,針對類級別的測試。比如使用Junit框架,針對一個類,寫一個測試類,測試目標類的大部分主要方法。
需要注意單元測試的級別是方法。項目當中,類之間的依賴調用是很常見的事,如果你要測試一個類,而這個目標類又調用了另一個類,那麼在測試時就沒有遵守“在一個類範圍內進行測試”,自然算不得單元測試。
如圖1:A、B、C、D類存在依賴關係,如果對A類進行單元測試,就需要採取Mock方式對依賴B類C類進行模擬。
2.項目介紹
此次使用的項目使用springboot+mybatis對數據庫進行增刪改查操作的功能,分別對項目的Controller
,Service
, 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 邏輯
- 對
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;
}
}
- 編寫測試代碼
對其中兩個接口進行了測試,一個使用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……
}
- 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……
}