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