1

I am writing a simple batch that writes a CSV file, and I wanted to use Spring Batch FlatFileItemWriter for that, using Spring Boot 2.3.1.RELEASE.

I want to unit test my writer so that I can confirm it's configured properly.

the code is very simple :

    public class CSVResultWriter implements ItemWriter<Project> {

    private final FlatFileItemWriter writer;

    public CSVResultWriter(String outputResource) {

      writer=new FlatFileItemWriterBuilder<Project>()
          .name("itemWriter")
          .resource(new FileSystemResource(outputResource))
          .lineAggregator(new PassThroughLineAggregator<>())
          .append(true)
          .build();

    }

   @Override
   public void write(List<? extends Project> items) throws Exception {
     writer.write(items);
   }
}

and I am writing a simple unit test without Spring, something like :

    File generatedCsvFile = new File(workingDir.toString() + File.separator + "outputData.csv");

    CSVResultWriter writer = new CSVResultWriter(generatedCsvFile.getAbsolutePath());

    Project sampleProject = Project.builder().name("sampleProject1").build();

    writer.write(List.of(sampleProject));

    assertThat(generatedCsvFile).exists();

But the test fails saying :

org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to

Looking at Spring source code, I don't understand how it's possible to make it work.. When trying to write items, the first thing that Spring does is checking that the writer is initialized :

https://github.com/spring-projects/spring-batch/blob/744d1834fe313204f06c0bcd0eedd472ab4af6be/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java#L237

    @Override
    public void write(List<? extends T> items) throws Exception {
        if (!getOutputState().isInitialized()) {
            throw new WriterNotOpenException("Writer must be open before it can be written to");
        }
...

but the way OutputState is built doesn't give a chance to say it has been initiated :

https://github.com/spring-projects/spring-batch/blob/744d1834fe313204f06c0bcd0eedd472ab4af6be/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java#L363

    // Returns object representing state.
    protected OutputState getOutputState() {
        if (state == null) {
            File file;
            try {
                file = resource.getFile();
            }
            catch (IOException e) {
                throw new ItemStreamException("Could not convert resource to file: [" + resource + "]", e);
            }
            Assert.state(!file.exists() || file.canWrite(), "Resource is not writable: [" + resource + "]");
            state = new OutputState();
            state.setDeleteIfExists(shouldDeleteIfExists);
            state.setAppendAllowed(append);
            state.setEncoding(encoding);
        }
        return state;
    }

--> the initialized flag in OutputState keeps its default value, which is false.

So I am a bit puzzled.. I guess when this is managed by Spring, some magic happens and it works.

Am I missing something obvious, or can't we really test this outside of Spring ?

Vincent F
  • 6,523
  • 7
  • 37
  • 79

1 Answers1

2

The FlatFileItemWriter implements the ItemStream contract, which will be automatically honored when used in a Spring Batch job.

If you want to use the writer outside of Spring, you need to call theses methods (open/update/close) manually. This is mentioned in the Item Stream section of the reference docs:

Clients of an ItemReader that also implement ItemStream should call open before any calls
to read, in order to open any resources such as files or to obtain connections.
A similar restriction applies to an ItemWriter that implements ItemStream
Mahmoud Ben Hassine
  • 28,519
  • 3
  • 32
  • 50
  • Thanks a lot. I had seen the open method, but since there was an expected parameter to it that I didn't know how to provide, I didn't look in details. But it's actually quite easy : I have a test specific implementation : public class TestCSVResultWriter extends CSVResultWriter { public TestCSVResultWriter(String outputResource) { super(outputResource); writer.open(mock(ExecutionContext.class)); } } – Vincent F Aug 26 '20 at 15:06