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

最后修改: 2011年 10月 2日


Table of Contents


1. Overview


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.


2. The Layers


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


public class FooService implements IFooService{

   IFooDAO dao;

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


1.2. The DAL/DAO Layer


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


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.


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:


public class FooServiceUnitTest{

   FooService instance;

   private HibernateTemplate hibernateTemplateMock;

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

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

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

   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.


4. Conclusion


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.