如何在Go中模拟对`ioutil.ReadFile`的调用?

huangapple go评论86阅读模式
英文:

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

问题

我有以下函数:

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

我需要模拟 ioutil.ReadFile,但我不确定如何做。这可行吗?

英文:

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?

答案1

得分: 20

如果你想进行模拟,有几种处理这个问题的方法。第一种,也许是最简单的方法,是改用ioutil.ReadAll而不是ioutil.ReadFile,它接受一个io.Reader接口。然后很容易根据这种方法注入你自己的io.Reader/文件系统实现。

如果你不想改变方法的签名,另一种选择是不使用ioutil,而是声明一个替代ReadFile的函数。使用对象方法而不是函数来注入会更容易,但是也是可行的。它依赖于一些可能对某些人来说不太好的包级变量魔法。下面是探索的选项:

package main

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

type ReadFile func(filename string) ([]byte, error)
// myReadFile是一个函数变量,可以重新分配以处理选项#2中的模拟
var myReadFile = ioutil.ReadFile

type FakeReadFiler struct {
	Str string
}

// 这是一个假的ReadFile方法,与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)

    // 选项1更优雅,但你必须改变方法的签名以接受io.Reader
	result1, err := ObtainTranslationStringsFileChoice1(buf)
	fmt.Printf("ObtainTranslationStringsFileChoice1 == %#v, %#v\n", result1, err)

    // 选项2保持方法的签名,但允许你重新分配myReadFile变量以更改测试中的方法行为
	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版本:sandbox

如果你想更复杂一些,我建议制作一个完整的文件系统模拟。这是我通常做的,而且并不像听起来那么困难:https://talks.golang.org/2012/10things.slide#8。在你的例子中,你没有使用结构体和接口,这样的模拟会更加健壮。

英文:

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.

答案2

得分: 2

如果你需要模拟一个io.Reader,可以使用https://github.com/stretchr/testify来解决。

在你的包中声明:

type ioReader interface {
    io.Reader
}

这只是告诉mockery你需要这样的接口,它将生成相应的模拟。

然后生成模拟:

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

然后在你的测试代码中可以这样做:

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)

以上是翻译好的内容,请确认是否满意。

英文:

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)

答案3

得分: -5

不要嘲笑ioutil.ReadFile。一旦你对它进行了模拟,测试ObtainTranslationStringsFile将不再比测试ObtainTranslationStrings多做任何事情。如果你需要测试ObtainTranslationStringsFile,可以创建一个临时文件,并将其路径传递给ObtainTranslationStringsFile。这样,你将实际测试该函数添加的功能。

英文:

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.

huangapple
  • 本文由 发表于 2014年1月5日 01:03:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/20923938.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定