现代化的spring-test使用方式
以下例子可以在
javaconfig-spring-test
中找到。
在classic-spring-test
中演示的单元测试,还是用配置文件的方式,但是从Spring4之后,官方就鼓励使用Java的方式对spring进行配置,而不是用以前那样的xml配置形式了,因此我们基于注解可以来简化单元测试的编写,我们称之为现代化的spring-test
方式。
修改单元测试
测试不用继承AbstractJUnit4SpringContextTests
,通过注解即可,然后对于bean的配置,可以通过Java配置风格完成
注解
使用RunWith
和ContextConfiguration
配置即可将一个类声明为支持Spring容器的测试用例。
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MemberJavaConfigTest.MemberServiceConfig.class)
public class MemberJavaConfigTest {
}
注解 | 说明 |
---|---|
RunWith |
该注解是junit 提供的,表示用那种方式来执行这个测试,这里是SpringRunner ,由spring-test 提供 |
ContextConfiguration |
对测试的Spring容器的配置,比如:配置的位置等 |
配置与示例
通过注解可以声明按照何种方式去执行测试,以及测试的Spring容器如何组装,但是还或缺在Spring容器中如何配置Bean,以前这是通过xml来进行配置的。
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MemberJavaConfigTest.MemberServiceConfig.class)
public class MemberJavaConfigTest {
@Autowired
private MemberService memberService;
@Test
public void insert_member() {
System.out.println(memberService.insertMember("windowsxp", "abc123"));
Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
}
@Configuration
static class MemberServiceConfig {
@Bean
public MemberService memberService(UserDAO userDAO) {
MemberServiceImpl memberService = new MemberServiceImpl();
memberService.setUserDAO(userDAO);
return memberService;
}
@Bean
public UserDAO userDAO() {
UserDAO mock = Mockito.mock(UserDAO.class);
Mockito.when(mock.insertMember(Mockito.any())).thenReturn(System.currentTimeMillis());
return mock;
}
}
}
可以看到只需要有一个类,被注解了Configuration
,该类就是一个配置类型,而这种Java Config Style已经是Spring官方推荐的方式了。
Bean
注解类似xml中的bean
标签,这里配置了两个Bean一个MemberService
的实现,另外一个是mock的UserDAO
。其中对MemberService
的配置需要依赖UserDAO
。
剩下的测试过程就和之前classic-spring-test
完全一致了,可以看到新的方式没有了恼人的xml配置,变得更加直接和高效。
SpringBoot环境下的测试方法
以下例子可以在
spring-boot-test
中找到。
Spring
框架实际上是依靠SpringBoot完成了续命,由它焕发了第二春,打开了一个全新的战场。在今天微服务大放异彩的环境下,针对SpringBoot的测试也会有所不同。
SpringBoot
实际是用来启动你的应用,所以它会有配置以及一系列约定大于配置的环境准备,所以需要依赖spring-boot-test
支持来完成单元测试。
修改单元测试
如果需要在单元测试启动时启动SpringBoot,需要做一下相关的配置,增加一些注解。
依赖
增加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
注解
和JavaConfig
的方式非常类似,通过注解可以声明该测试是SpringBootTest
,并且可以指定运行的SpringBoot
容器的配置。
@SpringBootTest(classes = SpringBootMemberTest.Config.class)
@TestPropertySource(locations = "classpath:test-application.properties")
@RunWith(SpringRunner.class)
public class SpringBootMemberTest {
注解 | 说明 |
---|---|
SpringBootTest |
描述了该SpringBoot单元测试是根据哪个配合来启动容器 |
TestPropertySource |
应用的配置使用哪个 |
配置
通过注解可以声明按照何种方式去执行测试,以及测试的Spring容器如何组装,还或缺在Spring容器中如何配置Bean。
@SpringBootTest(classes = SpringBootMemberTest.Config.class)
@TestPropertySource(locations = "classpath:test-application.properties")
@RunWith(SpringRunner.class)
public class SpringBootMemberTest {
@Autowired
private Environment env;
@MockBean
private UserDAO userDAO;
@Autowired
private MemberService memberService;
@Test
public void environment() {
Assert.assertEquals("Alibaba", env.getProperty("brand-owner.name"));
}
@Before
public void init() {
Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(System.currentTimeMillis());
}
@Test
public void insert_member() {
System.out.println(memberService.insertMember("windowsxp", "abc123"));
Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
}
@Configuration
static class Config {
@Bean
public MemberService memberService(UserDAO userDAO) {
MemberServiceImpl memberService = new MemberServiceImpl();
memberService.setUserDAO(userDAO);
return memberService;
}
}
}
可以看到新增了一个注解MockBean
,这个用来帮助我们创建一个Mock的UserDAO
,而不用通过编码来进行创建,回忆之前在classic
以及javaconfig
中的Mock方式,都需要调用Mockito.mock(Class type)
方法来创建一个Mock对象,而在SpringBootTest
中就不需要了,直接在成员变量上增加MockBean
的注解就可以了。
同时可以看到在单元测试中增加了一个注入属性,Environment
,它代表Spring运行的环境,可以从中获取配置,以下是test-application.application
中的内容:
brand-owner.name=Alibaba
brand-owner.company=Alibaba-inc.
在environment
测试方法中,可以访问测试的配置内容,从这里可以看到SpringBootTest
在spring-test
基础上,除了启动一个Spring容器,还准备好了一个SpringBoot
运行时环境。
但是从侧面上讲,使用SpringBootTest
就依赖了运行时环境,这不是一个好的选择,所以在大多数情况下,对于代码的单元测试spring-test
就可以完全应对。
单元测试覆盖率
就像刻意的刷分数一样,单元测试覆盖率也是一个我们追求的目标,当单元测试行覆盖率超过70%的时候,整个项目的质量会很不错。持续稳定的单元测试覆盖率,会保障一个应用一直处于较稳定的状态,后续投入维护的资源会降低。
在不少IDE中,如:IDEA,都内置了统计单元测试的工具,只需要按照package
运行测试即可,在这里我们不依赖具体的IDE,而是用maven插件来做。
jacoco
该插件对java8的语法支持较好,在pom
文件中增加配置。
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
当运行mvn test
时会生产单元测试覆盖率报告。
位置一般在项目的
target/jacoco-ut
目录下。
覆盖率
打开目录下的index.html
可以看到各个类的覆盖率情况。
<img src="https://raw.githubusercontent.com/weipeng2k/mockito-sample/master/resources/chapter6-1.png" />
缺失路径
点击到对应的package
中的类,可以查看缺失的测试路径,这样就可以指导哪些分支没有纳入单测。
<img src="https://raw.githubusercontent.com/weipeng2k/mockito-sample/master/resources/chapter6-2.png" />
测试驱动开发简介
测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码,然后只编写使测试通过的功能代码,从而以测试来驱动整个开发过程的进行。这有助于编写简洁可用和高质量的代码,有很高的灵活性和健壮性,能快速响应变化,并加速开发过程。
测试开发驱动模式
测试驱动开发的基本过程如下:
- 快速新增一个测试
- 运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过
- 做一些小小的改动,尽快地让测试程序可运行,为此可以在程序中使用一些不合情理的方法
- 运行所有的测试,并且全部通过
- 重构代码,以消除重复设计,优化设计结构
简单来说,就是不可运行/可运行/重构——这正是测试驱动开发的口号。
可取之处
测试驱动开发能够让代码上生产环境之前,能够以使用者的角度审视编写的代码:
- 如果代码难测,那就是对问题的分析还没有到位
- 如果大量的Mock,那就是依赖过于复杂
除了能够通过反向刺激让我们看到代码的不足,它还能以使用者的角度去看:
- 这个方法命名是否够妥帖
- 别人用这个函数会误用吗
- 这个类是不是承担了过多的职责