关于单元测试


前言

你来看这篇文章一定是打算做单元测试了,或者说也在考虑单元测试是否能真正帮助到你?

Q1: 为什么一定要做单元测试?

单元测试是一切测试进行的基石,它针对最小粒度unit的测试,位于测试金字塔的最底层,再往上是Service(比如淘宝体系下的HSF接口测试)-UI(比如基于webdriver的UI自动化测试),在实践的过程中你会发现单元测色是对代码进行测试的最有效工具之一

有更快的 开发-验证 循环(开发过程被划分为更小的快速迭代过程),无需等待依赖实现,无需特定环境

节省调试时间(越复杂的功能,潜在收益越大)

帮助应对业务的快速变化

便于项目交接或多人协作

Q2: 单元测试的粒度要做多细?

我的感受是:一不追求数量二不追求覆盖率,设计有效的案例进行单元测试实践。

StackOverflow上敏捷开发实践的奠基人Kent Beck也对此问题表达了观点:“我倾向于去对那些有意义的错误做测试,所以,我对一些比较复杂的条件逻辑会异常地小心。”

UT的粒度是多少,这个不重要,重要的是你会不会自己思考你的软件应该怎么做,怎么测

相关文档阅读: * 单元测试要做多细


哪些场景需要做单元测试

之前在工作中根据webx工程的分层总结了下单元测试、接口测试、UI测试的场景划分,这是一个较为理想的实践建议:

为什么说是较为理想的实践建议?看看现实 * 有人说dal层的CRUD我都写了多少年了,项目时间这么紧的情况下还要写这些无趣的增删改查单测 * 翻翻现有的应用代码,偶有零散的单元测试代码可见,但基本数据都是一次性(也就是不可持续跑起来),或者单元测试中无assert(只有System.out.println) * 环境常常跑不起来 * 数据准备麻烦,或者依赖的外部环境太多

我只能说,如果你能从中获得收益,就更能辩证地看待这些问题。


怎么写单元测试

单元测试的流程:场景(数据)构造-执行-断言(校验)。

1. 涉及的框架选型

主要在于单元测试框架、mock框架、数据准备、断言库。 之前对它们做了些简单的比较:框架对比调研


2. 写一个单元测试

基于前面也提到的一些问题,实践中我们常常省略了对dal层的单元测试,而是对一些逻辑复杂的集成单元进行单测(往往是biz中的一些manager实现类),这里用最基础的Junit4+Mockito来演示一下整个流程。

2.1 pom依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.taobao.hsf</groupId>
    <artifactId>hsfunit</artifactId>
    <version>1.0.5-SNAPSHOT</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>2.0.2-beta</version>
</dependency>


2.2 BaseTestCase

bean的加载、pandora容器启动(hsfunit)

public class BaseTestCase {
    protected static ApplicationContext ac = null;
    private static String[] configXml = {
        "basecase.xml"
    };
    static {
        try {
            HSFEasyStarter.start("hsfunit", "1.4.9.6");
            ac = new ClassPathXmlApplicationContext(configXml);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}


2.3 测试脚本编写

设计好要测试的场景,包括需要的数据准备、待测试接口、校验点。

public class BomMsgUnitTest extends BaseTestCase{
    private BomCore bomCore;
    private InventoryBomDAO inventoryBomDAO;

    private static final long  WAREHOUSE_ID = 8;
    private static final long  OWNER_ID = 8;

    private List<InventoryBomDTO> boms = new ArrayList<InventoryBomDTO>();
    private static final long BOM_NUM = 10L;


    @Before
    public void init(){
        bomCore = (BomCore)ac.getBean("bomCore");
        inventoryBomDAO = (InventoryBomDAO)ac.getBean("inventoryBomDAO");
        //数据初始化
        createBomAndStart();
    }

    @After
    public void destroy(){
        //省略数据清理
    }

    @Test
    public void test_N_CacheUpdate(){
        int cacheCountOld = bomPartMap.get(WAREHOUSE_ID + "_" + OWNER_ID).size();
        if (cacheCountOld > 0){
            System.out.println("初始时的cache列表:");
            listCacheBom(WAREHOUSE_ID, OWNER_ID);
        }

        for(int i = 0; i < BOM_NUM/2; i++){
            bomCore.stopUsing(boms.get(i).getId());
        }

        int bomCount, cacheCount;
        List<InventoryBomDO> list = inventoryBomDAO.getBomPartList(WAREHOUSE_ID, OWNER_ID);
        bomCount = list.size();
        cacheCount = bomPartMap.get(WAREHOUSE_ID + "_" + OWNER_ID).size();
        System.out.println("bomCount = " + bomCount + ", cacheCount = " + cacheCount);
        System.out.println("stop更新后的cache列表:");
        listCacheBom(WAREHOUSE_ID, OWNER_ID);
        Assert.assertEquals(bomCount, cacheCount);
        Assert.assertEquals(cacheCountOld, cacheCount + BOM_NUM / 2);
    }
}


2.4 当你需要mock

在单元测试中我们常常希望关注到核心代码逻辑本身的正确性,而暂时不用去关注所有的外部依赖,这时mock框架可以帮到我们。抽象来看适用两种场景:

  • 直接创建mock对象,并设置对象方法调用时的预期返回值
  • 被测bean A中,mock A依赖的bean B的行为动作(在spring框架中做待测类的mock时常用)
  • mock一些异常场景,比如按预期抛异常验证事务性回滚是否正确

具体使用实例参加之前写过的一篇文章:在spring中使用mockito


2.5 如何管理配置文件

常常需要把web子工程下的关于bean的配置文件拷贝到测试目录src/test/resources下,但这会带来一个问题,同一份配置文件在多处做维护,一旦配置出现修改都会造成不一致,最终会使得测试用例运行失败;而且维护需要成本。

一个最直接的方式,在测试BaseTestCase中使用配置文件时直接引用web子工程下的是否可行?可以。

(假设我们所有的单元测试单独写在utest子工程下)

一方面需要在utest的pom文件中,maven-surefire-plugin插件中增加classpath路径,这样就对路径可见,如下:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <includes>
            <include>**/**Test.java</include>
        </includes>
        <argLine>-Xmx512m</argLine>
        <additionalClasspathElements>
            <additionalClasspathElement>
                ${project.parent.basedir}/web/src/main/webapp
            </additionalClasspathElement>
        </additionalClasspathElements>
    </configuration>
</plugin>

另一方面,一般webx工程的bean配置总入口在webx.xml中,这里的import的配置分很多类,比如表单验证配置等等,但我们关心的是各个模块的bean导入部分,所以为了utest子工程引用这些bean更方便,各个模块的bean配置应放到一起,并规范一个总的引用入口

是否这样就可以了?

等等,发现web子工程下bean的配置不是纯粹的.xml文件,而是使用了auto-config进行替换的.vm文件,这就麻烦了,因为auto-config是运行期间做替换的,如果使用maven的autoconfig插件,只有在执行mvn package或者mvn install才会激活替换过程。也就是在执行单元测试时是没法做这个动态替换的。

综合上述,我们可以规范一下配置使用方式:

  • web子工程下的各个模块的bean配置放到一起,并规范一个总入口,如上面例子中的basecase.xml;
  • 这些bean的配置不使用autoconfig工具做动态替换,只在antx.properties维护相应的版本号信息等
  • utest的pom文件中增加classpath路径;
  • utest中引入总入口的bean配置,并增加antx.properties来做一些版本号信息的替换,即一下这段
<!--占位符配置开始 -->
<bean id="propertyConfigurer"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location">
        <value>antx.properties</value>
    </property>
</bean>


代码规范

  • 测试类命名
    • Java类路径src/main/java/下com.cainiao.wmpinventory.BomMsg
    • 对应的测试类路径src/test/java下com.taobao.wmpinventory.BomMsgUnitTest (单元测试用UTest结尾,接口集成测试用ITest结尾)
  • 测试方法命名(N表示正常流,E表示异常流,description一定要用英文)
    • test_N_description
    • test_E_description
  • 脚本代码中一定要写断言!禁止使用System.out.println做断言


crystal /
Published under (CC) BY-NC-SA in categories 测试  tagged with 单元测试