业务逻辑层(service层)单元测试的实践

Service层单元测试实践

为了更好的持续集成,我们需要单元测试覆盖到逻辑层(Service)和数据访问层(Dao)。
1. Service层开展单元测试的困境
Dao层我们可以使用Unitils、Spring、Dbunit结合,Dbunit方便开发人员准备数据,Spring配置文件也为单元测试专门做了优化,使用了测试数据源,事务的问题也解决。
但是Service层的问题就复杂很多,遇到的问题主要如下
1、业务逻辑复杂,分支繁多。不仅要构造正常的情况,还要测试异常的分支,这比Dao仅仅是几条sql就复杂多了。复杂的逻辑加上很多异常无法构造,一些关键的异常分支无法覆盖。
2、数据库垂直切分的设计,Service层不得以操作了多个数据库,而连接多个数据库导致测试极慢,另外还因为涉及到跨数据库事务的难题,这个时候使用DBUnit来准备每个数据库的数据的方法已经不能适应了,整个数据库的环境是不稳定的。
3、Service层的Spring配置文件复杂,不仅包括了数据库的配置,还有JMS队列、缓存等等。启动测试就需要这些环境的配合,稍微一个不小心就会出现配置错误,整个测试失败。测试受环境影响,容易集成失败。
2. 解决方案
经过大量的实践,我们认为不应该是让Service层的单元测试依赖太多的东西,,单元测试要体现“单元”的概念,不依赖数据库、不依赖Spring上下文。
根据这个原则,我们考虑使用使用Mock对象,把Service层用到的Dao等对象都一一mock并插入到Service对象中。然后通过Unitils模拟Dao的返回值,或者抛出异常。这样就可以把Service的测试完全隔离开。经过处理后,Service的覆盖率和处理速度都得到了提升。

下面根据一个实际的例子讲解如何开展Service层的单元测试。
订单业务逻辑是这样一个场景:
用户在网站上下了一个订单,后台处理订单,OrderService对象提供了一个processOrder的方法给外部调用,首先根据订单Id获取订单的信息,根据订单中关联的accountId获得用户的帐户相关信息,然后判断帐户中的余额是否大于当前订单的金额,如果是,则在用户帐户上扣取订单相应的金额,然后返回成功。如果否,则直接返回失败。
OrderService的代码如下
Java代码 收藏代码
public class OrderService {

OrderDao orderDao;  

AccountDao accountDao;  

/** 
 * 处理订单,在用户的帐户中扣取订单的金额 
 *  
 * @param orderId 
 * @return 
 */  
@Transactional  
public boolean processOrder(int orderId) {  
    // 获取订单详情  
    Order order = orderDao.getOrder(orderId);  

    Assert.notNull(order, "orderId is valid");  
    // 获取帐户信息  
    Account account = accountDao.getAccount(order.getAccountId());  

    Assert.notNull(account, "accountId is valid");  

    // 判断当前用户帐户余额是否大于订单的金额  
    if (account.getBalance() > order.getOrderAmount()) {  
        // 更新用户的帐户余额,减去订单的金额  
        accountDao.updateAccount(order.getAccountId(), account.getBalance() - order.getOrderAmount());  
        // 将订单改为已处理状态  
        orderDao.updateOrder(orderId, (byte) 1);  
        // 返回成功  
        return true;  
    } else {  
        // 如果余额不够,返回订单处理失败  
        return false;  
    }  
}  

}

一、为了测试,需要在Maven的POM文件中增加如下的配置
Java代码 收藏代码

org.unitils
unitils-mock
unitils.versiontestorg.unitilsunitilsinject {unitils.version}
test


com.alibaba
fastjson
test


org.unitils
unitils-io
${unitils.version}
test

unitils.version目前最新的为3.3版本

二、Unitils的环境配置
Unitils的启动,需要一个配置文件unitils.properties,这个文件默认需要放到classpath下。不过Service层不需要数据的设置,所以使用默认的配置即可, 不需要unitils.properties。

三、测试数据的准备
和Dao层有Dbunit导出测试数据不一样,Service层测试数据准备很麻烦,需要为每个Dao的返回对象做假数据。一般的String还好,返回JavaBean的就麻烦,而特别悲催是那种返回一个list的JavaBean接口,JavaBean还嵌套其他Bean,要一个个对象、属性的填塞。不行的是Dao的query函数往往都是返回这种List对象的,这样导致测试代码比开发工作量还大,而且很难维护,很多开发人员有抵触情绪。
于是我们希望和Dbunit一样,将数据的准备通过资源文件来完成,不用在测试代码中构造。在评估之后,发现JavaBean和Json之间互转的效率高,而且方便。所以我们将Dao的返回转换为Json字符串打印保存下来,存放为js文件。然后在Service的测试中,在通过Unitils的IO能力,将文件内容读出为字符串,再转换为List/Bean的对象,放到Mock的Dao返回中。这样工作就轻松了很多。
为了测试,我们准备了两个JavaBean的文件
ACCOUNT.js
{“accountId”:”S31993k”,”balance”:100}
ORDER.js
{“accountId”:”S31993k”,”orderAmount”:65,”orderId”:2345,”orderStatus”:0}
测试的文件默认放在单元测试用例相同的package下。即类似src/test/resources/com/xxx/service的目录等

四、单元测试用例的编写
测试代码同样要继承UnitilsJunit3的基类,

Java代码 收藏代码
public class OrderServiceTest extends UnitilsJUnit3 {
// 被测试的Service对象
@TestedObject
OrderService orderService = new OrderService();
// 自动按照类型注入到被测试对象中
@InjectIntoByType
Mock orderDaoMock;
// 自动按照类型注入到被测试对象中
@InjectIntoByType
Mock accountDaoMock;
// 准备AccountDao返回的模拟对象数据
@FileContent(“ACCOUNT.js”)
private String accountJs;
// 准备OrderDao返回的模拟数据
@FileContent(“ORDER.js”)
private String orderJs;

//各个测试用例共享的测试数据  
Account account;  
Order order;  

@Override  
public void setUp() {  
    account = JSON.parseObject(accountJs, Account.class);  
    order = JSON.parseObject(orderJs, Order.class);  
}  

/** 
 * 测试正常流程 
 */  
public void testProcessOrder1() {  
    orderDaoMock.returns(order).getOrder(2345);  
    orderDaoMock.returns(1).updateOrder(2345, (byte) 1);  
    accountDaoMock.returns(account).getAccount("S31993k");  
    accountDaoMock.returns(1).updateAccount("S31993k", 35);  
    assertEquals(true, orderService.processOrder(2345));  

}  

/** 
 * 测试订单金额大于用户余额的情况 
 */  
public void testNotEnoughBalancen() {  
    // 可以对返回的数据微调,这样就不需要额外的数据文件了  
    account.setBalance(10);  
    order.setOrderAmount(100);  

    orderDaoMock.returns(order).getOrder(2345);  
    orderDaoMock.returns(1).updateOrder(2345, (byte) 1);  
    accountDaoMock.returns(account).getAccount("S31993k");  
    // accountDaoMock.returns(1).updateAccount("S31993k", 35);  
    assertEquals(false, orderService.processOrder(2345));  

}  

/** 
 * 测试订单号存在的情况 
 */  
public void testOrderNotExist() {  
    try {  
        orderService.processOrder(5544);  
        fail("This should not happended");  
    } catch (IllegalArgumentException e) {  
        assertTrue(true);  
    }  
}  

/** 
 * 测试订单关联的帐户不存在的情况 
 */  
public void testAccountNotExist() {  
    order.setAccountId("FakeNumber");  
    orderDaoMock.returns(order).getOrder(2345);  
    try {  
        orderService.processOrder(2345);  
        fail("This should not happended");  
    } catch (IllegalArgumentException e) {  
        assertTrue(true);  
    }  
}  

}

这里指的是OrderService是被测试的对象,使用@TestObject来指定。
Java代码 收藏代码
@TestedObject
OrderService orderService = new OrderService();

请注意,这里Service是我们代码中直接new出来的,而不是Spring中拼装的。
Java代码 收藏代码
@InjectIntoByType
Mock orderDaoMock;

// 自动按照类型注入到被测试对象中 

Java代码 收藏代码
@InjectIntoByType
Mock accountDaoMock;

因为涉及了帐户和订单表的操作,所以这里有两个Dao,我们通过Unitils的Mock对象模拟出来,然后使用@InjectIntoByType的标签,让Unitils自动按照类型插入到被测试对象中。

Java代码 收藏代码
@FileContent(“ACCOUNT.js”)
private String accountJs;
// 准备OrderDao返回的模拟数据
@FileContent(“ORDER.js”)
private String orderJs;

@FileContent是Unitils-io包中提供的一个工具,他可以方便的读取资源文件到测试类中的字符串类变量中。我们可以利用它把Json字符串读出来。@FileContent默认加载当前测试类所在package下的资源文件,如果有特殊需求可以修改unitils.properties的属性。这里建议使用默认的规则,方便资源文件的规整。

Java代码 收藏代码
@Override
public void setUp() {
account = JSON.parseObject(accountJs, Account.class);
order = JSON.parseObject(orderJs, Order.class);
}

因为每个测试方法都需要account和order对象的实例。所以我们将其抽取到setUp方法中,可以给各个测试方法公用。这里是使用了Alibaba的FastJson作为解析Json的工具。这个工具可以根据自己的项目决定。
下面的测试用例是测试一个正常的情况
Java代码 收藏代码
/**
* 测试正常流程
*/
public void testProcessOrder1() {
orderDaoMock.returns(order).getOrder(2345);
orderDaoMock.returns(1).updateOrder(2345, (byte) 1);
accountDaoMock.returns(account).getAccount(“S31993k”);
accountDaoMock.returns(1).updateAccount(“S31993k”, 35);
assertEquals(true, orderService.processOrder(2345));

}  

使用
orderDaoMock.returns(order).getOrder(2345);
模拟Dao的返回,其含义就是让orderDao在接收到参数为‘2345’的时候,返回的对象是预制的order对象。模拟后,使用断言确定返回是否正确。
为了提高分支的覆盖率,我们在后面分别制造了订单金额大于余额的情况,和帐户、订单不存在的情况作为异常的测试。代码都很简单,不再一一赘述。

  1. 经验总结
    一、 Service的数据准备还是手工进行的,以后可以考虑写一些套件,自动录制Dao的输出,然后在Service的测试中回放出来。
    二、 Mock对象不仅可以模拟返回值,也可以按照要求抛出异常等,可以参考Unitils的说明。
    三、 测试代码也需要当做是正式代码一样呵护,经常性的进行重构,避免代码冗余。比如setUp方法中的公用方法就是后期抽取出来的。
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值