Search code examples
javagroovyjunitsftpspock

Testing an SFTP client with Spock Framework


We actually use JUnit and the great FakeSftpServerRule junit rule to test a custom SFTP client we made. That was working great.

Lastly, we want to get rid of junit in favor of the spock framework because we try to migrate to groovy.

Do you guys know any equivalent of FakeSftpServerRule or any way to "switch" a junit rule into a spock rule equivalent ?

Thank you a lot.


Solution

  • The same author also published Fake SFTP Server Lambda, which is independent of the test framework in contrast to the JUnit 4 rule you use.

    If you want to stick with the old tool, Spock 1.3 can also use JUnit 4 rules, and in Spock 2.x it might also work with the JUnit 4 compatibility layer.


    Update: Here is an example program using the SSHJ library for downloading a file from an SFTP server, so we have a subject under test:

    package de.scrum_master.stackoverflow.q71081881;
    
    import net.schmizz.sshj.SSHClient;
    import net.schmizz.sshj.sftp.SFTPClient;
    import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
    import net.schmizz.sshj.xfer.InMemoryDestFile;
    
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    
    public class SFTPFileDownloader {
      private String host;
      private int port;
      private String user;
      private String password;
    
      public SFTPFileDownloader(String host, int port, String user, String password) {
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = password;
      }
    
      protected static class ByteArrayInMemoryDestFile extends InMemoryDestFile {
        private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    
        @Override
        public ByteArrayOutputStream getOutputStream() {
          return outputStream;
        }
      }
    
      public String getFileContent(String path) throws IOException {
        try (
          SSHClient sshClient = setupSshj();
          SFTPClient sftpClient = sshClient.newSFTPClient()
        )
        {
          ByteArrayInMemoryDestFile inMemoryDestFile = new ByteArrayInMemoryDestFile();
          sftpClient.get(path, inMemoryDestFile);
          return inMemoryDestFile.getOutputStream().toString("UTF-8");
        }
      }
    
      private SSHClient setupSshj() throws IOException {
        SSHClient client = new SSHClient();
        client.addHostKeyVerifier(new PromiscuousVerifier());
        client.connect(host, port);
        client.authPassword(user, password);
        return client;
      }
    }
    

    The following Spock specification uses the Fake SFTP Server Lambda in two ways:

    1. In feature method "use original server class", we use withSftpServer the way it was intended by its author, i.e. configuring it and performing interactions in a single lambda or Groovy closure. The only way to make it a bit more spockish is to assign results to previously defined variables and use them in Spock conditions outside of the closure later. Like Leonard said, the resulting Spock specification code is suboptimal, because of the consumer's greed to immediately execute all interactions and close the server again.

    2. In feature method "use custom server class", we use a custom CloseableFakeSftpServer which shamelessly utilises its parent's private constructors, methods and fields. In a Groovy class, we can do that. But of course, it would be much better for the upstream library to be extended and opened up a little in order to support a more spock-like way to first create and configure the server and later perform interactions and verify results, without being confined to a lambda or closure. I even made the helper class @AutoCloseable and use the Spock @AutoCleanup extension in order to avoid manual closing in a cleanup: block. The helper class also uses Groovy's @Delegate FakeSftpServer in order to expose the delegate's methods in its own interface. This is a workaround for not being able to simply extend the original server class, because not even Groovy can call a private super constructor.

    So this is the helper class for test variant #2:

    package de.scrum_master.stackoverflow.q71081881
    
    import com.github.stefanbirkner.fakesftpserver.lambda.FakeSftpServer
    
    class CloseableFakeSftpServer implements AutoCloseable {
      @Delegate
      private FakeSftpServer fakeSftpServer
      private Closeable closeServer
    
      CloseableFakeSftpServer() {
        fakeSftpServer = new FakeSftpServer(FakeSftpServer.createFileSystem())
        closeServer = fakeSftpServer.start(0)
      }
    
      @Override
      void close() throws Exception {
        fakeSftpServer.fileSystem.close()
        closeServer.close()
      }
    }
    

    And here we finally have the specification with the two alternative feature methods (I like the second one better):

    package de.scrum_master.stackoverflow.q71081881
    
    import spock.lang.AutoCleanup
    import spock.lang.Specification
    
    import static com.github.stefanbirkner.fakesftpserver.lambda.FakeSftpServer.withSftpServer
    import static java.nio.charset.StandardCharsets.UTF_8
    
    class SFTPServerTest extends Specification {
      @AutoCleanup
      def server = new CloseableFakeSftpServer()
      def user = "someone"
      def password = "secret"
    
      def "use original server class"() {
        given:
        def fileContent = null
    
        when:
        withSftpServer { server ->
          server.addUser(user, password)
          server.putFile("/directory/file.txt", "content of file", UTF_8)
          // Interact with the subject under test
          def client = new SFTPFileDownloader("localhost", server.port, user, password)
          fileContent = client.getFileContent("/directory/file.txt")
        }
    
        then:
        fileContent == "content of file"
      }
    
      def "use custom server class"() {
        given: "a preconfigured fake SFTP server"
        server.addUser(user, password)
        server.putFile("/directory/file.txt", "content of file", UTF_8)
    
        and: "an SFTP client under test"
        def client = new SFTPFileDownloader("localhost", server.port, user, password)
    
        expect:
        client.getFileContent("/directory/file.txt") == "content of file"
      }
    
    }
    

    Try it in the Groovy web console.