tdd
测试驱动开发,从字面谁上很好理解,开发流程应该是这样的。 关键在于测试编写先于实现,如果是写完实现再补测试,虽然总比没有强,但它不是TDD。测试驱动开发是一种设计方法,并不是测试方法,要求在编写代码前考虑代码需要实现的功能。它不是灵丹妙药,不能解决所有问题,只是为我们找到解决问题指明方向。明白这一点,就先抛开单元测试、集成测试这些概念,那是测试方法,不是设计方法。
可以说,单元测试只是推动TDD的技术。反过来,全面、自动化、随时运行的单元测试只是TDD的一个结果。
TDD要求快速,不断改代码不断运行单元测试,测试和实现的切换以秒计,如果运行一个测试要好几分钟,换任何人都会崩溃,那TDD也只是流于表面的好想法而已。
tdd示例
1.新建一个类
public class StringUtils {
public boolean isEmpty(String srt) {
return false;
}
}
2.创建测试
我的idea快捷键:
command + shift + T
3.编写测试并运行:创建测试类后,可以按自己的习惯分屏。先写测试,运行失败(红灯)。
我的快捷键: 水平分屏:
command + ctrl + S
重复运行测试:command + R
4.实现代码,运行测试通过(绿灯) 5.重构,运行测试通过(绿灯)
6.idea自带覆盖率插件,可以随时查看覆盖率。
单测基础工具
TDD时设计方法,单元测试是实现TDD的一种方式,下面我们再来介绍各种单元测试框架和工具的使用。
初始化一个java项目,先把单测基础需要的框架、工具给配置好。比如junit和jacoco。
在上文tdd示例中,我们用了junit,用什么框架不重要,不过要求IDE(idea,eclipse)支持,构建工具(maven、gradle)支持。主流框架是junit和testng,testng功能更强大,不过一般junit就够了。如果用到起一些比较偏的库,一般都支持junit,但不一定支持testng。
- junit:https://junit.org/junit5/
- testng:https://testng.org/doc/index.html 测试覆盖率:代码覆盖率工具能够指出测试执行期间触及了哪些代码行,但并不能保证你遵循了良好的测试实践,因为这些指标中不包含测试质量。常用覆盖率工具有:
- JaCoCo:http://www.eclemma.org/jacoco/
- Cobetura:http://cobertura.sourceforge.net/
- Emma:http://emma.sourceforge.net/
一般选择JaCoCo,因为它支持java8。
maven+junit+jacoco
添加junit依赖。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
# 运行测试
mvn test
# 指定测试类
mvn test -Dtest=com.thoreauz.bootlearn.utils.StringUtilsTest
还可以指定包,指定方法测试,详解single-test
为什么mvn test能执行测试,原因是maven默认自带了插件maven-surefire-plugin。它不是一个单元测试框架,它只是在构建执行到特定生命周期阶段的时候,通过插件来执行JUnit或者TestNG的测试用例。可以在trage/surefire-reports
目录下看到执行报告。
我们希望不止在idea看到覆盖率,希望maven执行测试后生成覆盖率报告,可以通过jacoco插件实现。
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<!--把jacoco生成报告阶段绑定到mvn test阶段,这样mvn test执行完就能生产报告-->
<id>jacoco-site</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
注意,如果在maven-surefire-plugin插件中配置了argLine参数,需要如下配置:
<argLine>-Xmx1024m -XX:MaxPermSize=256m</argLine>
# 修改为
<argLine>${argLine} -Xmx1024m -XX:MaxPermSize=256m</argLine>
因为jacoco是通过javaagent实现,如果写死argLine
,jacoco插件带入的-javaagent
运行参数无法传递到测试运参数中,从而出现无法生成报告的情况。
默认报告地址:target/site/jacoco
断言
除了junit和testgn自带断言,还有以下常用库:
- Hamcrest:http://hamcrest.org/JavaHamcrest/tutorial
- JSONassert:https://github.com/skyscreamer/JSONassert
- AssertJ:http://joel-costigliola.github.io/assertj/
我更喜欢AssertJ,流式断言,语义清晰。示例如下:
@Test
public void assartJTest() {
assertThat("Frodo").isEqualTo("Frodo").isEqualToIgnoringCase("frodo");
assertThat(42).isGreaterThan(38).isGreaterThanOrEqualTo(38);
assertThat(Dates.parse("2014-02-01"))
.isEqualTo("2014-02-01")
.isNotEqualTo("2014-01-01")
.isAfter("2014-01-01").isBefore(Dates.parse("2014-03-01"));
assertThat(newArrayList(1, 2, 3))
.contains(1, atIndex(0))
.contains(2, atIndex(1))
.contains(3).isSorted();
assertThat(RequestMapping.class).isAnnotation();
assertThat("string").isInstanceOf(String.class);
}
mock
前面提到TDD要求快速,只关注某项功能的实现,所以我们需要把单测的依赖mock掉,给一个期望的返回值,mock对象主要有:
- 其他类:因为我只关注某个类的方法。
- 远程调用:prc、数据库、消息中间件等。
主要mock框架有:
- mockito:https://site.mockito.org/
- easymock:http://easymock.org/
- powermock:http://powermock.github.io/
前两者使用差别不大,我更喜欢mockito,因为它支持间谍类,spring boot的@mockbean也是基于mockito。而powermock主要用来完成一些前两个框架无法mock的方法,尽量别使用powermock,必须使用它一般表示代码设计比较糟糕。通常用来处理历史代码。
mockito使用
测试UserService:UserMongo需要访问mongodb,mock其返回值。 注解写法:
如果使用的是junit,可以使用@RunWith(MockitoJUnitRunner.class)注解在类上,不需要MockitoAnnotations.initMocks(this);
spring-boot:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
@mockBean对testng支持好像有点问题。
非spring-boot,把类注解换成
@ContextConfiguration(locations={"classpath:spring-test.xml"})
<bean id="userMongo" class="org.springframework.aop.framework.ProxyFactoryBean" >
<property>
<bean class = "org.mockito.Mockito" factory-method="mock">
<constructor-arg value="com.thoreauz.bootlearn.service.UserMongo"/>
</bean>
</property>
</bean>
mock finel方法
public class FinalClass {
final String finalMethod() { return "something"; }
}
public class FinalClassTest {
@Test
public void finalMethod() {
FinalClass concrete = new FinalClass();
FinalClass mock = mock(FinalClass.class);
given(mock.finalMethod()).willReturn("not anymore");
assertThat(mock.finalMethod()).isNotEqualTo(concrete.finalMethod());
}
}
运行会报错,因为mockito基于动态代理(cglib),它不能代理finel方法。现在换成了bytecode。
新版本加入了对finel method的支持。
创建文件:src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
mock-maker-inline
其他mockito使用经验
- 注解:使用注解,主要需要
@RunWith(MockitoJUnitRunner.class)
或者2MockitoAnnotations.initMocks(this)
; - @MockBean是spring-boot支持的,需要在SpringRunner下运行,别搞混了。
- mock void方法
doNothing().when(spy).add(anyInt(),anyString())
。 - mock 异常:
doThrow(new XxxException()).when(spy).add(anyInt(),anyString())
。 - mock 静态方法:需要使用poermock。
- spy和mock的区别:spy是间谍类,如果没有打桩,将会调用真正的方法。mock如果没有打桩,不会调用方法,返回默认零值。
- doReturn/when和when/thenReturn是有区别的,一般使用前者。在使用spy创建间谍类时,when/thenReturn会执行真正的方法,再给出mock的返回值,如果执行报错,就会中断。
- 打桩doReturn/when,参数要么都是固定值,要么都用匹配器(
anyInt()
.any(Xxx.clas)
,eq("string")
)。
总结
本文简单介绍TDD测试驱动开发,他是一种设计思想。单元测试上,介绍了maven+junit+jacoco配置。 具体到业务上,不会只是util类测试那么简单,很大可能依赖外部方法,那么,mockito是个不错的选择。