Java测试驱动看这篇就够了

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对象主要有:

  1. 其他类:因为我只关注某个类的方法。
  2. 远程调用: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使用经验

  1. 注解:使用注解,主要需要@RunWith(MockitoJUnitRunner.class)或者2MockitoAnnotations.initMocks(this);
  2. @MockBean是spring-boot支持的,需要在SpringRunner下运行,别搞混了。
  3. mock void方法doNothing().when(spy).add(anyInt(),anyString())
  4. mock 异常:doThrow(new XxxException()).when(spy).add(anyInt(),anyString())
  5. mock 静态方法:需要使用poermock。
  6. spy和mock的区别:spy是间谍类,如果没有打桩,将会调用真正的方法。mock如果没有打桩,不会调用方法,返回默认零值。
  7. doReturn/when和when/thenReturn是有区别的,一般使用前者。在使用spy创建间谍类时,when/thenReturn会执行真正的方法,再给出mock的返回值,如果执行报错,就会中断。
  8. 打桩doReturn/when,参数要么都是固定值,要么都用匹配器(anyInt().any(Xxx.clas)eq("string"))。

总结

本文简单介绍TDD测试驱动开发,他是一种设计思想。单元测试上,介绍了maven+junit+jacoco配置。 具体到业务上,不会只是util类测试那么简单,很大可能依赖外部方法,那么,mockito是个不错的选择。

CONTENTS