Spring Batch Testing: Asserting file equality after running a single step

by Landon | Jan 20, 2021 | Leave a comment

For some time at SoFi, we’ve worked with Spring Batch to provide a third-party integration with a service without a robust API, but that loves to work in terms of batch files.

There are a number of ways to deal with that, and we’ve taken a few different approaches. One of them is to implement a spring batch implementation that parses incoming files from the third party, logs content, and turns file content into a data stream consumable by other applications. Similarly, this little application also will take a data state that we persist and construct flat files that the third-party vendor can consume.

Although implementations of spring batch can vary widely (It is a pretty flexible system, after all), the team had a concern about littering the file system with unnecessary files hundreds of megabytes large at the end of every run. So, in our implementation, part of every “outgoing job,” as it were, deletes the file from the local file system after it’s built and shipped off to the vendor.

It quickly became obvious that testing modifications to our files was going to be a problem with this step involved in our batch jobs. End-to-end testing of a batch job is relatively straightforward. But rather than do end-to-end testing, what we needed was a way to test a single step, which was the step that captures data from our model and turns that into a file.

Here’s what we came up with. (Note: To protect SoFi IP, I’ve sanitized this pretty heavily. But I think that this code is representative of what was actually implemented.)

import static project.constants.GENERATE_PLACEMENT_FILE;

@Slf4j
@SpringBootTest
@ActiveProfiles("test")
public class GeneratePlacementFileStepIntegrationTests {

    private static final String OUTPUT_FILE_LOCATION = "build/test_output/generate_placement_file_step_test_results.txt";
    private static final String ASSERTION_FILE_LOCATION = "src/test/resources/placement/generate_file_step_expected_test_results.txt";

    @TestConfiguration
    static class SingleStepConfig {
        @Bean
        public JobLauncherTestUtils getJobLauncherTestUtils() {
            return new JobLauncherTestUtils() {
                @Override
                @Autowired
                public void setJob(@NotNull Job generateAndUploadPlacementFile) {
                    super.setJob(generateAndUploadPlacementFile);
                }
            };
        }
    }

    @BeforeAll
    static void removeFileIfExists() {
        var file = new File(OUTPUT_FILE_LOCATION);
        if (file.exists()) {
            assertTrue(file.delete());
        }
    }

    /**
     * this test will put data in the database
     * then it will run the step in the batch that builds a file
     * and will compare the built file to a manually built file that matches expected behavior
     */
    @Test
    @SqlGroup({
            @Sql("classpath:boilerplate/baseline-data.sql"),
            @Sql("classpath:batch-job/use-case-data.sql")
    })
    void insertDataAndRunStepAndTestFileEquality() throws Exception {
        assertFalse(new File(OUTPUT_FILE_LOCATION).exists());

        //build the params for this test execution
        final var params = new JobParametersBuilder()
                .addString(JOB_PARAM_OUTPUT_FILE, OUTPUT_FILE_LOCATION)
                .toJobParameters();
        JobExecution je = jobLauncherTestUtils.launchStep(GENERATE_PLACEMENT_FILE, params);
        assertEquals(ExitStatus.COMPLETED, je.getExitStatus());
        
//make sure the file looks like it should
        assertFileEquals(new File(ASSERTION_FILE_LOCATION),
                new File(OUTPUT_FILE_LOCATION));
    }

If you look at the Spring Batch Reference Documentation on end-to-end testing, they have the @SpringBatchTest annotation, and as part of that annotation, a JobLauncherTestUtils bean gets made available for you. This bean can run single steps in a batch job independently of the job as a whole… just what I wanted.

For us, though, going with @SpringBatchTest borked up our H2 data structure and didn’t spin up the necessary JPA beans in order to interact with our data layer. So, instead, we had to go with @SpringBootTest to spin up the full application context. This made all the JPA beans and the entire Hibernate data layer available, but left us without a JobLauncherTestUtils bean to run a single step.

So I made my own. The key here is this little snippet:

    @TestConfiguration
    static class SingleStepConfig {
        @Bean
        public JobLauncherTestUtils getJobLauncherTestUtils() {
            return new JobLauncherTestUtils() {
                @Override
                @Autowired
                public void setJob(@NotNull Job generateAndUploadPlacementFile) {
                    super.setJob(generateAndUploadPlacementFile);
                }
            };
        }
    }

This test configuration made the test utils bean available for this job. I then autowired the bean into the test and used it. Voila! The test ran the sql scripts that inserted my baseline data and use-case-specific data into the H2 db, and then the step that extracted that data and produce a file was run.

The last step is to test file equality.

        assertFileEquals(new File(ASSERTION_FILE_LOCATION),
                new File(OUTPUT_FILE_LOCATION));

The best part about this test is that in order to test future changes to this particular batch step, I don’t have to make changes to the test itself. I just have to make changes to the data state that gets built at the start and changes to the ASSERTION_FILE.

I thought this was a robust solution to our problem. I intend to repeat this testing pattern for other file-based batch jobs in the future.