我对单元测试的理解

保证测试的代码符合编写时的预期

目前为止,我还没有系统的学习过测试领域的知识,但是测试对于研发岗来说是重要的。测试在我的日常工作中主要是起到程序逻辑校验的作用,作为研发来说,自己做的单元测试的目的主要在于程序逻辑,而不是业务逻辑。也就是说,单元测试能保证通过单元测试的代码一定是符合我的预期的,而我的预期本身是不是有问题没法通过单元测试校验,需要集成测试以及专业的测试团队进行测试。

单元测试帮助我快速复现bug

在实际生产过程中,我的单元测试足够多,覆盖面足够广,那么出了问题的时候就可以快速定位到出错代码的单元测试。这样修改代码,就可以针对单元测试来进行修改。当然,这里也会出现,bug出现了却没有制定的单元测试,这时就可以添加单元测试,在修复bug的同时丰富单元测试用例。我称之为基于单元测试的敏捷开发。

单元测试帮助代码重构

这有两方面,第一方面是有了单元测试,在代码重构时就能可以保证代码的输入和输出不会出问题,那就可以放心大胆的一步一步重构。第二方面,单元测试也能保证代码质量,因为越小的代码,耦合越低的代码越容易进行单元测试。有的一些代码在写单元测试的时候会发现根本无从下手,这就是很明显的代码坏味道。

单元测试帮助阅读和理解旧代码

我发现这一点很少有人提及,因为单元测试一般都是测试一些比较关键的代码,而且输入和输出都会明确列举出来。如果源代码里有单元测试,那么读单元测试就能很清晰的了解到这一块代码的作用是什么。而如果没有单元测试,又有一块代码的逻辑不是很清晰,那么也可以通过补充单元测试的方式来搞懂代码。说白了,单元测试就是可以将一块代码简化成输入->输出这种形式,更易于人脑解析。

研发的单元测试比重

很多人不想写单元测试,是觉得单元测试时间太长。其实单元测试的好处大家都知道,如果给予更多的开发周期,相信更多人会花时间在单元测试上。其实这里关键的问题不止单元测试值不值得做,还有单元测试耗费的时间到底长不长。单元测试一定值得做,上面提到的好处在整体上一定会减少开发时间。而单元测试耗费的时间到底长不长呢?这就要提到一个误区了,有不少人是把单元测试也当作代码来写,这并没有太多问题,但是单元测试是一种工具,那么使用这种工具就一定是越简单越快越好,直白点就是有轮子就用轮子,代码越简单越好。比如写java的单元测试,如果不引用单元测试框架,但确实折磨,引入了框架再加上一些mock工具就很更加简单,而如果再使用一些辅助语言比如groovy或者scala,单元测试就会变得十分简单,就是在程序编写时顺手的一个工作。

Spock单元测试推荐

我再尝试了一些单元测试框架和mock工具之后,目前在写java程序时稳定使用的是spock测试框架,这时一个基于groovy的单元测试框架,groovy和java代码可以无缝接入,所以我的代码一般都是主程序用java写,测试程序用groovy写。

简单的spock测试程序大概长这样:

class PropertyCopierTest extends Specification {
    def "CopyBeanProperties"() {
        given:
        def sourceBean = new TestProperty("beanOne", 100)
        def destinationBean = new TestProperty()
        when:
        PropertyCopier.copyBeanProperties(TestProperty.class, sourceBean, destinationBean)
        then:
        destinationBean.with {
            name == "beanOne"
            id == 100
        }
    }
}

spock的教程网上有很多,我就不累述了:

Spock单元测试框架介绍以及在美团优选的实践

用Groovy更迅速地对Java代码进行单元测试

Introduction to Testing with Spock and Groovy

单元测试和Mock工具中的一些通用概念

mock

mock就是直接通过类创建一个对象,这个对象的返回值是模拟出来的也就是程序调用到指定方法就返回你事先定义好的值。

比如用Mockito框架mock:

//模拟PersonDao对象
 mockDao = mock(PersonDao.class);
 when(mockDao.getPerson(1)).thenReturn(new Person(1, "Person1"));
 when(mockDao.update(isA(Person.class))).thenReturn(true);

打桩(Stub)

这个是要创建一个实际存在的对象,但是对这个对象中的指定方法调用时返回的是自定义的对象,而其他的这个对象该返回啥返回啥。而mock如果你不定义一个函数的返回值,那就是没有返回值也就是null。当你想关注一个复杂对象中的部分状态时就可以使用打桩,比较全mock出来太麻烦有的时候甚至是无法实现的,因为这个对象还需要其他对象。Mockito里面使用spy来对一个对象打桩,注意,打桩是获得了一个新对象,使用新对象才能获得打桩的效果。

List list = new LinkedList();
List spy = spy(list);

//可选的,你可以stub某些方法
when(spy.size()).thenReturn(100);

//调用"真正"的方法
spy.add("one");
spy.add("two");

TDD和BDD

很多人都听说过测试驱动开发(TDD),而BDD是行为驱动开发的简称,这是TDD的一种拓展。简单来说,TDD是面向测试用例的,也就是更加注重代码本身的测试,我个人觉得这部分工作应该有专门的测试负责。而研发自己做的测试更应该倾向BDD,BDD的典型特征就是每一个单元测试都有一个准确的名字告诉使用者我要测试什么,而且给出各种逻辑分区,比如spock,就有given,when,expect,then这种,更直白点,BDD的单元测试就算看不懂代码,不懂相关语言也是能知道测试是在做什么的,我个人更喜欢这种测试方式,因为能大大降低沟通成本。

本文到此就结束了,这只是我目前工作四个月关于测试的一些思考,以前我没有写单元测试的习惯导致代码非常野蛮,不过我的理解还比较简单只停留在事件层面,有机会要拜读一些测试领域相关的著作,也恳请路过的大佬指点一二。