`
方世玉
  • 浏览: 21966 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

业务逻辑层(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的代码如下
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文件中增加如下的配置
		<dependency>
			<groupId>org.unitils</groupId>
			<artifactId>unitils-mock</artifactId>
			<version>${unitils.version}</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.unitils</groupId>
			<artifactId>unitils-inject</artifactId>
			<version>${unitils.version}</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.unitils</groupId>
			<artifactId>unitils-io</artifactId>
			<version>${unitils.version}</version>
			<scope>test</scope>
		</dependency>

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的基类,


public class OrderServiceTest extends UnitilsJUnit3 {
    // 被测试的Service对象
    @TestedObject
    OrderService orderService = new OrderService();
    // 自动按照类型注入到被测试对象中
    @InjectIntoByType
    Mock<OrderDao> orderDaoMock;
    // 自动按照类型注入到被测试对象中
    @InjectIntoByType
    Mock<AccountDao> 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来指定。
  @TestedObject
    OrderService orderService = new OrderService();

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

    // 自动按照类型注入到被测试对象中
  
 @InjectIntoByType
    Mock<AccountDao> accountDaoMock;

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

 
 @FileContent("ACCOUNT.js")
    private String accountJs;
    // 准备OrderDao返回的模拟数据
    @FileContent("ORDER.js")
    private String orderJs;

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

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

因为每个测试方法都需要account和order对象的实例。所以我们将其抽取到setUp方法中,可以给各个测试方法公用。这里是使用了Alibaba的FastJson作为解析Json的工具。这个工具可以根据自己的项目决定。
下面的测试用例是测试一个正常的情况
/**
     * 测试正常流程
     */
    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对象。模拟后,使用断言确定返回是否正确。
为了提高分支的覆盖率,我们在后面分别制造了订单金额大于余额的情况,和帐户、订单不存在的情况作为异常的测试。代码都很简单,不再一一赘述。


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

相关推荐

    单元测试实践小结[5]

    单元测试实践小结[5] 软件测试 7.XML:XMLUnit 8.J2EE:MockRunner 9.GUI:JFCUnit,Marathor 10.Other:JTestCase(采用XML定义测试过程) 分层架构下的单元测试 1Web层的单元测试 主要测试Controller的数据结构化...

    计算机专业实习报告.pdf

    中南大学实习报告 实习地点: 湖南省软件测评中心 专业班级:信安 1001 班 姓 名... 软件的分层 – 视图层 (view:如 jsp,html) – 控制层 (controller) – 业务逻辑层 (service) – 数据访问层 (dao) – 数据层 (bean

    基于SSM的篮球系列网上商城设计与实现(源码+部署说明+演示视频+源码介绍).zip

    通过这个项目,学生将学习到如何使用SSM框架搭建Web应用,以及如何实现数据库操作、业务逻辑处理等功能。源码部分包含了完整的项目结构,包括前端页面、后端控制器、实体类、DAO层接口、Service层接口及实现类、...

    ASP.NET设计模式-杨明军译(源码)

    第4章 业务逻辑层:组织 4.1 理解业务组织模式 4.1.1 Transaction Script 4.1.2 Active Record 4.1.3 Domain Model 4.1.4 Anemic Domain Model 4.1.5 领域驱动设计 4.2 小结 第5章 业务逻辑层:模式 5.1 ...

    基于J2EE框架的个人博客系统项目毕业设计论文(源码和论文)

    在数据库处理方面,不需要在数据层借助存储过程及数据库服务器端函数封装过多的业务逻辑,因此数据库系统采用相对精巧的MySQL[6]。 该在线博客系统服务器端如果需要布置到其他主机上,则该主机必备条件如下: 1. ...

    亿级流量电商详情页系统实战-缓存架构+高可用服务架构+微服务架构

    12、大型电商网站的前端页面的核心业务逻辑:完整讲解了大型电商网站的前端页面如何与后端整套系统配合的业务逻辑,包括了动态渲染系统直接渲染首屏的商品基本信息,滚屏时Ajax异步加载分段存储的商品介绍,Ajax异步...

    微信公众平台应用开发:方法、技巧与案例.(机械工业.柳峰)

     5.6.6 实现业务逻辑 123  5.6.7 部署应用及测试体验 125  5.7 小结 126 第6章 高级接口的使用 127  6.1 语音识别 127  6.1.1 开启语音识别功能 128  6.1.2 如何获取语音消息 128  6.2 客服接口 129...

    JAVA程序开发大全---上半部分

    20.7.2 定义对宠物商品信息进行数据库操作的业务逻辑类ThingsBuy 370 20.8 显示宠物新闻模块的实现 373 20.8.1 对应宠物新闻的实体类News 374 20.8.2 定义对宠物新闻进行数据库操作的业务逻辑类NewsBuy 374 20.8.3 ...

    亮剑.NET深入体验与实战精要2

    14.3.5 业务逻辑层的实现 509 14.3.6 表示层的实现 510 14.4 实现基于工厂模式的三层架构 512 14.4.1 扩展新增数据访问层 512 14.4.2 IDAL抽象接口的实现 513 14.4.3 创建DAL对象的封装 517 14.4.4 实现抽象工厂模式...

    亮剑.NET深入体验与实战精要3

    14.3.5 业务逻辑层的实现 509 14.3.6 表示层的实现 510 14.4 实现基于工厂模式的三层架构 512 14.4.1 扩展新增数据访问层 512 14.4.2 IDAL抽象接口的实现 513 14.4.3 创建DAL对象的封装 517 14.4.4 实现抽象工厂模式...

    云计算第二版

    6.6.2 HDFS 基准测试 219 6.7 HBase安装使用 219 6.7.1 HBase的安装配置 219 6.7.2 HBase的执行 220 6.7.3 Hbase编程实例 221 6.8 MapReduce编程 223 6.8.1 矩阵相乘算法设计 223 6.8.2 编程实现 224 习题 226 参考...

    asp.net知识库

    翻译MSDN文章 —— 泛型FAQ:最佳实践 Visual C# 3.0 新特性概览 C# 2.0会给我们带来什么 泛型技巧系列:如何提供类型参数之间的转换 C#2.0 - Object Pool 简单实现 Attributes in C# 手痒痒,也来个c# 2.0 object ...

    手机行业常用知识(普及)

    在接收和处理完SACCH中的定时提前量信息后,用户能够发送正常的、话音业务所要的求的是突发序列消息。当PSTN从拨号端连接到MSC,且MSC将话音路径接入服务基站时,SDCCH检查用户的合法及有效性,随后在手机和基站之间...

Global site tag (gtag.js) - Google Analytics