Search code examples
unit-testinggroovyfile-iospock

How do I test test these file i/o methods using Mock()? Using groovy & spock


I'm having trouble reading other Stack Overflow posts so after a few hours I'm looking for help.

I have two methods that I want to test. And I'd like to test the second one using Mock, but having trouble figuring out what to do.

Here's the first method:

String readFileContents(Path filePath) {
        StringBuilder fileContents = new StringBuilder()
        BufferedReader br = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)
        String line
        while ((line = br.readLine()) != null) {
            fileContents.append(line).append('\n')
        }
        fileContents
    }

And I test it with

class CdmFileSpec extends Specification {

    private CdmFile cdmFile
    private static final String filePath = 'src/test/resources/cdm/test/cdmFileTestFile.txt'

    void setup() {
        cdmFile = new CdmFile()
    }

    void 'test noFileExists'() {
        given:
        Path notRealPath = Paths.get('src/test/resources/cdm//test/notreal.txt')

        when:
        String fileContents = cdmFile.readFileContents(notRealPath)

        then:
        thrown NoSuchFileException
    }

    void 'test readFileContents() reads file contents'() {
        given:
        Path testFilePath = Paths.get(filePath)

        when:
        String fileContents = cdmFile.readFileContents(testFilePath)

        then:
        fileContents.contains('hip hop horrayy\n\nhoooo\n\nheyyy\n\nhoooo')
    }
}

This works as I've placed a real file in the filePath.

I'm wondering... how can I test the next method using Mock?

void eachLineInFileAsString(Path filePath,
                                @ClosureParams(value = SimpleType, options = ['java.lang.String'] )Closure applyLine) {
        BufferedReader br = Files.newBufferedReader(filePath)
        String line
        while ((line = br.readLine()) != null) {
            applyLine.call(line)
        }
    }

Solution

  • The problem with mocking in so many cases is that methods create their own dependencies instead of having them injected or calling a mockable service method creating them. I suggest you refactor your code just a little bit, extracting BufferedReader creation into a service method:

    package de.scrum_master.stackoverflow.q56772468
    
    import groovy.transform.stc.ClosureParams
    import groovy.transform.stc.SimpleType
    
    import java.nio.charset.StandardCharsets
    import java.nio.file.Files
    import java.nio.file.Path
    
    class CdmFile {
      String readFileContents(Path filePath) {
        StringBuilder fileContents = new StringBuilder()
        BufferedReader br = createBufferedReader(filePath)
        String line
        while ((line = br.readLine()) != null) {
          fileContents.append(line).append('\n')
        }
        fileContents
      }
    
      void eachLineInFileAsString(
        Path filePath,
        @ClosureParams(value = SimpleType, options = ['java.lang.String']) Closure applyLine
      ) {
        BufferedReader br = createBufferedReader(filePath)
        String line
        while ((line = br.readLine()) != null) {
          applyLine.call(line)
        }
      }
    
      protected BufferedReader createBufferedReader(Path filePath) {
        Files.newBufferedReader(filePath, StandardCharsets.UTF_8)
      }
    }
    

    Now mocking is quite simple and you don't even need your test resource file anymore (only if you want to do an integration test without mocks):

    package de.scrum_master.stackoverflow.q56772468
    
    
    import spock.lang.Specification
    
    import java.nio.charset.StandardCharsets
    import java.nio.file.NoSuchFileException
    import java.nio.file.Path
    import java.nio.file.Paths
    
    class CmdFileTest extends Specification {
      private static final String filePath = 'mock/cdmTestFile.txt'
      private static final String fileContent = """
        I heard, that you're settled down
        That you found a girl and you're, married now
        I heard, that your dreams came true
        I guess she gave you things
        I didn't give to you
      """.stripIndent()
    
      private CdmFile cdmFile
    
      void setup() {
        cdmFile = Spy() {
          createBufferedReader(Paths.get(filePath)) >> {
            new BufferedReader(
              new InputStreamReader(
                new ByteArrayInputStream(
                  fileContent.getBytes(StandardCharsets.UTF_8)
                )
              )
            )
          }
        }
      }
    
      def "non-existent file leads to exception"() {
        given:
        Path notRealPath = Paths.get('notreal.txt')
    
        when:
        cdmFile.readFileContents(notRealPath)
    
        then:
        thrown NoSuchFileException
      }
    
      def "read file contents into a string"() {
        given:
        Path testFilePath = Paths.get(filePath)
    
        when:
        String fileContents = cdmFile.readFileContents(testFilePath)
    
        then:
        fileContents.contains("your dreams came true\nI guess")
      }
    
      def "handle file content line by line"() {
        given:
        def result = []
        def closure = { line -> result << line }
        Path testFilePath = Paths.get(filePath)
    
        when:
        cdmFile.eachLineInFileAsString(testFilePath, closure)
    
        then:
        result == fileContent.split("\n")
      }
    }
    

    Please note that I am using a Spy() here, i.e. leaving the original CdmFile object intact and just stubbing the service method createBufferedReader(..) when called with exactly parameter Paths.get(filePath). For other paths the original method is called, which is important for the non-existent file test or if you want to add tests involving real resource file loading like in your own example.

    Whenever it is difficult to test a class or component, difficult to inject mocks or otherwise isolate the subject under test, that is a reason to refactor your application code for better testability. When done right also it should also result in better separation of concerns and better componentisation. If your tests become very sophisticated, contrived, brittle and hard to understand and maintain, that is usually a smell and you ought to refactor the application code instead.