Wednesday, May 22, 2013

Spring Integration File Polling and Tests

I recently implemented a small project where we had to poll a folder for new files and then trigger a service flow on the contents of the file.

Spring Integration is a great fit for this requirement as it comes with a channel adapter that can scan a folder for new files and then take the file through a messaging flow.

The objective of this specific article is to go over how a file poller flow can be tested.

To start with consider a simple flow implemented using the following Spring integration configuration:

<beans:beans xmlns="http://www.springframework.org/schema/integration"
    .....
 <channel id="fileChannel"></channel>
 <channel id="processedFileChannel"></channel>

 <int-file:inbound-channel-adapter directory="${inbound.folder}" channel="fileChannel" filename-pattern="*.*">
  <poller fixed-delay="5000"></poller>
 </int-file:inbound-channel-adapter>
 
 <service-activator input-channel="fileChannel" ref="fileHandlerService" method="handle" output-channel="processedFileChannel">
 </service-activator>
 
 <logging-channel-adapter id="loggingChannelAdapter" channel="processedFileChannel"></logging-channel-adapter>
 
 <beans:bean name="fileHandlerService" class="files.RealFileHandlingService"/>
 <context:property-placeholder properties-ref="localProps" local-override="true"/>
 
 <util:properties id="localProps">
 </util:properties>
</beans:beans>

This is better represented in a diagram:



Simple Testing of the flow

Now to test this flow, my first approach was to put a sample file into a folder in the classpath, dynamically discover and use this folder name during test and to write the final message into a test channel and assert on the messages coming into this channel. This is how the test configuration which composes the original Spring config looks like:

<beans:beans xmlns="http://www.springframework.org/schema/integration"
    ....
 <beans:import resource="si-fileprocessing.xml"/>
 <util:properties id="localProps">
  <beans:prop key="inbound.folder">#{new java.io.File(T(java.io.File).getResource("/samplefolder/samplefile.txt").toURI()).parent}</beans:prop>
 </util:properties>
 <channel id="testChannel">
  <queue/>
 </channel>
 
 <bridge id="loggingChannelAdapter" input-channel="processedFileChannel" output-channel="testChannel"></bridge>
</beans:beans>

and the test looks like this:
@Autowired @Qualifier("testChannel") PollableChannel testChannel;

@Test
public void testService() throws Exception{
 Message<?> message = this.testChannel.receive(5000);
 assertThat((String)message.getPayload(), equalTo("Test Sample file content"));
}

This works neatly, especially note that the path can be entirely provided in the configuration using a Spring-EL expression :
#{new java.io.File(T(java.io.File).getResource("/samplefolder/samplefile.txt").toURI()).parent}

Mocking out the Service

Now, to take it a little further, what if I want to mock the service that is processing the file (files.RealFileHandlingService). There is an issue here which will become clear once the mock is implemented. An approach to injecting in a mock service is to use Mockito and a helper function that it provides to create a mock this way using Spring configuration file:

<beans:bean name="fileHandlerService" factory-method="mock" class="org.mockito.Mockito">
 <beans:constructor-arg value="files.FileHandlingService"></beans:constructor-arg>
</beans:bean>

Once this mock bean is created, the behavior for the mock can be added in the @Before annotated method of junit, this way:

@Before
public void setUp() {
 when(fileHandlingServiceMock.handle((File)(anyObject()))).thenReturn("Completed File Processing");
}

@Test
public void testService() throws Exception{
 Message<?> message = this.testChannel.receive(5000);
 assertThat((String)message.getPayload(), equalTo("Completed File Processing"));
}

If the test is repeated now, surprisingly it will fail and the reason is - Spring application context is fully initialized by the time the call comes to the @Before method of Junit and the poller scanning the file coming into the folder gets triggered BEFORE the mock behavior is added on and so without the correct behavior of the mock service the test fails.

I am sure there are other fixes, but the fix that worked for me was to essentially control which folder is scanned and at what point the file is placed in the folder for the flow to be triggered. This is heavily based on some tests that I have seen in Spring Integration project itself. The trick is to create a temporary folder first using Spring config and set that folder as the polling folder:
<beans:bean id="temp" class="org.junit.rules.TemporaryFolder"
   init-method="create" destroy-method="delete"/>

<beans:bean id="inputDirectory" class="java.io.File">
 <beans:constructor-arg value="#{temp.newFolder('sitest').path}"/>
</beans:bean> 

<util:properties id="localProps">
 <beans:prop key="inbound.folder">#{inputDirectory.getAbsolutePath()}</beans:prop>
</util:properties>

Now place the file in the temporary folder only during the test run, this way @Before annotated method gets a chance to add behavior to the mock and the mocked behavior can be cleanly asserted:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("si-test.xml")
public class FileHandlingFlowTest {
 @Autowired @Qualifier("testChannel") private PollableChannel testChannel;
 @Autowired private FileHandlingService fileHandlingServiceMock;
 @Autowired @Qualifier("inputDirectory") private File inputDirectory;
 
 @Before
 public void setUp() {
  when(fileHandlingServiceMock.handle((File)(anyObject()))).thenReturn("Completed File Processing");
 }
 
 @Test
 public void testService() throws Exception{
  Files.copy(new File(this.getClass().getResource("/samplefolder/samplefile.txt").toURI()), new File(inputDirectory, "samplefile.txt"));
  Message<?> message = this.testChannel.receive(5000);
  assertThat((String)message.getPayload(), equalTo("Completed File Processing"));
 }
}

2 comments:

  1. Hi, nice explanation, but do you have the whole project on github or even a zip file?

    ReplyDelete
    Replies
    1. Sorry Diego, it has been a while and I appear to have lost the project. Should not be difficult to reproduce the steps using the Spring Integration reference material though.

      Delete