单元测试总结

Posted by zhidaliao on September 21, 2018

理论知识

单元测试的好处

  • 对自己代码负责,保证代码的正确性。
  • 有利于团队整体协作,整体代码质量,避免他人修改代码不能及时发现问题。
  • 良好的模式设计,好的设计能够简单快速的写出单元测试

单元测试的几个特性

好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。

  • A:Automatic(自动化)
  • I:Independent(独立性)
  • R:Repeatable(可重复)

rpc接口是否需要测试

RPC接口一般大家会忽略他的测试,觉得只要DAO层和业务层通过测试,验证了核心的业务流程就可以了,

  • RPC的接口一般包含了数据校验加工和逻辑分发处理,如果因为上层的处理失败,下层的核心业务是正常的一样不能提供服务
  • 保证返回给前端的数据结构符合要求

应该为私有方法添加测试么?

新创建项目: 在成功的用了TDD或者测试驱动重构(Test-Driven Refactoring)以后,你的代码中就不会出现针对私有方法的测试。 如果你用TDD编写全新的代码,在没有测试之前是没有功能的。私有方法是到了重构那一步的最后才会出现。把代码转移到私有方法中的这个过程,已经被先前写过的测试覆盖到了。所以,如果你成功了用了TDD,代码中就不会出现针对私有方法的测试。

遗留项目: 如果你在改善遗留代码,你就该使用测试驱动重构。这样的话,可能会临时针对私有方法写一些测试。但是,随着测试覆盖率的增加,那些public方法的测试会覆盖到所有的路径,也包括了私有方法的调用。所以,你也不再需要测试私有方法。

临时私有方法测试采用映射的方式编写;

Example example = new Example();
Method method = example.getClass().getDeclaredMethod("add", int.class);
method.setAccessible(true);
method.invoke(example,int.class)

另外可以参考这篇文章对私有方法的见解:Testing Private Methods with JUnit and SuiteRunner , 要点如下:

  • Don’t test private methods.
  • Give the methods package access.
  • Use a nested test class.
  • Use reflection.

单元测试回滚机制

默认机制:不回滚,需要自动回滚可以使用注释和集成类的方式设置:

注释
@Transactional
@TransactionConfiguration(defaultRollback=true)
类继承

AbstractTransactionalJUnit4SpringContextTests默认操作修改、新增、删除操作是事务自动回滚,避免出现脏数据,如果不需要回滚,可以添加@Rollback(false)注解即可。

extends AbstractTransactionalJUnit4SpringContextTests  

单元覆盖率统计

  • 推荐开源工具:jacoco
  • 代码覆盖率统计是用来发现没有被测试覆盖的代码
  1. 对Java字节码进行插桩,On-The-Fly和Offine两种方式。
  2. 执行测试用例,收集程序执行轨迹信息,将其dump到内存。
  3. 数据处理器结合程序执行轨迹信息和代码结构信息分析生成代码覆盖率报告。
  4. 将代码覆盖率报告图形化展示出来,如html、xml等文件格式。

参考文章:浅谈代码覆盖率

外部依赖

对于数据库的依赖:
  • 单元测试肯定是不能与实际数据库连接的,无论是本地开发数据库或者beta。所有对数据库操作的单元测试都需要是in memory。所以,你需要利用Dependency Injection的技术来提供一个in-memory database。
  • 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。 因为数据库里的数据都是有关联的,构造测试数据不是构造一两条记录那么简单
  • 不能在开发中使用测试库的数据作为测试结果,hardcode测试库的数据会造成单元测试失效,无法维护
对于外部系统的依赖
  • 使用Mock技术代替(推荐)。
  • 打桩处理,类似 swagger 这样的外部服务或者自己用NodeJs搭建一个简单的路由转发应用,定义好接口地址和返回的结果数据结构

Mock 工具

  • 外部的服务会导致在 单测环境下没有办法工作, 比如数据库和外部的系统
  • 测试应该关注的是类的代码实现, 如果外部的类会直接的影响到这些被测试的类,那不是我们想要的结果

Mockito 和 PowerMock 都用于集成测试中,使用mock对象替代耦合的类,两者的不同在于 Mockito 适用于所有标准的场景, PowerMock 用于特殊的情况:比如Mock私有 & 静态的方法。

几种Mock比较

直接参考以下文章:

Mock特性演示-Mockito

注意事项被Mock的类不要使用接口类,你测试的是类的实现逻辑是否正确,不是接口是否优雅;而且会提示你找不到方法抛出异常。

Mock & Spy

Mock一个对象,我们能改变对象的返回结果,实际的逻辑代码不会被执行

Spy一个对象,能改变对象的返回结果,但是对象调用的方法内的逻辑会被真实的执行。

@Test
public void testSpy(){
    Hello mock = Mockito.spy(Hello.class);
    Mockito.when(mock.getName(anyString())).thenReturn("1");
    mock.getName("");
}


class Hello{

    public String getName(String name){
        System.out.println("inner code ");
        return name;
    }

}

thenReturn & doReturn 执行顺序

List replaceList = Mockito.spy(new LinkedList());

//报错,在执行 get(0)的时候就会抛出异常
try {
    Mockito.when(replaceList.get(0)).thenReturn("foo");
}catch (IndexOutOfBoundsException index){
    Assert.assertTrue(true);
}

//正常执行,直接返回 "foo" 值 ; 推荐方式
doReturn("foo").when(replaceList).get(0);

多次调用返回不同结果

@Test
public void testMulti(){
    Hello mock = Mockito.mock(Hello.class);
    Mockito.when(mock.getName(anyString())).thenReturn("1").thenReturn("2").thenReturn("3");
    assertEquals(mock.getName(""),"1");
    assertEquals(mock.getName(""),"2");
    assertEquals(mock.getName(""),"3");
}

自定义返回结果

@Test
public void testAnswer(){

    List list = Mockito.mock(List.class);

    Mockito.when(list.get(0)).thenAnswer(new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            System.out.println(JSON.toJSONString(args));
            Object mock = invocation.getMock();
            return "hello world" ;
        }
    });

    list.get(0);

}

verify验证是否被执行

@Test
public void testVerify(){

    Hello mock = Mockito.mock(Hello.class);
    mock.getName("");
    verify(mock).getName("");

    // 测试无法通过
    //verify(mock).getName("1");

    // 测试执行次数
    verify(mock,Mockito.times(1)).getName("");

}

verify 验证方法从未被执行

@Test
public void testVerifyNever(){

    List<String> mockedList = Mockito.mock(ArrayList.class);
    mockedList.size();
    verify(mockedList, Mockito.never()).clear();

}

verify验证执行顺序

@Test
public void testVerifyOrder(){

    List<String> mockedList = Mockito.mock(ArrayList.class);
    mockedList.size();
    mockedList.add("a parameter");

    InOrder inOrder = Mockito.inOrder(mockedList);
    inOrder.verify(mockedList).size();
    inOrder.verify(mockedList).add("a parameter");
}

使用注释的方式

1:@Mock: 创建一个Mock.

2:@InjectMocks: 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

3:开启注释模式,选择其中一种即可。

// 手动开启
MockitoAnnotations.init(Object)
// 类注释
@RunWith(MockitoJUnitRunner.class)
public class MockitoServiceTest {

    @Mock
    SingBehavior singBehavior;


    @InjectMocks
    Star star;

    @Before
    public void init(){
        MockitoAnnotations.initMocks(this);
        Mockito.when(singBehavior.sing()).thenReturn("anySong");
    }


    @Test
    public void test(){
        String performance = star.performance();
        assertEquals(performance,"anySong");
    }
}

class SingBehavior {

    public String sing() {
        return "wind";
    }

}

class Star {

    private SingBehavior singBehavior;

    public String performance() {
        return singBehavior.sing();
    }

}

Mock特性演示-PowerMock

PowerMock特性:

  • PowerMockito 支持大部分Mockito 不支持的场景,比如:静态、私有、构造方法
  • PowerMockito 使用的是操纵字节的方式,所以他使用的是自己的 JUnit runner. 需要使用@PrepareForTest注释
  • PowerMock 大部分的写法和 Mockito 类似,因为 PowerMock 是扩展 Mockito 的 API 形成的

使用 PowerMock 需要添加以下注释:

@RunWith(PowerMockRunner.class)
@PrepareForTest({Demo.class})

Mock 私有方法

Mock技术一般用于对外部类和组件进行替换,如果单元测试中对私有方法的Mock是必要的,说明类的设计是有问题的。


@Test
public void testPrivate() throws Exception {

    Demo demo = PowerMockito.spy(new Demo());
    when(demo, "privateMethod").thenReturn(2);

    int luckyNumber = demo.testPrivate();
    assertEquals(2, luckyNumber);
}

class Demo {

    public Demo() {
    }

    public int testPrivate() {
        System.out.println("spy execute inner code");
        return privateMethod();
    }

    private int privateMethod() {
        return 10;
    }
}

方法不执行

@Test
public void testSupress() throws Exception {

    Demo demo = new Demo();
    PowerMockito.suppress(PowerMockito.method(Demo.class, "supressMethod"));
    //PowerMockito.doNothing().when(Demo.class,"supressMethod",null);

    int testSupress = demo.testSupress();
    assertEquals(0, testSupress);
}
class Demo {

    public Demo() {}
    
    public int testSupress() {
        return supressMethod();
    }

    private int supressMethod() {
        return 10;
    }

}

Mock 日期

写法未经过验证

Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MONTH,6);
PowerMockito.mockStatic(Calendar.class);
PowerMockito.when(Calendar.getInstance()).thenReturn(calendar);

//PowerMockito.whenNew(Calendar.class).withNoArguments().thenReturn(null);
        
//final Calendar date = Mockito.mock(Calendar.class);
//Mockito.when(date.get(Calendar.MONTH)).thenReturn(5);

Mock 静态方法

@Test
public void testStatic() throws Exception {

    PowerMockito.mockStatic(Demo.class);
    PowerMockito.when(Demo.testStatic()).thenReturn(2);

    int value = Demo.testStatic();
    assertEquals(value, 2);

}
class Demo {

    public Demo() {}

    public static int testStatic() {
        return staticMethod();
    }

    private static int staticMethod(){
        return 10;
    }
}

依赖

mockito-all发行已经停止,Mockito 2以上版本使用mockito-core

<!--mockito-->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>2.21.0</version>
</dependency>

<!--power-mockito-->
<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-core</artifactId>
  <version>2.0.0</version>
</dependency>

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-module-junit4</artifactId>
  <version>2.0.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-module-junit4-rule</artifactId>
  <version>2.0.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-api-mockito2</artifactId>
  <version>2.0.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-classloading-xstream</artifactId>
  <version>2.0.0</version>
  <scope>test</scope>
</dependency>

常见异常

1:java.lang.NoSuchMethodError org.mockito.internal.handler.MockHandlerFactory.createMockHandler(Lorg/mockito/mock/MockCreationSettings;)Lorg/mockito/internal/InternalMockHandler;

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.0-beta.5</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.0-beta.5</version>
    <scope>test</scope>
</dependency>

2:Extension API internal errororg.powermock.api.extension.reporter.MockingFrameworkReporterFactoryImpl

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito-common</artifactId>     
    <version>1.6.5</version>
</dependency>

3:javassist 包冲突问题

把不需要的版本排除掉即可,推荐版本

<dependency>
  <groupId>org.javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.21.0-GA</version>
</dependency>

参考网站

所有测试框架集合-github

单元测试框架PowerMock教程

Mockito And Private Methods

使用Mockito进行单元测试【2】—— stub 和 高级特性

Android单元测试(三):PowerMock框架的使用

TestNG使用介绍

Mockito and Power Mockito – Cheatsheet

JUnit Testing using Mockito and PowerMock

Mocking of Private Methods Using PowerMock

Verify用法

单元测试覆盖率-JaCoCo

Mocks Aren’t Stubs