加速Java应用开发速度3——单元/集成测试+CI
?
集成测试流程
?
集成测试流程?
?
?
可以看出,单元测试与集成测试唯一不同点是一个调用依赖系统而一个不调用;因为单元测试是最小粒度的测试,如在Java中是测试一个类,不会测试依赖系统;而集成测试是会测试依赖系统的。
?
测试的步骤:
环境:也叫做夹具(fixture)或者固件,表示调用被测系统时需要准备/清理的数据等等;
被测系统:在Java中就是要测试的类,如UserService;
依赖系统:测试被测系统时,其依赖的部分,如UserDao;
测试用例:包含测试方法的类,里边有很多测试方法来测试被测系统。
?
接下来仔细看看各部分都做了哪些工作。
?
2.2、环境?环境,也叫做夹具(fixture),表示调用被测系统时需要准备/清理的数据等等;保证测试时环境是干净的,如不被之前的数据库数据影响;保证每次测试都是在干净/新鲜的环境中执行的。所谓干净的环境表示如当前测试不被之前测试插入/删除/修改的数据造成影响。在junit中可以使用:
@Before(setUp) 安装夹具或准备环境:在测试用例的每个测试方法之前执行;比如创建新鲜的被测系统,单元测试时安装Mock的依赖系统;@After(tearDown)卸载夹具或清理环境:在测试用例的每个测试方法之后执行;比如数据库测试时回滚事务,删除数据;关闭文件;@BeforeClass:在整个测试用例之前执行;@AfterClass:在整个测试用例之后执行;使用如上方法,而不是直接在测试方法中安装/卸载;是因为不管有没有异常,@After/@AfterClass都会执行,这样防止出现异常可能造成环境是不新鲜的问题。
?
如果大家使用spring test来测试数据库相关的系统,可以考虑使用@TransactionConfiguration来支持默认事务回滚,这样不会对现有系统造成影响。具体可参考《【第十三章】 测试 之 13.1 概述 13.2 单元测试 ——跟我学spring3》和《【第十三章】 测试 之 13.3 集成测试 ——跟我学spring3》
?
测试时一定要保证环境是干净/新鲜的,才能保证每次测试的结果是一样的。
?
2.3、被测系统与依赖系统被测系统:在Java中就是被测试的Java类。
依赖系统:就是被测试Java类依赖的其他类。
?
如果是单元测试,一般情况下,会对依赖系统进行模拟(Mock),即给它一个假的实现;典型的如测试服务层时注入一个Mock的DAO层,这样的好处:
加快测试速度;因为不会调用真实的被测系统,所以速度特别快;测试还没有完成的功能;尤其在多团队协作时,可以只在定义好接口的情况下开发系统;?
如果是集成测试时,直接注入真实的依赖系统即可,好处:
完成联调;发现自己的问题;还可能发现自己使用上问题及使用的API的问题;单元测试虽然好,但是是隔离测试,即不会调用被测系统来完成测试,因为不是真实的联调,所以很可能会潜在有一些问题,因此还是需要集成测试。(所以不是很刻意分单元或集成测试,且有些系统可能只有集成测试)
?
但是集成测试速度是比较慢的,一般提交给CI执行,不影响当前开发进度。
?
2.4、验证验证的目的:是保证实际结果和我们预期的结果是否一致,说白了就是是否是我们想的那样。
?
一般使用断言来验证,如:
Assert.assertEquals(expectedResult, actualResult); //验证预期结果和实际结果是否相等
?
验证主要有两种:
结果验证行为验证结果验证:即验证被测系统返回的结果是否正确,如:
@Test public void testCount() { String ql = "select count(o) from User o"; long expectedCount = repositoryHelper.count(ql) + 1; User user = createUser(); repositoryHelper.getEntityManager().persist(user); long acutalCount = repositoryHelper.count(ql); Assert.assertEquals(expectedCount, acutalCount); }
验证返回的数据总数 = 插入之前的总数 + 1; 即结果验证。此处我们使用了一种叫做相对(delta)测试;即不关心数据库里到底多少条,只关心实际的和预期的差。
?
行为验证:即验证被测系统是否调用了依赖系统的某个API ,这个只有当我们使用Mock时测试时比较简单,如当用户注册时:
1、加积分
2、发系统消息
3、……
此时我们并不能通过结果验证是否调用了这些方法;那么我们可以使用Mock技术来完成验证是否调用了这些API,比如使用jmock测试框架就支持行为验证。集成测试是很难进行行为验证的,如果测试需要预留间谍接口。
?
3、测试有哪些好处?我们写代码的目的是正确的完成某个功能,如何保证正确呢?测试!所以在不使用如单元测试技术时,我们也是需要测试,但是这个测试是我们人工验证的。缺点很明显:
不是自动的,每次需要对比预期结果与实际结果,尤其数据量/逻辑复杂时更痛苦;不是回归的,上次测试完成后,下次还得重复自己一遍;为了解决这个问题,我们使用如单元测试技术来解决这个问题:
测试自动化;即验证预期结果与实际结果交给计算机吧;测试回归性,可以重复执行测试,验证修改后逻辑是否还是正确的;即测试的好处,从如上已经提炼出来了:
缩短发现问题到解决问题的时间;重复使用测试,保证修改后的代码还是正确的;如果做开源项目,可以提供给使用人员参考如何使用;因为单元测试都非常快,所以提升了开发速度;4、一切都需要测试吗?肯定不是,一切都是相对的;哪些不需要测试呢:
你非常熟悉的功能;
一些简单的CRUD;
你认为不需要测试的;比如你很有把握的东西,就没有必要浪费时间测试了;
哪些需要测试呢:复杂的业务逻辑/系统核心功能,最典型的如订单系统:一定要有足够的单元测试保证,这是一个电商系统的核心;还有如用户系统、积分系统等等;框架级别/工具级别/通用级别的代码需要测试,即提供给第三方使用的代码,因为这些代码可能被很多系统依赖,应该保证其正确性;而且还要保证以后版本升级的向下兼容;你认为需要测试的,比如你没有把握的东西,还是写点测试来缩短如开发web项目的重启系统的时间吧;?测试不是不耗时间的,没意义的测试就是浪费时间,最典型是一些书上的对一个增删改查进行测试,实际项目没有任何意义。所以你应该只对自己很难驾驭的觉得有必要的代码进行测试。不要成为一个测试狂,什么都测试。?
?
一些测试可以参考我的《es——JavaEE快速开发脚手架》中的代码。通过测试我得到了许多好处。?
?
到此我们介绍完成了测试,但是如果我们使用了如集成测试时,测试执行起来可能比较慢,跑一遍测试可能需要5分钟,那怎么办呢?
每天下班前跑一遍集成测试,然后修复,下班走人;
CI:持续集成,交给持续集成服务器,自动地测试完成后把测试报告以邮件的形式发到开发人员邮箱;
?
------------------------------------分割线----------------------------------
?
?
接下来介绍一下CI吧。
1、为什么需要CI
2、CI如何工作的
3、travis-ci介绍
?
1、为什么需要CI正如前边说的,我们单独测试可能会遇到如下问题:
如果写了一个测试,就要把所有测试跑一遍看看整个系统是否是正确的,那么每次等待时间是非常漫长的;如果团队中的其他成员改了功能并提交了,如何快速得到该次提交对当前系统代码是正确还是失败的反馈;那怎么办呢?自动化地持续集成(CI)!CI的核心就是干这件事情的。自动化持续地集成测试。
?
使用CI后,如果使用Maven,可以新建多个profile:
本地测试时忽略一些比较慢的测试;CI服务器上执行所有测试;?
2、CI如何工作的一个典型的持续集成流程:
?
如图所示:
?
?
?
持续集成服务器其实就是一个定时器,自动帮你下载最新代码、编译、测试、集成及产生报告发给开发人员。
?
常见的CI服务器有:
Apache ContinuumHudsonCruiseControlJenkins?CITeamCity?Travis CI?
我09年时使用过TeamCity社区版,足够满足常见需求;目前我使用github托管项目,使用Travis CI进行分布式的持续集成,免费,目前看来还是不错的。
?
3、travis-ci介绍我现在开发的ES-JavaEE项目开发脚手架就是使用travis ci进行持续集成;具体参考《Getting started》进行与Github集成,其支持的语言:
CC++ClojureErlangGoGroovyHaskellJavaJavaScript (with Node.js)Objective-CPerlPHPPythonRubyScala支持的数据库:
MySQLPostgreSQLMongoDBCouchDBRedisRiakRabbitMQMemcachedCassandraNeo4JElasticSearchKestrelSQLite3更多请参考其官网的介绍。
?
?
如果是Java开发人员,支持的JDK包括:OpenJDK 和 OracleJDK。 如果使用的是OpenJDK,Maven中使用ascii2native插件时,需要如下配置:?
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>native2ascii-maven-plugin</artifactId> <version>1.0-alpha-1</version> <executions> <execution> <phase>generate-resources</phase> <goals> <goal>native2ascii</goal> </goals> <configuration> <encoding>UTF-8</encoding> <src>src/main/messages</src> <dest>target/${project.artifactId}/WEB-INF/classes</dest> <includes>messages.properties</includes> </configuration> </execution> </executions> <!-- native2ascii 使用的tools.jar --> <dependencies> <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.7.0</version> <scope>system</scope> <systemPath>${java.home}/../lib/tools.jar</systemPath> </dependency> </dependencies> </plugin>
如果使用mysql,端口只能是3306。
如果想开端口测试,这是不允许的。
?
?
如下是我项目中的一个配置.travis.yml,放到项目的根下即可:
-----------------------------------
language: java ? ? ? ? ? 语言
?
env: ? ? ? ? ? ? ? ? ? ? ? ? ? 环境
? - DB=mysql ? ? ? ? ? ? ?使用mysql
?
jdk:
? - openjdk ? ? ? ? ? ? ? ?jdk使用openjdk
?
mysql:?
? database: es ? ? ? ? 数据库名为es
? username: root ? ? 用户名为root
? password : ? ? ? ? ? ?密码为空
? encoding: utf8 ? ? ?编码为utf8
?
install: ? ? ? ? ? ? ? ? ? ? 安装时执行的脚本
? - mvn install -Dmaven.test.skip=true ? ? mvn安装并跳过测试
?
before_script: ? ? ? ?script之前执行的测试
? - cd web ? ? ? ? ? ? ?
? - mvn db:create ?创建数据库的mvn命令(此处使用了?maven-db-plugin 插件)
? - mvn db:schema ?创建脚本的mvn命令
? - mvn db:data ? ? ? ?安装数据的mvn命令
? - cd ..
?
script: ? ? ? ? ? ? ? ? ? ? ?测试时执行的脚步
? - cd common?
? - mvn test ? ? ? ? ? ? ?测试common子模块
? - cd ..
? - cd web
? - mvn test -Pit ? ? ? 测试web子模块,并指定使用it profile测试(即集成测试的配置,具体参考pom.xml中的profile/it)
?
notifications: ? ? ? ? ?触发
? email: ? ? ? ? ? ? ? ? ?测试完成后测试报告发到哪
? ? - zhangkaitao0503@gmail.com ?
-----------------------------------
?
?
持续集成不能修复代码的错误,而是和单元测试一样,缩短发现问题带解决问题的时间,这样可以提高开发效率,降低项目风险,提高项目的稳定性。而且尤其是团队协作时,可以发现其他人的代码是否对自己的代码产生影响。?
?
?
到此我们利用单元测试+CI可以加速开发人员的开发速度。利用好单元测试和CI,不要纯粹为了单元测试和CI而去做这些事情。
?
本文没有介绍TDD,TDD并不会那么美好,我认为我们可以借鉴TDD的一些思想,但决不能迷信TDD,有时候,尤其如开发企业应用,先写功能再写测试可能效率更高,而且大部分时候是不需要TDD的。而且我也没能在实际项目中获取太多TDD的好处,但是我获得了测试的好处。
?
本文也没有介绍测试覆盖率,我认为不要一味的追求覆盖率,有时候有的覆盖率没有任何意义。所以不要让为了覆盖率而覆盖率拖慢了项目开发进度。
?
?
正如stackoverflow上的一篇帖子《How deep are your unit tests?》上Kent Beck的回答: