1.单元测试

单元测试是编写单元测试类,针对类级别的测试。比如使用Junit框架,针对一个类,写一个测试类,测试目标类的大部分主要方法。
需要注意单元测试的级别是方法。项目当中,类之间的依赖调用是很常见的事,如果你要测试一个类,而这个目标类又调用了另一个类,那么在测试时就没有遵守“在一个类范围内进行测试”,自然算不得单元测试。
如图1:A、B、C、D类存在依赖关系,如果对A类进行单元测试,就需要采取Mock方式对依赖B类C类进行模拟。

springboot自动生成单元测试报告 springboot 单测_单元测试

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