Question

I have had a hell of a time trying to get a mocking framework up and running that can test my SFTP Service. I was familiar with EasyMock, PowerMock and JMockit but ended up going with GMock. test ('org.gmock:gmock:0.8.2') { excludes 'junit' }

Now that I have the successful happy-path test running I am writing my retry logic and then my failure scenarios. I am now running into two problems. I can't seem to find solutions on these as almost everything for Grails and GMock are sparsely documented.

Method under test: I am using this blog's SFTP with JCraft's JSch example and have expanded it slightly to fit my needs. I take in the credentials for connecting and the file name. I create a FileOutputStream and then connect to the SFTP server. If I get an exception then I will retry it nth times (simplified here for SO purposes).

/**
 * Transfers the file from the remote input server to the local output server.
 *
 * @param fileName
 *      - the file name
 * @param inputFtpCredential
 *      - the input server
 * @param outputFtpCredential
 *      - the output server
 * @param mode
 *      - the mode for the transfer (defaults to {@link ChannelSftp#OVERWRITE}
 * @throws SftpException if any IO exception occurs. Anything other than
 *      {@link ChannelSftp#SSH_FX_NO_SUCH_FILE SSH_FX_NO_SUCH_FILE} or {@link
 *      ChannelSftp#SSH_FX_PERMISSION_DENIED SSH_FX_PERMISSION_DENIED} may cause a retry
 */
public void transferRemoteToLocal(String fileName, FtpCredential inputFtpCredential, FtpCredential outputFtpCredential, Integer mode = ChannelSftp.OVERWRITE) {
    for (retryCounter in 0 .. maxRetries) {
        FileOutputStream output
        try {
            File file = new File(outputFtpCredential.remoteBaseDir, fileName);
            // set stream to append if the mode is RESUME
            output = new FileOutputStream(file, (mode == ChannelSftp.RESUME));

            /*
             * getting the file length of the existing file. This is only used
             * if the mode is RESUME
             */
            long fileLength = 0
            if (file.exists())
                fileLength = file.length()
            load (output, fileName, inputFtpCredential, mode, fileLength)
            // success
            return
        } catch (exception) {
            // if an exception is thrown then retry a maximum number of times
            if (retryCounter < maxRetries) {
                // let the thread sleep so as to give time for possible self-resets
                log.info "Retry number ${retryCounter+1} of file $fileName transfer after $sleepDuration ms"
                Thread.sleep(sleepDuration)
                mode = ChannelSftp.RESUME
            } else {
                int exceptionID = (exception instanceof SftpException)?(exception as SftpException).id:0
                throw new SftpException(exceptionID, "Max number of file transfer retries ($maxRetries) exceeded on file $fileName", exception)
            }
        } finally {
            if (output != null)
                output.close()
        }
    }
}

def load(OutputStream outputStream, String fileName, FtpCredential ftpCredential, Integer mode, Long fileIndex = 0)
throws SocketException, IOException, SftpException, Exception  {
    connect(ftpCredential) { ChannelSftp sftp ->
        sftp.get(fileName, outputStream, mode, fileIndex)
    }
}

So this works in conjunction with the methods from the blog. I wrote my happy path scenario and got it working with GMock.

public void testSavingRemoteToLocal_Success() throws JSchException {
    // Holders for testing
    String fileToTransfer = 'test_large_file.txt'
    FtpCredential localCredential = new FtpCredential()
    // populate credential
    FtpCredential remoteCredential = new FtpCredential()
    // populate credential

    // Mocks
    File mockFile = mock(File, constructor(localCredential.remoteBaseDir, fileToTransfer))
    mockFile.exists().returns(false)

    FileOutputStream mockFOS = mock(FileOutputStream, constructor(mockFile, false))

    // connection
    JSch mockJSch = mock(JSch, constructor())
    Session mockSession = mock(Session)
    ChannelSftp mockChannel = mock(ChannelSftp)

    mockJSch.getSession(remoteCredential.username, remoteCredential.server, remoteCredential.port).returns(mockSession)
    mockSession.setConfig ("StrictHostKeyChecking", "no")
    mockSession.password.set(remoteCredential.password)
    mockSession.connect().once()
    mockSession.openChannel("sftp").returns(mockChannel)
    mockChannel.connect()
    mockChannel.cd(remoteCredential.remoteBaseDir).once()

    // transfer
    mockChannel.get (fileToTransfer, mockFOS, ChannelSftp.OVERWRITE, 0)

    // finally method mocks
    mockChannel.exit()
    mockSession.disconnect()
    mockFOS.close()

    // Test execution
    play {
        service.transferRemoteToLocal(fileToTransfer, remoteCredential, localCredential)
    }
}

Error 1: I then did a simple copy/paste and didn't change a thing except the test method name and I get the following error:

java.lang.StackOverflowError
at java.lang.ref.SoftReference.get(SoftReference.java:93)
at org.codehaus.groovy.util.ManagedReference.get(ManagedReference.java:41)
at org.codehaus.groovy.util.ManagedConcurrentMap$Entry.isEqual(ManagedConcurrentMap.java:62)
at org.codehaus.groovy.util.AbstractConcurrentMap$Segment.getOrPut(AbstractConcurrentMap.java:91)
at org.codehaus.groovy.util.AbstractConcurrentMap.getOrPut(AbstractConcurrentMap.java:35)
at org.codehaus.groovy.reflection.ClassInfo.getClassInfo(ClassInfo.java:103)
at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.getMetaClass(MetaClassRegistryImpl.java:227)
at org.codehaus.groovy.runtime.InvokerHelper.getMetaClass(InvokerHelper.java:751)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallStaticSite(CallSiteArray.java:59)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallSite(CallSiteArray.java:146)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
at org.codehaus.groovy.runtime.callsite.StaticMetaClassSite.call(StaticMetaClassSite.java:55)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
at org.codehaus.groovy.runtime.callsite.StaticMetaClassSite.call(StaticMetaClassSite.java:55)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
at org.codehaus.groovy.runtime.callsite.StaticMetaClassSite.call(StaticMetaClassSite.java:55)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)

and this goes on for a while.

Error 2: I then decide to comment out happy-path and do the retry scenario. So I try using .times(2) everywhere and it didn't like the .times(2) on the constructor. If I don't do that, then it complains because the constructor is called twice since a retry closes out everything and then re-instantiates it when retrying.

I then tried creating two mocks of everything up to the failure, and that throws some sort of NPE during the construction of the second FileOutputStream mock. It seems to be doing a compare on the File.

public void testSavingRemoteToLocal_RetryOnce() throws JSchException {
    // Holders for testing
    String fileToTransfer = 'test_large_file_desktop.txt'
    FtpCredential localCredential = new FtpCredential()
    // populate credential
    FtpCredential remoteCredential = new FtpCredential()
    // populate credential

    // Mocks
    // First loop that fails
    File mockFile2 = mock(File, constructor(inputCredential.remoteBaseDir, fileToTransfer))
    mockFile2.exists().returns(false)

    FileOutputStream mockFIO2 = mock(FileOutputStream, constructor(mockFile2, false))

    // connection
    JSch mockJSch2 = mock(JSch, constructor())
    Session mockSession2 = mock(Session)

    mockJSch2.getSession(outputCredential.username, outputCredential.server, outputCredential.port).returns(mockSession2)
    mockSession2.setConfig ("StrictHostKeyChecking", "no")
    mockSession2.password.set(outputCredential.password)
    mockSession2.connect().raises(new SftpException(0, "throw an exception to retry"))
    mockSession2.disconnect()
    mockFIO2.close()

    // second loop that passes
    File mockFile = mock(File, constructor(inputCredential.remoteBaseDir, fileToTransfer))
    mockFile.exists().returns(false)

    FileOutputStream mockFIO = mock(FileOutputStream, constructor(mockFile, true)) // <-- Fails here with a NPE in mockFile.compareTo

    // connection
    JSch mockJSch = mock(JSch, constructor())
    Session mockSession = mock(Session)
    ChannelSftp mockChannel = mock(ChannelSftp)

    mockJSch.getSession(outputCredential.username, outputCredential.server, outputCredential.port).returns(mockSession)
    mockSession.setConfig ("StrictHostKeyChecking", "no")
    mockSession.password.set(outputCredential.password)
    mockSession.connect()
    mockSession.openChannel("sftp").returns(mockChannel)
    mockChannel.connect()
    mockChannel.cd(outputCredential.remoteBaseDir)

    // transfer
    mockChannel.get (fileToTransfer, mockFIO, FtpMonitor.getInstance(assetId), ChannelSftp.RESUME, 0)

    // finally method mocks
    mockChannel.exit()
    mockSession.disconnect()
    mockFIO.close()

    // Test execution
    play {
        service.sleepDuration = 200
        service.sftpCopyFrom(outputCredential, inputCredential, fileToTransfer, assetId )
    }

    // Assert the results
}
Was it helpful?

Solution

Have you tried Gmock 0.8.3? I remember that I have fixed some bug related to this.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top