-->

Unit testing FTP consumer with Apache Camel

2020-07-17 07:57发布

问题:

I have the below route. In unit test, since I doesn't have the FTP server available, I'd like to use camel's test support and send an invalid message to "ftp://hostname/input" and verify that it failed and routed to "ftp://hostname/error".

I gone through the documentation which mainly talks about using the "mock:" endpoint but I am not sure how to use it in this scenario.

public class MyRoute extends RouteBuilder
{
    @Override
    public void configure()
    {
        onException(EdiOrderParsingException.class).handled(true).to("ftp://hostname/error");

        from("ftp://hostname/input")
            .bean(new OrderEdiTocXml())
            .convertBodyTo(String.class)
            .convertBodyTo(Document.class)
            .choice()
            .when(xpath("/cXML/Response/Status/@text='OK'"))
            .to("ftp://hostname/valid").otherwise()
            .to("ftp://hostname/invalid");
    }
}

回答1:

As Ben says you can either setup a FTP server and use the real components. The FTP server can be embedded, or you can setup a FTP server in-house. The latter is more like an integration testing, where you may have a dedicated test environment.

Camel is very flexible in its test kit, and if you want to build an unit test that do not use the real FTP component, then you can replace that before the test. For example in your example you can replace the input endpoint of a route to a direct endpoint to make it easier to send a message to the route. Then you can use an interceptor to intercept the sending to the ftp endpoints, and detour the message.

The advice with part of the test kit offers these capabilities: http://camel.apache.org/advicewith.html. And is also discussed in chapter 6 of the Camel in action book, such as section 6.3, that talks about simulating errors.

In your example you could do something a like

public void testSendError() throws Exception {
    // first advice the route to replace the input, and catch sending to FTP servers
    context.getRouteDefinitions().get(0).adviceWith(context, new AdviceWithRouteBuilder() {
        @Override
        public void configure() throws Exception {
            replaceFromWith("direct:input");

            // intercept valid messages
            interceptSendToEndpoint("ftp://hostname/valid")
                .skipSendToOriginalEndpoint()
                .to("mock:valid");

            // intercept invalid messages
            interceptSendToEndpoint("ftp://hostname/invalid")
                .skipSendToOriginalEndpoint()
                .to("mock:invalid");
        }
    });

     // we must manually start when we are done with all the advice with
    context.start();

    // setup expectations on the mocks
    getMockEndpoint("mock:invalid").expectedMessageCount(1);
    getMockEndpoint("mock:valid").expectedMessageCount(0);

    // send the invalid message to the route
    template.sendBody("direct:input", "Some invalid content here");

    // assert that the test was okay
    assertMockEndpointsSatisfied();
}

From Camel 2.10 onwards we will make the intercept and mock a bit easier when using advice with. As well we are introducing a stub component. http://camel.apache.org/stub



回答2:

Have a look at MockFtPServer!

<dependency>
    <groupId>org.mockftpserver</groupId>
    <artifactId>MockFtpServer</artifactId>
    <version>2.2</version>
    <scope>test</scope>
</dependency>

With this one you can simulate all sorts of behaviors like permission problems, etc:

Example:

fakeFtpServer = new FakeFtpServer();

fakeFtpServer.setServerControlPort(FTPPORT);

FileSystem fileSystem = new UnixFakeFileSystem();
fileSystem.add(new DirectoryEntry(FTPDIRECTORY));
fakeFtpServer.setFileSystem(fileSystem);
fakeFtpServer.addUserAccount(new UserAccount(USERNAME, PASSWORD, FTPDIRECTORY));

...

assertTrue("Expected file to be transferred", fakeFtpServer.getFileSystem().exists(FTPDIRECTORY + "/" + FILENAME)); 


回答3:

take a look at this unit test and those in the same directory...they'll show you how to standup a local FTP server for testing and how to use CamelTestSupport to validate scenarios against it, etc...

example unit test...

https://svn.apache.org/repos/asf/camel/trunk/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/FromFileToFtpTest.java

which extends this test support class...

https://svn.apache.org/repos/asf/camel/trunk/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/FtpsServerTestSupport.java



回答4:

In our project we do not create mock FTP Server to test the route but we use properties that can be replaced by a file Camel Component for the local development and unit testing.

Your code would look like this:

public class MyRoute extends RouteBuilder
{
    @Override
    public void configure()
    {
        onException(EdiOrderParsingException.class)
          .handled(true)
          .to("{{myroute.error}}");

        from("{{myroute.input.endpoint}}")
            .bean(new OrderEdiTocXml())
            .convertBodyTo(String.class)
            .convertBodyTo(Document.class)
            .choice()
              .when(xpath("/cXML/Response/Status/@text='OK'"))
                .to("{{myroute.valid.endpoint}}}")
              .otherwise()
                .to("{{myroute.invalid.endpoint}}");
    }
}

And locally and for system test we use a file endpoint declared in the property file:

myroute.input.endpoint=file:/home/user/myproject/input
myroute.valid.endpoint=file:/home/user/myproject/valid
myroute.invalid.endpoint=file:/home/user/myproject/invalid
myroute.error=file:/home/user/myproject/error

or in a JUnit CamelTestSupport you can use the useOverridePropertiesWithPropertiesComponent method to set the properties you want to overrides.

As an alternative you can also use a "direct" route instead but you can miss some File options that can be tested by the unit test.

And we only test the FTP connection with the real system by setting the properties like this:

myroute.input.endpoint=ftp://hostname/input
myroute.valid.endpoint=ftp://hostname/valid
myroute.invalid.endpoint=ftp://hostname/invalid
myroute.error=ftp://hostname/error

With this you can also have different configuration for e.g production server that will differentiate from the Integration Test Environment.

Example of Properties for Production environment:

myroute.input.endpoint=ftp://hostname-prod/input
myroute.valid.endpoint=ftp://hostname-prod/valid
myroute.invalid.endpoint=ftp://hostname-prod/invalid
myroute.error=ftp://hostname-prod/error

In my opinion it is totally acceptable to use file endpoint to simplify the JUnit code and it will test the route only and not the connection.

Testing the connection is more like an Integration Test and should be executed on the real server connected with the real external system (in your case FTP servers, but can be other endpoints/systems as well).

By using properties you can also configure different URL's per environment (For example: we have 3 testing environments and one production environment, all with different endpoints).