在项目开发过程中必不可少的会用到测试框架来检查自己的代码逻辑,可能大多数人和我一样从来没有怎么重视过测试代码,认为测试代码存在与否的意义不大。但是,看过很多大牛的项目后,发现他们写的项目中测试用例的代码远多于实际代码。所以,为了向大牛们看齐,最近又重新学习了一波之前用到过的测试框架,在这里做一个小结。
我在项目开发过程中使用的单元测试框架有Junit、TestNG以及Mockito,Junit和TestNG使用的比较多,Mockito最近才开始使用。对于Junit和TestNG的用法我就不多做介绍了,相信大家都会使TestNG和JUnit是针对Java语言的两个比较常用的测试框架。JUnit出现的比较早,但是早期的JUnit 3对测试代码有非常多的限制,使用起来很不方便,后来的JUnit 4得到很大的改进。TestNG的出现介于JUnit 3和JUnit 4,但是TestNG在很多方面还要优于JUnit 4。下面从整体上对TestNG和JUnit 4进行比较全面的比较。
TestNG与JUnit的相同点
1. 使用annotation,且大部分annotation相同。
2. 都可以进行单元测试(Unit test)。
3. 都是针对Java测试的工具。
TestNG与JUnit的不同点:
1. JUnit只能进行单元测试,TestNG可以进行单元测试,功能测试,端到端测试,集成测试等。
2. TestNG需要一个额外的xml配置文件,配置测试的class、method甚至package。
3. TestNG的运行方式更加灵活:命令行、ant和IDE,JUnit只能使用IDE。
4. TestNG的annotation更加丰富,比如@ExpectedExceptions、@DataProvider等。
5. 测试套件运行失败,JUnit 4会重新运行整个测试套件。TestNG运行失败时,会创建一个XML文件说明失败的测试,利用这个文件执行程序,就不会重复运行已经成功的测试。
TestNG比JUnit 4灵活性的体现:
1. JUnit 4中必须把@BeforeClass修饰的方法声明为public static,这就限制了该方法中使用的变量必须是static。而TestNG中@BeforeClass修饰的方法可以跟普通函数完全一样。
2. JUnit 4测试的依赖性非常强,测试用例间有严格的先后顺序。前一个测试不成功,后续所有的依赖测试都会失败。TestNG 利用@Test 的dependsOnMethods属性来应对测试依赖性问题。某方法依赖的方法失败,它将被跳过,而不是标记为失败。
3. 对于n个不同参数组合的测试,JUnit 4要写n个测试用例。每个测试用例完成的任务基本是相同的,只是受测方法的参数有所改变。TestNG的参数化测试只需要一个测试用例,然后把所需要的参数加到TestNG的xml配置文件中。这样的好处是参数与测试代码分离,非程序员也可以修改参数,同时修改无需重新编译测试代码。
4. JUnit 4的测试结果通过Green/Red bar体现,TestNG的结果除了Green/Red bar,还有test-output文件夹,对测试结果的描述更加详细,方便定位错误。
相比于Junit和TestNG,mockito的功能是解决units之间由于耦合而难于被单独测试的问题,下面做下简单介绍,具体的用法请参照http://site.mockito.org/
什么是mock?
在软件开发的世界之外, “mock”一词是指模仿或者效仿。 因此可以将“mock”理解为一个替身,替代者. 在软件开发中提及”mock”,通常理解为模拟对象或者Fake。
为什么需要Mock?
Mock是为了解决units之间由于耦合而难于被测试的问题。所以mock object是unit test的一部分。
Mock的好处是什么?
提前创建测试,TDD(测试驱动开发)这是个最大的好处吧。
如果你创建了一个Mock那么你就可以在service接口创建之前写Service Tests了,这样你就能在开发过程中把测试添加到你的自动化测试环境中了。换句话说,模拟使你能够使用测试驱动开发。
团队可以并行工作
这类似于上面的那点;为不存在的代码创建测试。但前面讲的是开发人员编写测试程序,这里说的是测试团队来创建。当还没有任何东西要测的时候测试团队如何来创建测试呢?模拟并针对模拟测试!这意味着当service借口需要测试时,实际上QA团队已经有了一套完整的测试组件;没有出现一个团队等待另一个团队完成的情况。这使得模拟的效益型尤为突出了。
你可以创建一个验证或者演示程序
由于Mocks非常高效,Mocks可以用来创建一个概念证明,作为一个示意图,或者作为一个你正考虑构建项目的演示程序。这为你决定项目接下来是否要进行提供了有力的基础,但最重要的还是提供了实际的设计决策。
为无法访问的资源编写测试
这个好处不属于实际效益的一种,而是作为一个必要时的“救生圈”。有没有遇到这样的情况?当你想要测试一个service接口,但service需要经过防火墙访问,防火墙不能为你打开或者你需要认证才能访问。遇到这样情况时,你可以在你能访问的地方使用MockService替代,这就是一个“救生圈”功能。
Mock 可以交给用户
在有些情况下,某种原因你需要允许一些外部来源访问你的测试系统,像合作伙伴或者客户。这些原因导致别人也可以访问你的敏感信息,而你或许只是想允许访问部分测试环境。在这种情况下,如何向合作伙伴或者客户提供一个测试系统来开发或者做测试呢?最简单的就是提供一个mock,无论是来自于你的网络或者客户的网络。
隔离系统
有时,你希望在没有系统其他部分的影响下测试系统单独的一部分。由于其他系统部分会给测试数据造成干扰,影响根据数据收集得到的测试结论。使用mock你可以移除掉除了需要测试部分的系统依赖的模拟。当隔离这些mocks后,mocks就变得非常简单可靠,快速可预见。这为你提供了一个移除了随机行为,有重复模式并且可以监控特殊系统的测试环境。
其实上面的特点总结成一句话:Mock可以解决测试对象的依赖问题,让测试能够只针对测试对象本身的逻辑,保证测试不受依赖的影响。
下面有个例子可以清晰的体现这一点,直接上代码:
public class OrderService {
private PriceService priceService=new PriceService();
private ProductService productService=new ProductService();
/**
* 该方法是返回订单信息
* 依赖价格服务、商品服务
* @param orderCode
* @return
*/
public OrderInfo getOrderInfoByOrderCode(String orderCode){
PriceInfo priceInfo=priceService.getPriceInfoByProductCode("L3001");
ProductInfo productInfo=productService.getProductInfoByProductCode("L3001");
OrderInfo orderInfo=new OrderInfo();
orderInfo.optionText=productInfo.optionText;
orderInfo.productCode=productInfo.productCode;
orderInfo.productName=productInfo.productName;
orderInfo.price=priceInfo.price;
return orderInfo;
}
}
对于上面这个方法,在不使用Mockito来进行测试时,需要价格服务的getPriceInfoByProductCode和商品服务的getProductInfoByProductCode方法开发完成才能进行测试,下面是不使用Mockito的测试代码
public class OrderServiceTestWithJunit {
private OrderService orderService;
private ProductService productService;
private PriceService priceService;
@Before
public void setUp() throws Exception {
orderService=new OrderService();
productService=new ProductService();
priceService=new PriceService();
ReflectionTestUtils.setField(orderService,"productService",productService);
ReflectionTestUtils.setField(orderService,"priceService",priceService);
}
@Test
public void getOrderInfoByOrderCode() throws Exception {
OrderInfo orderInfo=orderService.getOrderInfoByOrderCode("testCode");
Assert.assertNotNull(orderInfo);
}
}
这样的确可以完成对订单服务的测试,但是前提是产品服务和价格服务已经完成且保证正确性,如果依赖的服务存在异常,这时异常将会从订单服务抛出,那么定位异常将会非常麻烦。因此,对于这种有很多依赖的测试单元,只使用Junit或者TestNG,想要对测试单元本身的逻辑进行测试,几乎是不可能的。
下面介绍用Mockito的做法
public class OrderServiceTestWithMock {
@Mock
private ProductService productService;
@Mock
private PriceService priceService;
@InjectMocks
private OrderService orderService;
@BeforeClass
public void setUp(){
MockitoAnnotations.initMocks(this);
setMockData();
}
private void setMockData() {
when(priceService.getPriceInfoByProductCode(anyString())).thenAnswer(invoke->{
PriceInfo priceInfo=new PriceInfo();
priceInfo.price= BigDecimal.valueOf(100);
return priceInfo;
}
);
when(productService.getProductInfoByProductCode(anyString())).thenAnswer(invocation -> {
ProductInfo productInfo=new ProductInfo();
productInfo.optionText="10支装";
productInfo.productCode="L3001";
productInfo.productName="mock商品";
return productInfo;
});
}
@Test
public void testGetOrderInfoByOrderCode() throws Exception {
OrderInfo orderInfo=orderService.getOrderInfoByOrderCode("Test");
Assert.assertNotNull(orderInfo.optionText);
}
}
这样就可以去除订单服务的所有依赖,只对订单服务的逻辑进行测试(其实就是可以甩锅!!!)。
源码见: https://git.oschina.net/F4T/test-demo.git
JUnit 4 和 TestNG 在表面上是相似的,然而设计 JUnit 的目的是为了分析代码单元,而 TestNG 的预期用途则针对高级测试。对于大型测试套件,我们不希望在某一项测试失败时就得重新运行数千项测试,TestNG 的灵活性在这里尤为有用。这两个框架都有自己的优势,需要按照测试要求来选择,不过对于开发来说其实选择那一个没有什么太大的区别,因为用到的测试框架的功能不多,都是些基本的用法,对于微服务或者多人协作完成一个功能的情况下,Mockito是必须接入。
其实开发写测试用例是一个不断梳理自己代码逻辑的过程,可能在写的过程中你就发现了逻辑上的漏洞。