`
方世玉
  • 浏览: 22173 次
  • 性别: 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方法中的公用方法就是后期抽取出来的。
分享到:
评论

相关推荐

    Junit 单元测试完整案例

    4. 测试Action类:通过模拟Service层的依赖,直接测试Action类的execute()方法,确保业务逻辑的正确性。 四、测试实践 在案例中,你将看到如何创建测试类,定义测试方法,设置测试数据,以及如何利用JUnit和Spring...

    JUnitTestSSH-Service层

    2. **Service层**:在三层架构(展示层、业务逻辑层、数据访问层)中,Service层负责处理业务逻辑,与DAO层交互,为Controller层提供数据。它是应用的核心,包含了复杂的业务规则和流程。 3. **Mocking和Stubbing**...

    三层体系结构总结:将业务规则、数据访问、合法性校验等工作放到了中间层进行处理

    在实际应用中,三层架构可能还会涉及到其他组件,如服务层(Service Layer)和持久层(Persistence Layer),它们可能是业务逻辑层和数据访问层的进一步细分。服务层可以提供更抽象的业务操作,而持久层则专注于具体...

    三层架构例子

    - 服务(Service):作为业务逻辑层的核心,实现具体业务功能。 - 实体(Entity):表示数据库中的表或对象,用于数据访问层和业务逻辑层之间的数据交换。 - 数据库访问对象(DAO):位于数据访问层,负责执行数据库...

    SSM整合实现分页以及单元测试代码

    测试主要包括对Service层和Mapper层的测试,确保业务逻辑和数据库操作的正确性。 6. **ssmJarTemplateWithPage**:这个文件名可能是整合后的项目模板或者示例,包含分页功能。通常,它会包含一个基础的SSM项目的...

    service层的单体操作抽象设计

    总之,"service层的单体操作抽象设计"关注的是如何在Java环境中,通过定义接口和基类来构建服务层的结构,使其具有良好的可扩展性和可维护性,同时结合设计模式和最佳实践,实现高效、灵活的业务逻辑处理。

    单元测试实践小结[5]

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

    三层结构的简单使用和构造

    在实际开发中,三层架构的具体实现方式可能会有所不同,例如使用Service层作为业务逻辑层和表现层之间的桥梁,或者采用Repository模式来封装数据访问操作。无论采用何种方式,关键在于保持各层之间的松耦合,以提高...

    基于Openbiz 让PHP实现极致业务逻辑重用 _php_

    2. **业务逻辑重用**:Openbiz通过其独特的服务层和服务对象(Service Object)机制,实现了业务逻辑的高度抽象和重用。开发者可以创建可复用的服务组件,降低代码冗余,提高开发效率。 3. **数据访问对象(DAO)**...

    Laravel开发-lara-service

    在Laravel框架中,服务层(Service Layer)是应用程序架构中的一个重要组成部分,它负责处理业务逻辑,隔离模型、控制器和视图之间的复杂性,确保代码的可维护性和可测试性。"Laravel开发-lara-service"这个项目可能...

    C#三层七层架构小案例

    3. **服务层(Service Layer)**:提供跨层的服务,如身份验证、授权、事务管理等,使得业务逻辑层可以专注于业务处理。 4. **领域模型层(Domain Model Layer)**:封装了业务实体和业务规则,是业务逻辑的核心。 ...

    Asp.Net的经典三层架构实例

    Asp.Net的经典三层架构是一种广泛应用于Web开发的设计模式,它将应用程序分为三个主要部分:表现层(Presentation Layer)、业务逻辑层(Business Logic Layer)和数据访问层(Data Access Layer),以此实现良好的...

    有史以来最简单的三层实例(C#)

    在实际项目中,三层架构还可以扩展到更多的层次,比如添加服务层(Service Layer)来封装业务逻辑,使之更易于作为Web服务或API提供。此外,还可以加入缓存层(Cache Layer)来提高性能,或者异常处理层(Exception ...

    Laravel开发-laravel-service-layer

    4. **测试**:服务层的独立性使得单元测试变得更容易,可以针对服务类编写独立的测试用例,确保业务逻辑的正确性。 5. **可扩展性**:随着项目的发展,服务层可以轻松地添加新的服务,以应对不断变化的需求。 为了...

    winform三层架构

    WinForm三层架构是一种在Windows Forms(WinForm)应用程序中实现软件设计模式的方法,它将应用程序逻辑分解为三个主要部分:表现层(Presentation Layer)、业务逻辑层(Business Logic Layer)和数据访问层(Data ...

    C# 三层架构传值简单学习

    此外,对于大型项目,还可以考虑引入服务层(Service Layer)来进一步分离业务逻辑,提供更灵活的服务。 总的来说,掌握C#的三层架构是提升软件开发能力的关键步骤,它能帮助开发者构建出结构清晰、易于维护的应用...

    jsp三层架构

    在这个层面上,开发者会创建JavaBeans或者使用Spring框架中的Service组件来封装业务逻辑。这些组件不直接与数据库交互,而是调用数据访问层的服务来获取或存储数据。这样做的好处是提高了代码的复用性,同时使得业务...

    DELPHI三层架构设计方案

    - 可测试性:独立的层便于单元测试和集成测试,提高软件质量。 - 扩展性:通过增加新的层或调整现有层,可以轻松适应新需求或技术变化。 学习Delphi三层架构设计时,需要注意以下几点: - 掌握好Delphi的基本语法和...

Global site tag (gtag.js) - Google Analytics