Flying while programming in modern Java and C++

Hi, my name is Ján! In this corner of the web I vent about everyday struggles of being a private pilot and a software engineer.

Back

Testing in Java: Unit testing and Integration testing with Spring

A lot has been already written about Java, Unit Testing and Integration Testing, even Behavioral Testing. There are opinions I did and did not adapt as my own.

This sums up what I consider to be a good practice and how I like to lead my Dev work.

Unit Testing

The primary goal of Unit Testing (UT) is to take the smallest piece of testable software in the application, isolate it from the remainder of the code, and determine whether it behaves exactly as expected. Each unit is tested separately before integrating them into modules to test the interfaces between modules.

When Unit Testing, each test should exercise a single scenario or a single code path. Unit Tests test class by class, method by method, code path by code path. If a tested class or method has a Dependency, it has to be:

  1. stubbed
  2. mocked
  3. faked
  4. dummied
  5. spied

The five just named together form a set of Test Doubles. It is best to look at an example LoginService to be tested:

public class LoginServiceImpl implements LoginService {

  private final UserDao dao;

  public LoginServiceImpl(UserDao dao) {
    this.dao = dao;
  }

  @Override
  public boolean isValidLogin(String login) {
    User user = dao.findByLogin(login); 
    return user != null;
  }
}

One can approach its dependencies (or collaborators) injection during the test in two ways:

  1. use Mocks
    public class LoginServiceTest {
    
      private static final String VALID_LOGIN = "jan";
      private static final String NO_SUCH_LOGIN = "no-such-login";
      
      private LoginService service;
      private UserDao dao;
      
      @Before
      public void setUp() {
        dao = createMock(UserDao.class); // could be just annotated with @Mock
        service = new LoginServiceImpl(dao);
      }
    
      @After
      public void after() {
        // verifies that records expected have been called during the test
        verify(dao);
      }
    
      @Test
      public void testIsValidLogin_Null() {
        // record phase of the mock - what calls are expected to by called and what the result should be
        expect(dao).findByLogin(null).andReturn(null);
        // record phase finished, replay and expect methods to be called
        replay(dao);
        assertFalse(service.isValidLogin(null));
      }
    
      @Test
      public void testIsValidLogin_NoSuchLogin() {
        expect(dao).findByLogin(NO_SUCH_LOGIN).andReturn(null);
        replay(dao);
        assertFalse(service.isValidLogin(NO_SUCH_LOGIN));
      }
    
      @Test
      public void testIsValidLogin_ExistingLogin() {
        expect(dao).findByLogin(VALID_LOGIN).andReturn(new User(VALID_LOGIN));
        replay(dao);
        assertTrue(service.isValidLogin(StubUserDao.VALID_LOGIN));
      }
    }
    
  2. use real implementations as the dependencies if possible or create test Stubs
    public class LoginServiceTest {
    
      private static final String NO_SUCH_LOGIN = "no-such-login";
      private static final String VALID_LOGIN = "jan";
    
      private LoginService service;
      
      @Before
      public void setUp() {
        service = new LoginServiceImpl(new StubUserDao());
      }
    
      @Test
      public void testIsValidLogin_Null() {
        assertFalse(service.isValidLogin(null));
      }
    
      @Test
      public void testIsValidLogin_NoSuchLogin() {
        assertFalse(service.isValidLogin(NO_SUCH_LOGIN));
      }
    
      @Test
      public void testIsValidLogin_ExistingLogin() {
        assertTrue(service.isValidLogin(VALID_LOGIN));
      }
    
      private static final class StubUserDao implements UserDao {
    
        @Override
        public User findByLogin(String login) {
          if(LoginServiceTest.VALID_LOGIN.equals(login)) {
            return new User(login);
          }
          return null;
        }
      }
    }
    

A must read article on this topic is Martin Fowler's Mocks arent Subs article.

Stub Pros & Cons

  • Pros
    • it is a plain Java implementation of an interface you already know and use
    • they are reusable
  • Cons
    • a single Test scenario approach can result in many Stub implementations
    • a change to the interface results in update of your Test Stubs
    • the Stub must implement all the interface methods, even thought you are not using them
    • refactoring of a Stub may break multiple tests when you reuse your Stubs
    • sometimes they are not reusable
    • they are reusable to an extent, when you do not have to create Test specific code paths inside the Stub itself

Mock Pros & Cons

  • Pros
    • no need to maintain Test specific interface implementations in your code
    • you only "implement" what you need to record for a specific test
    • mocks verify Test behavior and hold a state per Test
  • Cons
    • even though it is a simple Java code, it does not look simple on a first look
    • mocks have some learning curve, although very small

Mocking or Stubbing?

I am inclined to call myself a Mocker. Not that Practical or Classic approach to Unit Testing should be completely avoided, I prefer Mocking to Stubbing and here is why:

  1. If your dependency graph is deeper than a single level, you need to take care of wiring multiple levels of your dependencies for each test. Since Unit Tests must be isolated from the environment including your DI container such as Spring, you need to do this manually. DI is not what you should be focused on when Unit Testing.
  2. Real implementations cannot be always used, they may use external resources, call other systems, use deployment environment specific resources, etc. To isolate your UT, you need to create a Test implementation of your Dependency or a Stub. This is often trivial, but isn't a Mock such an implementation of a Dependency? Instead of writing and maintaining additional Stub implementations, use Mocks to record what your system should do. Expected behavior of the Dependency is then clear from the Test itself.
  3. System Under Test (SUT) is then clearly defined whereas with the Classic approach the UT is, in fact, testing its dependencies as well.
  4. Mocks usually fail with a descriptive message when their expectations have not been fulfilled during the Test. It helps investigating the source of a Test failure. Stub Implementations have to handle such situations manually.

Conclusion? In real life applications testing, you will probably end up using both Mocks and Stubs. Given reasons above, pick Mocks before Stubs.

Important! You do not need any Dependency Injection (DI) container such as Pico, Guice or Spring to write Unit Tests.

Test Driven Development (TDD)

Thanks to this technique, there will be no such thing as "I have no time to write Unit Tests". If you do not write UT, you will try to print debugging messages or do some similar hacking around anyway.

Very good reading about TDD can be found at MSDN. In short, TDD is about constantly iterating between the UT and actual Production code in incremental steps:

  1. write a UT and see it fail
  2. write the actual code and make the test pass
  3. if the code is unfinished, goto 1.

This way your code becomes naturally well Unit Tested with excellent coverage, because you write a UT for the given code path first.

Integration Testing

When writing an Integration Test (IT), you wire up multiple dependent units and test them in the context of their surrounding infrastructure. It is essential to distinguish between:

  1. connected IT
    • uses real resources and services
    • often created while integrating with databases or APIs
    • connected IT usually not a part of your standard build test suite, as you cannot predict the availability of the dependent resource, its timely response or even stability of data returned
  2. isolated (disconnected) IT
    • similarly to Unit Tests, IT uses Mocks, Stubs and Dummies
    • Test Double’s goal is to replace the dependent resource with a stable, predictable and repeatable result
    • isolated IT should be a part of your build test suite

Sometimes it is a bit challenging to identify a SUT when writing an IT. If you integration test a single Service or Adapter in your code base, than the SUT is your ServiceIT or AdapterIT. When you integration test your application via its public endpoints (e.g. an API by calling its operations or a Web Application by interacting with it), the SUT may be the tested endpoint itself or more likely the whole application.

Integration Testing a Spring application

Spring comes with a whole module to support IT called spring-test. Contains JUnit and TestNG support classes.

IT with JUnit and Spring

  • SpringJUnit4ClassRunner class for JUnit to use with its @RunWith annotations
  • @ContextConfiguration annotation to specify Spring @Configuration annotated classes

Let the application use ApplicationConfiguration class to define live DataSource @Bean configuration:

@Configuration
public class ApplicationConfiguration {

  @Bean
  public DataSource dataSource() {
    BoneCPDataSource ds = new BoneCPDataSource();
    ds.setDriverClass("com.mysql.jdbc.Driver");
    ds.setJdbcUrl("jdbc:mysql://localhost:3306/batman");
    ds.setUsername("Bruce");
    ds.setPassword("Wayne");

    return ds;
  }
}

Its @Configuration override for IT purposes redefines the dataSource @Bean, it may also populate the database with fresh data for each @Test or Test Suite:

@Configuration
// @Import the Live configuration, there may be more than just the DataSource
@Import(ApplicationConfiguration.class)
public class ITApplicationConfiguration {

  @Bean
  public DataSource dataSource() {
    BoneCPDataSource ds = new BoneCPDataSource();
    ds.setDriverClass("org.hsqldb.jdbc.JDBCDriver");
    ds.setJdbcUrl("jdbc:hsqldb:mem:test");
    ds.setUsername("sa");
    ds.setPassword("");

    return ds;
  }
}

Avoid using different @Configuration IT classes for different ITs. Spring caches Test Contexts and by using a single Test @Configuration, you avoid multiple context boots during your IT phase.

IT for the LoginService class becomes:

@ContextConfiguration(classes = ITApplicationConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class LoginServiceIT {

  private static final String NO_SUCH_LOGIN = "no-such-login";
  private static final String VALID_LOGIN = "jan";

  @Inject
  private LoginService service;

  @Test
  public void testExists_Null() {
    assertFalse(service.isValidLogin(null));
  }

  @Test
  public void testExists_NoSuchLogin() {
    assertFalse(service.isValidLogin(NO_SUCH_LOGIN));
  }

  @Test
  public void testIsValidLogin_ExistingLogin() {
    assertTrue(service.isValidLogin(VALID_LOGIN));
  }
}

IT with TestNG and Spring

Writing IT for TestNG is a bit more clunkier. There is no such thing as @RunWith support inside the framework. Thus Spring has to fallback to the infamous IT inheritance pattern (criticized by many for example Three Reasons Why We Should Not Use Inheritance In Our Tests or The Benefits of Testable Code) with its AbstractTestNGSpringContextTests:

Although with TestNG you limit your IT suite due to the given AbstractTestNGSpringContextTests class, you may still consider it because of its @DataProviders.

Sources

Contact

Tell us!
Menu

Items marked with * are required