如何定义单元
对于单元测试中的单元,不同的人有不同的看法:可以理解为一个方法,可以理解为一个完整的接口实现,也可以理解为一个完整的功能模块或者是多个功能模块的一个耦合。
根据以往的单元测试经验,在设计单元测试用例时,当针对方法级别展开单元测试时,重点关注的是方法的底层逻辑;当针对的是模块时,针对的是实际的业务逻辑实现;当针对整合后的模块进行测试时,一般称之为集成测试。
不管是单元测试还是集成测试,都可以统一的理解为单元测试。因为他们的本质都是对方法或接口的一种测试形式,只是所处的阶段不一样罢了。
1. 集成测试应该由谁编写
在我们的实际工作中,研发人员在提交代码之前,会设计一些“冒烟测试”级别集成测试用例。等到整个功能开发完成后,测试人员会根据业务需求和设计的测试用例,来进行整体的集成测试用例的编写、执行、失败用例分析,以及代码的调式和问题代码的定位等工作。
2. 集成测试用例
业务相关的测试主要是通过spring-test来进行集成测试,基本的测试结构为先定义一个基类用来初始化被测试类。
测试基类定义结构如下:
@RunWith(SpringJUnit4ClassRunner.class)
ContextConfiguration(locations = {"classpath:./spring/applicationContext.xml"})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class BaseSpringJunitTest {
@Autowired
protected BusinessRelatedServiceImpl businessRelatedService;
}
业务相关的测试类定义如下格式:
public class BusinessRelatedServiceImplDomainTest extends BaseSpringJunitTest {
@Test
public void testScenario1 (){
new Thread(new DOSAutoTest("testScenario1")).start();
Thread.sleep(1000*60*1);
String requestJson=""//测试入参;
RequestPojo request=( RequestPojo )JSONUtils.jsonToBean(requestJson,RequestPojo .class);
ResponsePojo response= businessRelatedService.businessRelatedMethod(ResponsePojo );
//业务相关的assert区域
}
}
3. 如何解决下游系统依赖
businessRelatedMethod方法在处理业务逻辑的过程中需要调用下游JSF(Jingdong Service Framework,完全自主研发的高性能RPC服务框架)提供的订单接口(OrderverExportService),并根据入参中的订单编号获取订单的详细信息(ResultPojo getOrderInfoById (long orderId))。
那么如何获取下游JSF接口的返回正确数据就变成了一个比较重要的问题。如果是在功能测试或者联调测试阶段,可以由下游测试人员来提供数据。不过这样沟通和测试成本较高,无法满足业务快速上线和变化的要求,尤其在集成测试阶段这个问题就变得尤为明显,因为下游数据对于上游来说是不可控的。这样mock下游数据就变得尤为紧急和重要。
4. Mock框架的选择
在整个java生态圈中,支持mock的开源框架还是比较多的,比如常用的mockito、powermock、easymock和jmockit等开源框架。这些框架在mock方面都具有比较强大的功能与比较广泛的使用量。但是这些框架都具有一个相同的缺点,那就是需要或多或少的编码工作来mock所需要的接口返回数据。
在设计mock框架的时候,我们考虑到尽量让写单元测试的人员或研发人员少编码或不编码,来获取不同的业务场景所需要的测试数据。
Mock框架 第一版
#p#分页标题#e#该版本的mock框架的整体思想为:结合JSF的特性,Override所有下游接口的方法,然后将实现下游接口的应用部署到测试环境,发布一个有别与真实下游接口的服务,在接口调用的时候,通过不同的JSF接口别名来进行区分。Mock的数据存储在数据库中。
该框架类调用关系:
Mock接口的具体实现:
public class OrderverExportServiceImp extends OrderverExportServiceAdapter {
@Resource
private OrderverMapper orderverMapper;
@Override
public ResultPojo getOrderInfoById (long orderId) {
OrderverPojo orderverMock=orderverMapper.getOrderId(new Long(orderId).toString());
ResultPojo result=new ResultPojo ();
result.setFiled1(null);
result.setFiled1(0);
result.setFiled2(null);
result.setFiled3(null);
result.setResult(true);
…//mock需要的数据
result.setReturnObject(orderver);
return result;
}
}
Mock服务发布完后的效果:
在集成测试阶段,只需要修改该接口的JSF别名,就可以实现该接口的mock调用。
<jsf:consumer id="orderverExportServiceJsf" interface="xxx.xxx.xxx.xxx.xxx.OrderverExportService"
protocol="jsf" timeout="${timeout}"
alias="${alias}" retries="2" serialization="hessian">
</jsf:consumer>
alias=orderver_mock
该框架的优缺点
优点:
做集成测试用例设计时,不用编写代码,只需要维护测试场景所需要的返回数据;
该框架不仅可以用在集成测试中,在下游接口无变更的前提下,同时还可以用在后续系统测试与联调测试阶段。
缺点:
mock服务的发布依赖于服务器与数据库,当依赖的服务器或数据库出现跌机情况时,该mock服务不用;
该框架的维护成本比较大,当下游依赖的接口较多时,所有的服务包含的方法均需要进行override;
当下游的接口定义发生变化时比如新增接口方法,该mock服务需要重新override该新增的方法并且需要重新打包部署;
下游接口方法的数据结构发生变化时,存储数据的数据表结构需要做相应的调整,对于业务变化较快的系统,这种类型的改动频率还是较高。
Mock框架 第二版
#p#分页标题#e#为了解决上述mock框架依赖服务器与数据库的问题,我们又做了第二次尝试。将mock框架设计为jar包的形式,提供给程序来调用。在下游接口的实现方式上第二版与第一版保持不变,同时业务数据不放数据库,而是将业务数据放到文件中。变化的点为接口调用上需要将对应的jsf:comsumer节点替换为对应的实际mock的实现类。
Mock接口的实现:
@Service("orderverExportService")
public class OrderverExportServiceMock extends OrderverExportServiceAdapter {
@Override
public ResultPojo getOrderInfoById(long orderId) {
ResultPojo result=new ResultPojo ();
result.setFiled1(null);
result.setFiled1(0);
result.setFiled2(null);
result.setFiled3(null);
result.setResult(true);
…//mock需要的数据
result.setReturnObject(orderver);
return result;
}
}
Mock接口调用配置:
<!--<jsf:consumer id="orderverExportServiceJsf" interface="xxx.xxx.xxx.xxx.xxx.OrderverExportService" protocol="jsf" timeout="${timeout}"alias="${alias}" retries="2" serialization="hessian">
</jsf:consumer>-->
<bean id="orderverExportServiceJsf" class="xxx.xxx.xxx.xxx.xxx.OrderverExportServiceMock"></bean>
该框架的优缺点
优点:
做集成测试用例设计时,不用编写代码,只需要维护测试场景所需要的返回数据;
相比较第一个版本,该版本在执行效率上有了较大的提升,因为mock类的加载是走的本地Spring配置文件,同时数据加载也是走的本地文件;
无需再依赖于服务器部署和数据库依赖。
缺点:
该框架的维护成本比较大,当下游依赖的接口较多时,所有的服务包含的方法均需要进行override;
当下游的接口定义发生变化时比如新增接口方法,该mock服务需要重新override该新增的方法并且需要重新打包,然后上传到maven仓库;
下游接口方法的数据结构发生变化时,对于业务变化较快的系统,这种类型的改动频率还是较高。
Mock框架 第三版
随着需要mock的接口变的越来越庞大,以上两种mock框架的实现的缺点就变的越来越突出。该框架可以说从根本上解决了上述框架实现的问题。因为该框架充分利用了JDK的动态代理,反射机制以及JSF提供的高级特性来实现我们的mock框架。框架维护任务可以做到无需做更多的针对接口的编码任务。测试人员只需要将重点放在测试数据的准备上。
框架整体调用时序图:
框架的核心类图:
其中DOSAutoTest类用来启动和发布JSF的mock接口,JSFMock通过动态代理的方式,实现下游接口的mock功能并根据测试场景获取对应的mock数据。
其中,mock的数据以json格式存储在mock框架项目工程的指定目录下。
该框架解决的问题
省去了利用第三方mock框架如jmockit,mockito,powermock时,需要在单元测试或集成测试类中写mock代码的麻烦;
该框架模拟数据返回时,完全的模拟了接口之间的调用关系;
测试人员或研发人员在利用该框架mock数据时,无需额外的代码,就可以实现mock数据的返回;
在模拟下游数据返回时,发布的mock接口调用完成后就自行销毁,无需额外服务器进行部署与维护。
在进行接口mock时,无需在mock框架中添加相关的接口maven依赖。
单元测试展开方式
1. 单元测试应该由谁编写
单元测试由谁编写?针对这个问题,大家在网上会找到不同的观点:
#p#分页标题#e#一个观点是,谁写代码,谁自己写单元测试。当然,有的结对编程里面,也有相互写的,不过,这个过程中,两个人是共同完成的代码。也不违反谁写代码谁写单元测试的原则。
另一个观点是单元测试应该由其它的研发人员或测试人员来进行编写,理由大概可以理解为对于非代码编写人员来说,在设计单元测试用例的时候,对应的是一个黑盒。在这样的背景下,设计出来的用例覆盖程度更高。
2. 单元测试的行业现状
如果研发来负责单元测试的编写,很多时候研发人员都不编写单元测试。研发人员不编写单元测试的原因其实也是比较容易理解的,因为编写单元测试用例工作太耗时。有时候研发的经理或项目的业务方会认为单元测试用例会减缓项目的整体进度。有时候甚至整个公司层面都不认可花费大量的时间在单元测试上是合理的,尤其是在项目周期紧张和业务变动较大的项目上。因为单元测试从一定程度上来说确实增加的研发人员的编码量,同时还会增加代码的维护成本。
如果测试来负责单元测试的编写,目前的现状是测试人员需要时间理解代码,写单元测试的时间会变长。有代码修改之后,在项目的测试压力之下,有的测试人员,就选择不维护单元测试,而选择赶紧完成传统的手工测试。
3. 单元测试用例自动生成
人工编写测试用例成本增加,那么我们考虑是否可以通过自动生成的方式来实现单元测试呢?EvoSuite是由Sheffield等大学联合开发的一种开源工具,用于自动生成测试用例集,生成的测试用例均符合Junit的标准,可直接在Junit中运行。
对于非业务相关的模块,在单元测试的实践中,就可以直接使用上述工具来自动生成单元测试代码。虽然该工具只是辅助测试,并不能完全取代人工,测试用例的正确与否还需人工判断,但是通过使用此自动测试工具能够在保证代码覆盖率的前提下极大地提高测试人员的开发效率。
下面来详细介绍如何使用该工具生成单元测试用例以及如何检查单元用例的正确性。
EvoSuite为Maven项目提供了一个插件,该插件的具体配置如下所示:
<plugin>
<groupId>org.evosuite.plugins</groupId>
<artifactId>evosuite-maven-plugin</artifactId>
<version> ${evosuiteVersion} </version>
<executions><execution>
<goals>
<goal>
prepare
</goal>
</goals>
<phase>
process-test-classes
</phase>
</execution></executions>
</plugin>
除了需要配置上述plugin外,maven还需要做如下的配置:
<dependency>
#p#分页标题#e#<groupId>org.evosuite</groupId>
<artifactId>evosuite-standalone-runtime</artifactId>
<version>${evosuiteVersion}</version>
<scope>test</scope>
</dependency>
<!--上述依赖主要是用来自动生成单元测试用例-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version>
<configuration>
<systemPropertyVariables>
<java.awt.headless>true</java.awt.headless>
</systemPropertyVariables>
<testFailureIgnore>true</testFailureIgnore>
<skipTests>false</skipTests>
<properties>
<property>
<name>listener</name>
<value>org.evosuite.runtime.InitializingListener</value>
</property>
</properties>
</configuration>
</plugin>
上述plugin主要是用来混合执行手动设计的单元测试用例和使用EvoSuite自动生成的单元测试用例。
以上EvoSuite所需的plugin和maven依赖配置完成之后,就可以使用maven命令来自动生成单元测试用例并执行了。
#p#分页标题#e#mvn -DmemoryInMB=2000 -Dcores=2 evosuite:generate evosuite:export test
生成测试用例后,可以通过人工排查生成测试用例的正确性。
写在最后
不管是研发还是测试负责集成或单元测试,选取适合自身项目的mock框架,一方面可以缩短测试代码的编写时间,另一方面可以加速测试代码的执行效率,同时又可以降低测试代码的维护成本。不管是行业中通用的mock框架还是定制化的框架,都可以广泛的应用的测试中。
因为做mock框架不是目的,目的是为了能高效的设计出更多的测试覆盖场景,来进一步提升测试效率、保证产品质量和将测试人员从繁重的手工测试中得以解放。
当单元测试代码已经准备完毕,如何才能发挥测试代码的作用以及如何评价测试代码的效率和做单元测试的投入产出比如何来衡量等等这些问题,将在后续的文章中给大家一一解答。
【本文来自51CTO专栏作者张开涛的微信公众号(开涛的博客),公众号id: kaitao-1234567】