Spring Batch Testing & Mocking Revisited with Spring Boot

Jonny Hackett Development Technologies, Java, Spring, Spring Batch, Spring Boot, Testing 3 Comments

Attention: The following article was published over 5 years ago, and the information provided may be aged or outdated. Please keep that in mind as you read the post.

Several years ago, 2012 to be precise, I wrote an article on an approach to unit testing Spring Batch Jobs. My editors tell me that I still get new readers of the post every day, so it is time to revisit and update the approach to a more modern standard.

The approach used in the original post was purely testing the individual pieces containing any business logic. Back then, we didn’t have some of the mocking capabilities that we have today, so I went with an approach that made sense at the time.

However, there have been a few improvements in the past several years. One of those improvements has been the ability to Mock beans within a Spring Context. That’s where the @MockBean annotation comes to the rescue.

In my previous example, we took a look at how to unit test a piece of the job functionality. You can find that article and the source code here.

In this post, we’ll update the example from purely unit testing the collection of components that cover just the business logic to an approach that provides us the means to do a fully automated integration test of the Spring Batch Job. We’ll keep an eye on illustrating best practices for reusability and leveraging the new mocking strategy that Spring Boot provides.

Code Example

The code example was a pretty simple implementation with the business logic contained within the processor. The unit test associated with that code used the Mockito mocking framework to supply return values that would normally be records returned from the database. The solution allowed a way to provide the inputs of the processing in a defined and controlled manner to achieve a repeatable and testable outcome.

Unfortunately, the biggest disadvantage is that the code in that example isn’t reusable. So how does this look fast-forwarding to today?

These days, we’re now creating our Spring Batch jobs inside Spring Boot apps along with reusable services to perform the business logic that was previously performed within the ItemProcessor and ItemWriter. In turn, there has also been a shift in testing practices to how we perform the automated testing of batch jobs.

Diving Into Our Example Code

First, let’s take a look at how the CustomerInvoiceProcessor code would change. Remember, we’re now moving that business logic that is performed within the ItemProcessor into a reusable service. For this particular example let’s keep it simple and create a Spring @Service annotated class that will perform the business logic. You’ll need an interface, so let’s create that:

public interface CustomerInvoiceService
{
    CustomerInvoice generateCustomerInvoice(Integer customerId, Date fromDate, Date toDate);
}

And now we’ll need to implement that interface, which should perform the same business logic that the CustomerInvoiceProcessor was doing before:

@Service
public class CustomerInvoiceServiceImpl implements CustomerInvoiceService
{
    @Autowired
    private TimeEntryDao timeEntryDao;

    @Autowired
    private EmployeeDao employeeDao;

    @Override
    public CustomerInvoice generateCustomerInvoice(Integer customerId, Date fromDate, Date toDate)
    {
        CustomerInvoice invoice = new CustomerInvoice();
        // this is where all of the magic happens
        return invoice;
    }

}

Now that we’ve implemented our CustomerInvoiceService, which should contain the exact same business logic, we’ll wire that service into the ItemProcessor and call it. The CustomerInvoiceProcessor should now look something like this:

public class CustomerInvoiceProcessor implements
        ItemProcessor<Customer, CustomerInvoice> {

    private StepExecution stepExecution;
 
    @Autowired
    private CustomerInvoiceService;
 
    @SuppressWarnings("unused")
    @BeforeStep
    private void beforeStep(StepExecution stepExecution) {
        this.stepExecution = stepExecution;
    }

    @Override
    public CustomerInvoice process(Customer customer) throws Exception {
        JobParameters jobParams = stepExecution.getJobParameters();
        Date fromDate = jobParams.getDate("fromDate");
        Date toDate = jobParams.getDate("toDate");
 	  CustomerInvoice invoice = this.customerInvoiceService. generateCustomerInvoice(customer.getCustomerId(),fromDate, toDate);
         return invoice;
    }
}

Now that our Spring Batch job is running within a Spring Boot app, we can create a Spring Boot Test that allows us to load and execute the batch job within an ApplicationContext.

However, we’ll need to leverage a new mocking strategy that Spring Boot has provided. That’s where the @MockBean annotation comes in handy.

For example, in the previous article’s unit test, you’ll notice that the TimeEntryDao and EmployeeDao were annotated with the Mockito’s @Mock annotation. In our new Spring Boot Test, we’ll use the @MockBean annotation instead.

Spring Boot’s @MockBean annotation allows us to add mocks to a Spring ApplicationContext. Straight from the MockBean javadocs, any existing single bean of the same type defined in the context will be replaced by the mocked bean. If no existing bean is defined, a new one will be added.

When @MockBean is used on a field, as well as registered in the application context, the mock will be injected into the field. That works perfect for our CustomerInvoiceService we created earlier, which has the TimeEntryDao and EmployeeDao auto-wired into the service. We still want to be able to control and mock any of the input data used within the business logic. Then all we would need to do is use Mockito’s when/thenReturn stubbing functionality to provide all of the input data needed for the service, or any other processing logic.

Here’s an idea of what the SpringBootTest might end up looking like. This isn’t fully implemented but should give you some understanding of how it would be implemented.

@SpringBootTest
public class ProcessCustomerInvoiceJobTest implements InitializingBean
{
    private static final String jobName = "CreateCustomerInvoice-Job";
    private static final LocalDate fromDate = LocalDate.parse("2012-01-01");
    private static final LocalDate toDate = LocalDate.parse("2012-01-31");

    @Autowired
    private JobRegistry jobRegistry;

    @Autowired
    private JobLauncher jobLauncher;

    @MockBean
    private TimeEntryDao timeEntryDao;

    @MockBean
    private EmployeeDao employeeDao;

    private BatchJobLauncher batchJobLauncher;

    @Override
    public void afterPropertiesSet() throws Exception
    {
        this.batchJobLauncher = new BatchJobLauncher(this.jobRegistry, this.jobLauncher);
    }

    private Customer createMockCustomer()
    {
        Customer c = new Customer();
        c.setCustomerId(1);
        c.setCustomerEmail("[email protected]");
        c.setCustomerName("Acme Product Co.");
        return c;
    }

    private Employee createMockEmployee(int i, String firstName, String lastName)
    {
        Employee e = new Employee();
        e.setEmployeeId(i);
        e.setFirstName(firstName);
        e.setLastName(lastName);
        e.setHourlyRate(10.00);
        return e;
    }

    private List<TimeEntry> createMockTimeEntries()
    {
        List<TimeEntry> entries = new ArrayList();
        entries.add(createMockTimeEntry(1, 1, LocalDate.parse("2012-01-03"), 8));
        entries.add(createMockTimeEntry(1, 2, LocalDate.parse("2012-01-05"), 9.5));
        return entries;
    }

    private TimeEntry createMockTimeEntry(int customerId, int employeeId, LocalDate entryDate, double hours)
    {
        TimeEntry t = new TimeEntry();
        t.setCustomerId(customerId);
        t.setEmployeeId(employeeId);
        t.setEntryDate(entryDate);
        t.setHours(hours);
        t.setDescription("Invoice batch processing job and unit tests.");
        return t;
    }

    @Before
    public void setUp() throws Exception
    {
        //stub out the results you want return when the DAO's are called from within the service
        when(this.timeEntryDao.findCustomerTimeBilled(1, fromDate.toDate(), toDate.toDate())).thenReturn(createMockTimeEntries());
        when(this.employeeDao.getById(1)).thenReturn(createMockEmployee(1, "John", "Doe"));
        when(this.employeeDao.getById(2)).thenReturn(createMockEmployee(2, "Jane", "Doe"));
    }

    @Test
    public void verifyJobResults() throws Exception
    {
        //provide the job parameters
        JobParametersBuilder builder = new JobParametersBuilder();
        builder.addDate("fromDate", fromDate.toDate());
        builder.addDate("toDate", toDate.toDate());

        JobExecution jobExection = this.batchJobLauncher.executeBatchJob(jobName, builder);
        //verify job executed successfully
        //verify job results
        //don't forget to clean up any output the job created.
        
    }

}

Wrapping Up

We’ve come a long way from the original article that I wrote roughly 7 years ago. We’ve gone from an approach of purely unit testing the collection of components that cover just the business logic to an approach that provides us the means to do a fully automated integration test of the Spring Batch Job.

We can even take this further, which we have on my current project, and write tests using Serenity BDD. Serenity BDD allows you to write acceptance tests using a more explicit Given/When/Then style of testing that also provides reporting and code coverage capabilities.

Maybe we’ll tackle that subject in a future follow up blog.

Spring Batch Blog Series

Part One: Introducing Spring Batch

Part Two: Getting Started With Spring Batch

Part Three: Generating Large Excel Files Using Spring Batch

Scaling Spring Batch – Step Partitioning

Spring Batch Unit Testing and Mockito

Spring Batch – Replacing XML Job Configuration With JavaConfig

0 0 votes
Article Rating
Subscribe
Notify of
guest

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments