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……
}