An application running in complete isolation is quite rare these days. Often, they integrate with other services to increase the value they deliver. When dealing with legacy applications, I often see the pattern of file transmission via FTP. Because the correct and stable integration of services can be an important part of business it should be tested properly.
In this blog post, I will show you how you can test the file transmission via FTP with MockFtpServer.
What is MockFtpServer?
MockFtpServer is a project that provides two FTP server implementations, that can be used to test your FTP client code.
The first implementation is the FakeFtpServer
. It is a higher-level abstraction of an FTP server providing features like users, virtual filesystems, and files and folders. It is useful for most testing scenarios and this blog post will focus on it.
The second implementation is the StubFtpServer
. Other than the FakeFtpServer, it provides a low-level-API that allows the configuration of FTP commands.
The Example
Let’s imagine our company has built a custom shopping system a decade ago. While everything is still functioning as intended, the user-facing parts just don’t feel up-to-date. Our Sales Team wants it to be updated. Because we want to reduce the blast radius of our changes, we decided to split the shopping system into two parts. While remaining the order processing as is, we extract the shopping part into a separate deployable artifact with a new tech stack. Because of some infrastructure constraints, we decide to transfer the placed orders as an XML file via FTP to a server, where they get asynchronously processed.

Testing the sending of Data
The first thing we want to test is the successful transmission of order files from the shop to the FTP server.
Because the transmission itself is not the focus of this blog post, I will just summarize it. We’ll make use of the Spring Integration project. We start by adding the
FtpHandler
- a MessagingGateway as described in the documentation. Next, we configure the outbound adapter in our
FtpConfiguration
, as described here. Now we can use the FtpHandler
in our
OrderService
to transfer the generated order XML file to the FTP server.
In our test we will use the MockFtpServer library, to test the successful transmission of the generated file.
@SpringBootTest(properties = {"ftp.port=12021"})
class OrderServiceIT {
private FakeFtpServer fakeFtpServer;
@Autowired
private OrderService orderService;
@BeforeEach
void setup() {
fakeFtpServer = new FakeFtpServer();
fakeFtpServer.setServerControlPort(12021);
fakeFtpServer.addUserAccount(new UserAccount("admin", "admin", "/"));
FileSystem fileSystem = new UnixFakeFileSystem();
fileSystem.add(new DirectoryEntry("/"));
fakeFtpServer.setFileSystem(fileSystem);
fakeFtpServer.start();
}
@AfterEach
void cleanup() {
fakeFtpServer.stop();
}
@Test
void orderFile_isTransmittedSuccessfully() throws IOException {
final var customerId = UUID.fromString("add46359-60b0-44c5-b00b-f22367c0533d");
final var itemId = UUID.fromString("80cea71c-b024-4e84-8c1c-af9580728132");
final var orderId = orderService.place(customerId, itemId);
final var orderXml = fakeFtpServer.getFileSystem().getEntry("/" + orderId + ".xml");
assertNotNull(orderXml);
}
}
- We start the complete application context for our test and set the property
ftp.port
to 12021. This is the port the mocked FTP server will listen to. - In the test setup, we instantiate a
FakeFtpServer
provided by MockFtpServer. We also set its control port, create a user, and a file system and start it. - To minimize the chance of side effects we stop the FakeFtpServer after each test. This way each test will start with a clean filesystem.
- We call the method that creates and transfers the order XML file
- The FakeFtpServer provides an API to access the content of its file system. We use it to check if a file with our expected name exists.
Testing the receiving of Data
Now that we tested the sending of data to an FTP server, we want to test that the application can successfully receive and process the data. Again, we use Spring Integration for communication with the FTP server. We extend our
FtpConfiguration
by adding an ApplicationEventPublishingMessageHandler
and an IntegrationFlow
. The latter polls files from the FTP server, wraps them into custom
OrderReceived
-Events and publishes them. The event is handled by the
OrderListener
which parses the order XML file and converts it into an
Order
. This object is then passed into the
OrderProcessingService
where it is finally persisted in the database. If the order XML file could be processed successfully, it will be deleted from the FTP server.
In our test, we want to make sure that our order gets persisted in the database and that the order XML file is removed from the FTP server.
@SpringBootTest(properties = {"ftp.port=12021"})
class OrderProcessingIT {
private FakeFtpServer fakeFtpServer;
@Autowired
private OrderRepository orderRepository;
// setup and cleanup of FakeFtpServer ommited for brevity
@Test
void orderFile_isProcessedSuccessfully() {
final var orderId = UUID.fromString("221f5cb1-65f0-4688-8d8e-4176bf37423e");
final var customerId = UUID.fromString("c98d9bfa-fd98-4166-8596-9ccc0f7f06b8");
final var itemId = UUID.fromString("caec7bae-b6e8-482c-bc39-c1bb2b9f7b8a");
final var orderFileName = "/" + orderId + ".xml";
final var orderFileContent = """
<order>
<id>%s</id>
<customer-id>%s</customer-id>
<item-id>%s</item-id>
</order>
""".formatted(orderId, customerId, itemId);
fakeFtpServer.getFileSystem().add(new FileEntry(orderFileName, orderFileContent));
await().atMost(Duration.ofSeconds(5))
.until(() -> orderRepository.count() == 1L);
final var processedOrder = orderRepository.findById(orderId);
assertTrue(processedOrder.isPresent());
assertEquals(customerId, processedOrder.get().getCustomerId());
assertEquals(itemId, processedOrder.get().getItemId());
await().atMost(Duration.ofSeconds(5))
.until(() -> fakeFtpServer.getFileSystem().getEntry(orderFileName) == null);
}
}
- We add an order XML file to the filesystem of the FakeFtpServer.
- The added file gets processed asynchronously. With Awaitility we wait until an order is persisted in the database.
- We use the OrderRepository to receive our order from the database and check its content afterward.
- Again, we use Awaitility to wait until the order XML file is deleted from the FakeFtpServer.
Conclusion
If you need to test your application’s integration with an FTP server, the MockFtpServer project got you covered. The higher-level API of the FakeFtpServer is intuitive and lets you set up your test scenarios quite easily. Because everything runs in-memory, you won’t need to configure extra infrastructure and must not fear unavailabilities that would cause flaky tests. On the other hand, in memory solutions also have their downsides. They are not the real thing you will integrate within the production environment. Same as you may want to use a real PostgreSQL database in your integration tests, you may want to use a real FTP server as well. As always it’s a tradeoff you have to make.
Resources
- You can find the code of this blog post in this GitHub repository
- Do you want to learn more about the MockFtpServer project? Have a look at the official project page
- Curious about the Spring Integration project? Have a look at the Spring Integration Reference Guide