James B
James B

Reputation: 452

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)
        }
    }

Upvotes: 0

Views: 1771

Answers (2)

kriegaex
kriegaex

Reputation: 67287

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.

Upvotes: 1

tim_yates
tim_yates

Reputation: 171074

There's no need for a Mock, as you can just use a locally defined Closure:

def "test the method"() {
    given:
    def result = []
    def closure = { line -> result << line }
    Path testFilePath = Paths.get(filePath)

    when:
    eachLineInFileAsString(testFilePath, closure)

    then: // I'm guessing here
    result == [
        'line 1',
        'line 2',
        'line 3',
        'line 4'
    ]
}

Upvotes: 1

Related Questions