有效的单元测试

Posted by 周瑞进 on January 8, 2018
zhruji@gmail.com

有效的单元测试

认识单元测试

什么是单元测试

测试有黑盒测试和白盒测试之分,黑盒测试顾名思义就是我们不了解盒子的内部结构,我们通过
文档或者对该功能的理解,指定了相应的输入参数,然后判断得出的结果是否正确。普通的用户、
开发人员、QA工程师都可以进行黑盒测试。
与之对应的白盒测试则需要了解到内部的实现细节,一般是由开发人员自己来进行的,是基于对
代码逻辑结构、各个关联方法了解上进行的。

白盒测试主要有2种类型

  • 静态代码分析:静态的分析代码质量,如用Findbugs插件扫描代码。
  • 动态测试:单元测试,用预先的一组数据动态的调用代码。

单元测试的必要性

测试金字塔

在金字塔越下面的地方bug是最多、最好发现、修改成本最小的。反之逐渐往上bug的修复成本是越来越大的。直观的感受就是一个开发期的bug,看一眼代码可能就知道问题了。如果是换成线上的,好一点的话通过日志可以判断出来,如果日志看了没发现什么那就有点惨了。有时候需要在测试环境模拟生产的场景进行排查等。

单元测试具有以下几个好处

  • 方便对局部代码逻辑进行验证(如:微服务架构的话rpc端的方法不好触发,不用启动整个系统)
  • 快速的反馈问题(将问题快速的扼杀在构建前)
  • 给代码重构提供了底气(复杂的模块经常不敢轻易变动,因为关联太多怕导致其他异常,有时候也经常发生在修补了一个bug引发了另一个bug的问题)

单元测试的基础知识

如何编写单元测试

可理解的代码非常重要,测试代码也是如此。建议测试代码遵循BDD(Behavior Driven Development )的风格,即使代码再复杂你也要将它抽象出 //given //when //then 三个基础部分。

通过测试用例反思你的代码

  • given定义了一大堆,就是在测试一个方法的时候需要mock一大堆对象,这时候可能是代码设计有误,内聚力不足、与外部高度耦合、职责模糊。
  • when一般只有一个,如果需要调用多个的话,可能实现代码没有给使用者提供合适的接口,需要调用一大堆接口才能完成一个动作。
  • then 主要是要去校验结果,这边的校验要尽可能的包含所有出参结果的校验。不能只看调用后没报错,或者返回的状态码是对的就好了。
      import static org.mockito.BDDMockito.*;
    
      Seller seller = mock(Seller.class);
      Shop shop = new Shop(seller);
    
      public void shouldBuyBread() throws Exception {
      //given
      given(seller.askForBread()).willReturn(new Bread());
    
      //when
      Goods goods = shop.buyBread();
    
      //then
      assertThat(goods, containBread());
      }
    

    拓展:

  • TDD(测试驱动开发)

    这是极限编程中推崇的开发模式,要求在开发前先写测试代码,基于测试的代码来编写业务代码,这样的好处一方面可以反复检验你的功能代码,一方面有助于你业务代码具有高内聚低耦合的效果。

  • BDD(行为驱动开发)

    可以看作TDD的一个补充,他主要是输出一些更贴近用户的文档,让用户更好理解。 据我简单的理解是:基于BDD的测试用例你要充分写好你的注解,given(说明好上下文),when(指定哪个事件),then(对结果进行较详细的说明)

  • DDD(领域驱动开发)

    他主要是对业务进行建模,是业务人员与开发人员沟通的桥梁,他要求开发人员在写代码前要充分的理解业务。现在微服务的架构很火热,微服务里面注重业务的划分,DDD是微服务的一个不错的实践方式。

基础设施

单元测试一般可以借助自动化构建工具,每次打包的时候进行自动化测试,有问题直接抛出,保证了打包程序的质量,单元测试通过后可以自动发布到开发、QA、仿真等环境。 常见的自动化构建工具有:Jenkins、gitlab-ci、gitlab、docker CI构建

常见术语

  • stub

很多人把它叫做桩。 测试某个代码时可能需要访问外部系统的服务,这时候我们可以启一个模拟的服务来代替。(比如系统中需要对接各个快递公司的服务)。

  • mock

与stub类似,都是用来模拟一些外部依赖。stub主要指的是外部系统,mock一般是本系统内的其他方法。

  • spy

spy是间谍的意思,监视某个对象的行为。mock是模拟了整个对象,而spy的对象还是真是的对象,它可以对真实对象中的某些行为进行改变。

@Test
    public void real_partial_mock(){
        List list = spy(new ArrayList());
        // 执行的是list的真实api
        assertEquals(0,list.size());
        A a  = mock(A.class);
        // 执行的所有方法都不是A的
        when(a.doSomething(anyInt())).then(。。。);
    }

单元测试实战

工具介绍

mocktio

mocktio

单元测试中一个很好的测试工具,简单列举下他的功能点,详情点击:传送门

  • 可以mock对象
      @Mock
    
  • 可以spy某个真实对象的个别方法
      @spy
    
  • 可以方便的往某个对象中注入mock对象
      @InjectMocks
    
  • 可以模拟调用方法时的参数匹配
      when(mockedList.get(anyInt())).thenReturn("element");   
    
  • 可以判断某个方法调用次数
      verify(mockedList, times(1)).add("once");  
    
  • 可以模拟方法调用抛出异常
      when(mockedList.get(1)).thenThrow(new RuntimeException());
    

powermock/jmock

可对静态方法、私有函数、Final函数进行模拟

testNG

可以比较方便的进行集成测试

AssertJ

比junit强大很多的断言工具,能够很强大的进行流式断言。

传送门

Assertions.assertThat(propsSendDetailPoList)
                .isNotEmpty()
                .hasSize(3)
                .extracting(PropsSendDetailPo::getFcalqty1)
                .contains(0L,0L,2L);

场景分析

静态对象mock

参看

@RunWith(PowerMockRunner.class)  //1
    @PrepareForTest({WebChatUtil.class}) //2
    public class WebChatUtilTestCase extends AbstractJUnit {
    
    @Before
    public void init(){
        PowerMockito.mockStatic(WebChatUtil.class);// 3
    }
    
    @Test
    public void testWebchatEnable(){
        try {
            Calendar c = Calendar.getInstance();
            c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DATE), 16, 35);
            PowerMockito.spy(WebChatUtil.class); // 创建spy,如果不创建的话,后面调用WebChatUtil就都是Mock类,这里创建了spy后,只有设置了mock的方法才会调用mock行为
            PowerMockito.doReturn(c).when(WebChatUtil.class, "getCurrentTime"); //Mock私有方法
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

跨类mock

你要测试的方法里面引用了别的对象的方法,对于这个待测方法里面依赖别的对象你获取不到,如何对他进行mock。

  • 最原始的方法-自己用反射实现 此时要测试 A 对象,A里面引用了 B 对象,用来进行持久化操作的,这时候可以通过反射,将 A 对象的 B 属性替换成自己定义的mock对象。 

      B bmapper  = mock(B.class);
      // 一个待测试的对象
      A a = new A();
    
      // bPoMapper 是 A 里面的一个属性(处理持久化用的)
      Class<A> aclass = A.class;
      Field bMapperField = aclass.getDeclaredField("bmapper");
      bMapperField.setAccessible(true);
      bMapperField.set(a, bmapper);
    
  • 借助mocktio工具

      //@InjectMocks 会将所有 @mock对象注入到自己的属性中
      @InjectMocks
      A a;
      @Mock
      B b;
    

常见问题

  • 很多人会在每个类中继承springjunit框架
    • 导致构建时候非常慢,每继承一个spring容器就会反复启动一次,严重占用了构建机器的性能也影响效率
    • 我们只是为了测试代码逻辑没什么必要启动spring容器,依赖的bean可以手动new出来,可以自己mock相应对象。
    • 有的人启动spring容器是为了能够连接数据库
      • 可以写一个连接数据库的基类
      • 可以借助某些工具dbunit等
  • 测试数据依赖某个数据库
    • 测试对于的数据全部要自己初始化,测试用例跑完所有的数据必须回滚。
    • 可以用内存数据库
    • 可以在构建的时候专门指定一个测试数据库
  • 不要只是为了覆盖率而测试
    • 要对测试的场景足够清楚,对返回对断言足够细致,不然很难发现问题,就会导致一个现象 - 你的测试用例只是为了证明你对代码是对的,而不是在校验你对代码。

作者:周瑞进 以上如果有误欢迎指正、交流