Blog-Archiv

Sonntag, 20. Oktober 2019

InjectMocks Annotation to Mock a DAO in a Java Service

This is another Blog about the Mockito Java library. Mocking means simulating, and it is for unit testing.

Test Target

The use case under test is finding a birthday celebration that actually happened. Let's assume such birthdays were stored in a database with their year, but we don't know which was the latest, as celebrations did not happen every year. Thus the business logic is searching back from a given year to find the most recent celebration.

It is this logic that I want to test, and nothing else. I am not interested in testing database access, further I don't want to rely on data stored in a database.

Technically I know that the service uses a DAO, and I know the DAO class so that I can mock it. I also have access to the service implementation class, so that I can construct it. Nevertheless the DAO is a private hidden field in the service. The challenge is to write a unit test, using Mockito, that asserts that the service actually searches backwards for a birthday celebration.

Example Maven Project

Here is the example project's file structure.

The dependencies in Maven pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>fri</groupId>
    <artifactId>mockitoTest3</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.1.0</version>
        </dependency>

    </dependencies>

</project>

Open a terminal window and launch following command to download all dependencies, compile the project and execute the test (Maven must be installed before):

cd dirWherePomXmlIs
mvn package

Java Sources

Following is the service interface. The comment on the findBirthdayCelebration() method describes the business logic. (Mind that methods in interfaces don't need to be declared as public, because everything in a Java interface implicitly is public.)

package fri.mockitoTest.service;

import java.util.Date;

public interface BirthdayCelebrationService
{
    /**
     * @param startYear the year where to start searching backward.
     * @return the most recent year when your birthday celebration actually happened.
     */
    Date findBirthdayCelebration(int startYear);
}

The service implementation contains the business logic that I want to test:

package fri.mockitoTest.service.impl;

import java.util.Date;
import fri.mockitoTest.service.BirthdayCelebrationService;

public class BirthdayCelebrationServiceImpl implements BirthdayCelebrationService
{
    private static final int MAXIMUM_YEARS_BACK = 100;
    
    private BirthdayCelebrationDao dao = new BirthdayCelebrationDao();
    
    public Date findBirthdayCelebration(int startYear) {
        Date found = null;
        int year = startYear;
        for (int count = 0; found == null && count < MAXIMUM_YEARS_BACK; count++)   {
            found = dao.findBirthdayCelebration(year);
            year--;
        }
        return found;
    }
}

Finally here is the DAO, this normally encapsulates persistence access (database):

package fri.mockitoTest.service.impl;

import java.util.Date;

public class BirthdayCelebrationDao
{
    public Date findBirthdayCelebration(int year) {
        // TODO: read from database for given year
        return null;
    }
}

As the DAO will be mocked and this is just a demo example, I did not implement anything here.

The Unit Test

Following is the unit test source code. Explanations see below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package fri.mockitoTest.service;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Calendar;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import fri.mockitoTest.service.BirthdayCelebrationService;
import fri.mockitoTest.service.impl.BirthdayCelebrationDao;
import fri.mockitoTest.service.impl.BirthdayCelebrationServiceImpl;

/**
 * How to mock a DAO in an existing Service, so that we can
 * test service functionality without needing a real DAO.
 */
public class BirthdayCelebrationServiceTest
{
    @Mock
    private BirthdayCelebrationDao dao;
    
    @InjectMocks
    private BirthdayCelebrationService service = new BirthdayCelebrationServiceImpl();
    
    @Before
    public void initMocks() {
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void whenNoBirthdayCelebrationThenSkipToPreviousYear_Success()  {
        // set up test data
        final int LATEST_BIRTHDAY_CELEBRATION_YEAR = 2017;
        final Date expectedBirthdayCelebration = newBirthdayDate(LATEST_BIRTHDAY_CELEBRATION_YEAR);
        final int START_YEAR = LATEST_BIRTHDAY_CELEBRATION_YEAR + 2;
        
        // set up behavior
        for (int year = START_YEAR; year > LATEST_BIRTHDAY_CELEBRATION_YEAR; year--)
            when(dao.findBirthdayCelebration(year)).thenReturn(null);
        when(dao.findBirthdayCelebration(LATEST_BIRTHDAY_CELEBRATION_YEAR)).thenReturn(expectedBirthdayCelebration);
        
        // take action
        final Date birthdayCelebration = service.findBirthdayCelebration(START_YEAR);
        
        // assert result
        assertEquals(expectedBirthdayCelebration, birthdayCelebration);
    }

    private Date newBirthdayDate(int year) {
        final Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.MONTH, Calendar.MAY);
        calendar.set(Calendar.DAY_OF_MONTH, 12);
        calendar.set(Calendar.YEAR, year);
        return calendar.getTime();
    }
}

The static imports from JUnit and Mockito in lines 3 and 4 are for using static methods from these libraries like assertEquals(....) and when(....) without having to always write their class prefix. This makes the code much better readable (in case you know what a static import is:-).

Line 22 and 23 define the DAO mock. When initMocks() on line 29 gets called through the JUnit @Before annotation, Mockito will instantiate a BirthdayCelebrationDao and put it on the private field.

Line 25 and 26 define the mock injection. Line 26 contains a harcoded instantiation of a service object. This must be done when the object is used through an interface, in this case BirthdayCelebrationService. Mockito would not know which concrete class it should use for instatiation. Optionally I could also declare BirthdayCelebrationServiceImpl as field-type and leave out the allocation, Mockito would then allocate the object when processing the @InjectMocks annotation. Thus @InjectMocks is also an implicit @Mock annotation.

After instantiation Mockito initializes the service. For injection it uses all mocks and spys defined so far in this class. This is the way how the mocked DAO from line 23 arrives in the service on line 26.

The test method on line 34 carries the test target in its name. I want to make sure that the service has a search-backwards loop for some year that had a birthday celebration.

Every unit test consists of at least three parts:

  1. Build test data
  2. Carry out the action to test
  3. Assert the action result (to success or failure)

In case we use mocks, we also need to teach them behavior, Mockito calls this "stubbing". We could regard this to be the last part of building test data. Line 36 - 38 defines test data. Line 41 - 43 teaches the DAO mock behavior (stubs the DAO). This is done using the Mockito methods when() and thenReturn(). (We also could remove the for-loop, because Mockito mocks return null by default when nothing was stubbed.)

On line 46 the service gets called. This is the action under test. We ask it to search the most recent birthday celebration back from 2019.

On line 49 we assert that the most recent birthday celebration actually was in 2017, like it was stubbed on line 43. The actual date of the birthday, generated on line 52, does not matter here, just the stub date and the result date must be the same. This proves that the service searched backwards by decrementing the input year.

Mind that this test does not make sure that the service would not go into an endless loop, as it would happen when no MAXIMUM_YEARS_BACK limit was built into BirthdayCelebrationServiceImpl. We would have to write another test method for that!

Conclusion

The @InjectMocks annotation is not easily understandable, but quite useful. We would have to evaluate whether recursive injection works. Its documentation says:

Mockito is not an dependency injection framework, don't expect this shorthand utility to inject a complex graph of objects be it mocks/spies or real objects.



Keine Kommentare: