How would I mock a call to ioutil.ReadFile in go?

14,990

Solution 1

Don't mock ioutil.ReadFile. Once you mocked it, testing ObtainTranslationStringsFile wouldn't test any more than testing ObtainTranslationStrings would. If you need to test ObtainTranslationStringsFile, create a temporary file and pass its path to ObtainTranslationStringsFile. That way you will actually test the functionality that is added by that function.

Solution 2

There are a couple of ways to handle this if you want to mock this. The first, and perhaps simplest, is to change from using ioutil.ReadFile, and instead call ioutil.ReadAll which takes an io.Reader interface. It's pretty easy to then inject your own io.Reader/filesystem implementation per this method.

If you prefer not to change the method signature, the other option is to not use ioutil, and instead declare a function replacement for ReadFile. Injecting that would be easier with an object method rather than a function, but it's do-able. It just relies on package level variable magic that may seem distasteful to some. See the options explored below:

package main

import (
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    _ "log"
)

type ReadFile func(filename string) ([]byte, error)
// myReadFile is a function variable that can be reassigned to handle mocking in option #2
var myReadFile = ioutil.ReadFile

type FakeReadFiler struct {
    Str string
}

// here's a fake ReadFile method that matches the signature of ioutil.ReadFile
func (f FakeReadFiler) ReadFile(filename string) ([]byte, error) {
    buf := bytes.NewBufferString(f.Str)
    return ioutil.ReadAll(buf)
}

func main() {
    payload := "PAYLOAD"
    path := "/dev/nul"
    buf := bytes.NewBufferString(payload)

    // option 1 is more elegant, but you have to change the method signature to take an io.Reader
    result1, err := ObtainTranslationStringsFileChoice1(buf)
    fmt.Printf("ObtainTranslationStringsFileChoice1 == %#v, %#v\n", result1, err)

    // option 2 keeps the method signature, but allows you to reassign a the myReadFile variable to change the method behavior for testing
    fake := FakeReadFiler{Str: payload}
    myReadFile = fake.ReadFile
    result2, err := ObtainTranslationStringsFileChoice2(path)
    fmt.Printf("ObtainTranslationStringsFileChoice2 == %#v, %#v\n", result2, err)
}

func ObtainTranslationStringsFileChoice1(rdr io.Reader) ([]string, error) {
    if contents, err := ioutil.ReadAll(rdr); err == nil {
        return []string{string(contents)}, nil
    } else {
        return nil, err
    }
}

func ObtainTranslationStringsFileChoice2(path string) ([]string, error) {
    if contents, err := myReadFile(path); err == nil {
        return []string{string(contents)}, nil
    } else {
        return nil, err
    }
}

Playground versions here: sandbox.

If you want to get more sophisticated, I recommend making a full on file system mock. It's what I typically do, and not as difficult as it sounds: https://talks.golang.org/2012/10things.slide#8. With your example, you weren't using structs and interfaces, which really makes this sort of mocking much more robust.

Solution 3

In case you need to mock an io.Reader here is a solution using https://github.com/stretchr/testify

In your package declare

type ioReader interface {
    io.Reader
}

This is only needed to tel mockery that you need such interface an it will generate the corresponding mock.

Then generate mocks

go get github.com/vektra/mockery/.../
mockery -inpkg -all

Then in your test code you can do that

str := "some string"
r := &mockIoReader{}
r.
    On("Read", mock.Anything).
    Run(func(args mock.Arguments) {
        bytes := args[0].([]byte)
        copy(bytes[:], str)
    }).
    Return(len(str), nil)
Share:
14,990
MarkD
Author by

MarkD

Updated on July 24, 2022

Comments

  • MarkD
    MarkD almost 2 years

    I have the following function:

    func ObtainTranslationStringsFile(path string) ([]string, error) {
        if contents, err := ioutil.ReadFile(path); err != nil {
            return ObtainTranslationStrings(string(contents))
        } else {
            return nil, err
        }
    }
    

    I need to mock ioutil.ReadFile, but I'm not sure how to do it. Is it possible?

  • Dhir Pratap
    Dhir Pratap almost 7 years
    I disagree. A test for ObtainTranslationStringsFile should not try exercise ioutil.ReadFile, it should exercise how path, contents, err, and the return value are handled. Thus ioutil.ReadFile can and should be mocked out. (And exercised only at integration test level)
  • icelemon
    icelemon about 6 years
    One shouldn't need to exercise the "real ioutils.ReadFile" function to do unit test. The unit test for the function ObtainTranslationStringsFile should only testing the logic inside the function, but not the function it's calling. Therefore, if there is way, it's always preferred to mock ioutils.ReadFile to return error, and to not return error.
  • Mark G.
    Mark G. almost 6 years
    “Testing ObtainTranslationStringsFile wouldn't test any more than testing ObtainTranslationStrings would.” Not so. There’s logic in the former that’s not present in the latter — namely, what happens when err == nil vs. err != nil. That logic warrants its own testing. Unit tests for ObtainTranslationStringsFile should not assert/provoke “correct” behavior of/from ioutil.ReadFile. That’s not their responsibility. Their job is to assert correct behavior of ObtainTranslationStringsFile in response to (a mocked) ioutil.ReadFile. Of course, integration tests are another story…