Avoid brittle tests for the Service layer – 避免对服务层进行脆性测试

最后修改: 2023年 8月 28日

Table of Contents

目录

1. Overview

1.概述

There are many ways to test the Service Layer of an application. The goal of this article is to show one way of unit testing this layer in isolation, by mocking out the interactions with the database entirely.

有很多方法可以测试应用程序的服务层。本文的目标是展示一种单独对该层进行单元测试的方法,即完全模拟出与数据库的交互。

This example will use Spring for the dependency injection, JUnit, Hamcrest and Mockito for testing, but the technologies can vary.

这个例子将使用Spring进行依赖注入、JUnit、HamcrestMockito用于测试,但这些技术可能有所不同。

2. The Layers

2.分层设计

The typical java web application will have a service layer on top of a DAL/DAO layer which in turn will calls the raw persistence layer.

典型的java web应用会在DAL/DAO层之上有一个服务层,而DAL/DAO层又会调用原始持久层。

1.1. The Service Layer

1.1.服务层

@Service
public class FooService implements IFooService{

   @Autowired
   IFooDAO dao;

   @Override
   public Long create( Foo entity ){
      return this.dao.create( entity );
   }

}

1.2. The DAL/DAO Layer

1.2.DAL/DAO层

@Repository
public class FooDAO extends HibernateDaoSupport implements IFooDAO{

   public Long create( Foo entity ){
      Preconditions.checkNotNull( entity );

      return (Long) this.getHibernateTemplate().save( entity );
   }

}

3. Motivation and Blurring the Lines of the Unit Test

3.单元测试的动机和模糊界限

When unit testing a service, the standard unit is usually the service class, simple as that. The test will mock out the layer underneath – in this case the DAO/DAL layer and verify the interactions on it. Exact same thing for the DAO layer – mocking out the interactions with the database (HibernateTemplate in this example) and verifying the interactions with that.

在对服务进行单元测试时,标准的单元通常是服务,就这么简单。测试将模拟出下面的层–在本例中是DAO/DAL层,并验证它的交互。DAO层也是如此–模拟出与数据库(本例中为HibernateTemplate)的交互,并验证与之的交互。

This is a valid approach, but it leads to brittle tests – adding or removing a layer almost always means rewriting the tests entirely. This happens because the tests rely on the exact structure of the layers, and a change to that means a change to the tests.

这是一个有效的方法,但它导致了脆弱的测试 – 增加或删除一个层几乎总是意味着完全重写测试。发生这种情况是因为测试依赖于层的确切结构,而这种变化意味着对测试的改变。

To avoid this kind of inflexibility, we can grow the scope of the unit test by changing the definition of the unit – we can look at a persistent operation as a unit, from the Service Layer through the DAO and all the way day to the raw persistence – whatever that is. Now, the unit test will consume the API of the Service Layer and will have the raw persistence mocked out – in this case, the HibernateTemplate:

为了避免这种不灵活的情况,我们可以通过改变单元的定义来扩大单元测试的范围–我们可以把持久化操作看成一个单元,从服务层到DAO,一直到原始持久化–不管那是什么。现在,单元测试将消耗服务层的API,并将原始持久化模拟出来–在这种情况下,就是HibernateTemplate

public class FooServiceUnitTest{

   FooService instance;

   private HibernateTemplate hibernateTemplateMock;

   @Before
   public void before(){
      this.instance = new FooService();
      this.instance.dao = new FooDAO();
      this.hibernateTemplateMock = mock( HibernateTemplate.class );
      this.instance.dao.setHibernateTemplate( this.hibernateTemplateMock );
   }

   @Test
   public void whenCreateIsTriggered_thenNoException(){
      // When
      this.instance.create( new Foo( "testName" ) );
   }

   @Test( expected = NullPointerException.class )
   public void whenCreateIsTriggeredForNullEntity_thenException(){
      // When
      this.instance.create( null );
   }

   @Test
   public void whenCreateIsTriggered_thenEntityIsCreated(){
      // When
      Foo entity = new Foo( "testName" );
      this.instance.create( entity );

      // Then
      ArgumentCaptor< Foo > argument = ArgumentCaptor.forClass( Foo.class );
      verify( this.hibernateTemplateMock ).save( argument.capture() );
      assertThat( entity, is( argument.getValue() ) );
   }

}

Now the test only focuses on a single responsibility – when creation is triggered, does the creation reach the database?

现在,测试只关注一个责任–当创建被触发时,创建是否到达数据库?

The last test uses Mockito verification syntax to check that the save method has been called on the hibernate template, capturing the argument in the process so that it can be checked as well. The responsibility of creating the entity is verified via this interaction test, without the need to check any state – the test trusts that the hibernate save logic is working as intended. Of course that needs to be tested as well, but that is another responsibility and another type of test.

最后一个测试使用Mockito验证语法来检查save方法是否已经在hibernate模板上被调用,在此过程中捕获参数,以便也能被检查。通过这个交互测试验证了创建实体的责任,而不需要检查任何状态–该测试相信hibernate的保存逻辑是按预期工作。当然,这也需要测试,但这是另一种责任和另一种类型的测试。

4. Conclusion

4.结论

This technique invariably leads to more focused tests, which makes them more resilient and flexible to change. The only reason the test should now fail is because the responsibility under test is broken.

这种技术无一例外地导致了更有针对性的测试,这使得它们对变化更有弹性和灵活性。现在测试失败的唯一原因是被测试的责任被破坏了。