关于单元测试



前言

测试是开发过程中必不可少的, 但是实际工作中严格按照标准, 测试用例能够覆盖大部分业务逻辑的, 估计连一半都不到.

每天的工作流程应该是这样的:

  1. git上把代码更新到本地, 跑通所有单元测试, 确保代码修改前是正确的
  2. 新增代码
  3. 对新增代码进行单元测试, 保证新增代码没有问题
  4. 提交到git

关于测试工具

单元测试主要使用Junit工具, 已经是很古老的技术了, 现在的Junit4直接通过注解就可以实现, 特别方便.

另一个测试的必备利器就是数据模拟工具Mockito. 假设我们在代码中有如下的调用关系:

类调用关系 我们要对A进行单元测试的时候需要整个调用树都构建出来, 即BCDE的示例都需要, 但是显然我们只关心A, 这个时候我们可以使用Mockito模拟BC的返回, 使用mock对象来代替BC.


Junit使用详解

Junit4使用方式特别简单, 只需要加一些注解就可以. 下面对一些常用的注解进行简单说明:

  • @Test: 标识一个普通的测试方法, @Test(timeout = 1000)表示测试方法执行超过1000毫秒后算超时, 测试将失败; @Test(expected = Exception.class)表示测试方法期望得到的异常类, 如果方法执行没有抛出指定的异常, 则测试失败
  • @Before: 初始化方法, 对每一个方法都执行一次
  • @BeforeClass: 在所有测试方法前执行一次
  • @After: 在每个测试方法后执行一次
  • @AfterClass: 在所有测试方法后执行一次
  • @Ignore("not ready yet"): 表示暂时不执行该测试方法
  • @RunWith: 在JUnit中有很多个Runner, 他们负责调用你的测试代码, 该注解用于指定一个Runner
  • @Suite.SuiteClasses: 打包测试, 需要与@RunWith(Suite.class)联合使用, 比如下面的例子:
@RunWith(Suite.class)
@SuiteClasses({ATest.class, BTest.class, CTest.class})
public class ABCSuite {
    // 类中不需要编写代码
}
  • Parameterized.Parameters: 参数化测试, 这个稍微复杂一些, 一般有这么几个条件:
    • 该类被注解为@RunWith(Parameterized.class)
    • 这个类有一个构造函数, 存储测试数据
    • 这个类有一个静态方法生成并返回测试数据, 并注明@Parameters注解
    • 这个类有一个测试, 它需要注解@Test到方法

下面是参数化测试的一个例子:

@RunWith(Parameterized.class)
public class ParameterTest {

    private String name;
    private boolean result;

    /**
     * 该构造方法的参数与下面@Parameters注解的方法中的Object数组中值的顺序对应
     */
    public ParameterTest(String name, boolean result) {
        super();
        this.name = name;
        this.result = result;
    }

    // 将对参数构建出的每个对象都执行一遍, 即本例中将会执行三遍
    @Test
    public void test() {
        Assert.assertTrue(name.contains("小") == result);
    }

    /**
     * 相当于通过不通参数构建出了三个对象
     */
    @Parameterized.Parameters
    public static Collection<?> data(){
        // Object 数组中值的顺序注意要和上面的构造方法ParameterTest的参数对应
        return Arrays.asList(new Object[][]{
                {"小明2", true},
                {"坏", false},
                {"莉莉", false},
        });
    }
}

另外单测中还常用到一些断言方法, 比较简单, 不再介绍.


Mockito使用详解

使用Mockito可以设定当调用哪个对象的哪个方法时, 返回什么数据; 还可以验证调用了某对象的某方法几次.
创建mock对象不能对final, Anonymous, primitive类进行mock

下面是一个简单的例子及相关说明

    @Test
    public void testMock() {
        // 创建mock对象,参数可以是类,也可以是接口
        List<String> list = mock(List.class);
        // 设置方法的预期返回值
        when(list.get(0)).thenReturn("helloworld");
        // 当调用 list.get(0) 时, 将返回之前设置的值
        String result = list.get(0);
        // junit 测试
        Assert.assertEquals("helloworld", result);

        // 设定方法返回某异常
        when(list.get(1)).thenThrow(new RuntimeException("test excpetion"));

        // stubbing 形式, doXXX 返回的是一个 Stubber 对象
        doReturn("helloworld").when(list).get(0);  // 效果与最上面那个when一样
        doNothing().doThrow(new RuntimeException("void exception")).when(list).clear(); // 返回 void 的方法

        /**
         * 验证方法调用, 不关心返回值, 只关心调用了几次
         */
        verify(list).get(0);  // 验证是否调用了 list.get(0)
        verify(list, times(1)).get(0);  // 默认调用一次,times(1)可以省略
        verify(list, times(3)).get(0);  // 验证调用三次
        verify(list, times(0)).get(0);  // 一次也没调用
        verify(list, never()).get(0);       // 一次也没调用, 同上
        verify(list, atLeastOnce()).get(0); // 至少一次
        verify(list, atLeast(2)).get(0);    // 至少两次
        verify(list, atMost(5)).get(0);     // 最多5次
    }

参数匹配器

  • Mockito类继承于Matchers
  • Matchers类中有许多参数匹配器用于匹配一种类型, 比如anyInt, anyString, anyMap...
  • 如果使用参数匹配器, 那么所有的参数都要使用参数匹配器, 不能即有get(0)又有get(anyInt())

如下例子:

        when(list.get(anyInt())).thenReturn("helloworld");
        when(map.get(anyString())).thenReturn("hello");
        when(list.get(0)).thenReturn("helloworld");  // 再这么用, 就会报错

Spring Boot Test

与单元测试不通, 在Spring中需要初始化完整的应用程序上下文, 因此这种测试常称为集成测试.
Spring Boot提供了一个测试相关的starter:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

我们在之前的Spring项目中使用JUnit测试时, 测试类是这样写的:

@RunWith(SpringJUnit4ClassRunner.class)  // Spring JUnit支持
@ContextConfiguration(classes = Application.class)  // 指定启动类
@WebAppConfiguration // 如果是Web项目, Junit需要模拟ServletContext, 因此需要加上这个注解
public class ExampleServiceTest {
}

而在Spring Boot 1.4已经做了优化, 变成了这样:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class ExampleServiceTest {
}
  • SpringRunnerSpringJUnit4ClassRunner的新名字, 这个名字只是让名字看起来简单些.
  • SpringBootTest将使用你的SpringApplication来创建ApplicationContext, classes属性可以不指定,会自动发现, 另外还有一个webEnvironment属性用于指定web的测试环境
  • Spring Boot 1.3中的@SpringApplicationConfiguration@WebIntegrationTest已经被废弃掉了, 被@SpringBootTest替代了

模仿和侦查

在Spring项目中做测试的时候会发现大部分情况下都需要模拟特定的bean, 使某个bean的特定方法返回你想要的数据. 在Spring Boot中变的特别简单, 比如:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {
    @MockBean
    private RemoteService remoteService;  // 这个bean是被mock出来的, 会替换掉spring中的那个bean
    @Autowired
    private Reverser reverser;  // 这个bean是spring中的
    @Test
    public void exampleTest() {
        // RemoteService 已经被注入到了 reverser bean里了
        when(remoteService.someCall()).thenReturn("mock");
        String reverse = reverser.reverseSomeCall();
    }
}

@MockBean是把原来的bean用mock的bean整个替换掉了, 而@SpyBean还会执行原来bean的方法, 但是后面可以mock想要的方法