您不能忘记一件重要的事情:如果与文件系统交互的代码通过上述文件系统接口 (filesystem) 使用 fs 全局变量 (或测试代码可以更改的其他一些filesystem 值,例如传递的fs 参数)。
让我们看看这样一个示例函数:
func getSize(name string) (int64, error) {
stat, err := fs.Stat(name)
if err != nil {
return 0, err
}
return stat.Size(), nil
}
这个简单的getSize() 函数返回由其名称指定的文件的大小,如果filesystem.Stat() 失败则返回错误(返回错误)。
现在让我们编写一些完全覆盖getSize() 函数的单元测试。
我们需要什么
我们需要一个模拟版本的filesystem,模拟它实际上不与文件系统交互,但在调用filesystem 的方法时返回合理的数据(在我们的例子中是filesystem.Stat())。为了最简单地模拟filesystem(或任何接口),我们将在mockedFS 中嵌入filesystem,因此我们“继承”了它的所有方法,我们只需要模拟可测试代码实际使用的内容。请注意,调用其他方法会导致运行时恐慌,因为我们不会真正为这个嵌入的filesystem 提供一个合理的、非nil 的值,但为了测试,它是不需要的。
由于filesystem 返回值os.FileInfo(除了错误),这是一个接口(并且它的实现不是从os 包中导出的),我们还需要模拟os.FileInfo。这将是mockedFileInfo,我们将非常类似于模拟filesystem:我们将嵌入接口类型os.FileInfo,所以实际上我们只需要实现FileInfo.Size(),因为这是唯一的方法由可测试的getSize() 函数调用。
准备/设置模拟文件系统
一旦我们有了模拟类型,我们就必须设置它们。由于getSize() 使用全局fs 变量与文件系统进行交互,我们需要将mockedFS 的值分配给这个全局fs 变量。在这样做之前,建议保存它的旧值,并在我们完成测试后正确恢复旧值:“cleanup”。
由于我们完全想测试getSize()(包括错误情况),我们为mockedFS 提供了控制它是否应该返回错误的能力,以及告诉它返回什么以防万一的能力。不希望出现任何错误。
在进行测试时,我们可以操纵mockedFS 的“状态”,使其行为符合我们的需要。
以及测试代码
废话不多说,完整的测试代码:
type mockedFS struct {
// Embed so we only need to "override" what is used by testable functions
osFS
reportErr bool // Tells if this mocked FS should return error in our tests
reportSize int64 // Tells what size should Stat() report in our test
}
type mockedFileInfo struct {
// Embed this so we only need to add methods used by testable functions
os.FileInfo
size int64
}
func (m mockedFileInfo) Size() int64 { return m.size }
func (m mockedFS) Stat(name string) (os.FileInfo, error) {
if m.reportErr {
return nil, os.ErrNotExist
}
return mockedFileInfo{size: m.reportSize}, nil
}
func TestGetSize(t *testing.T) {
oldFs := fs
// Create and "install" mocked fs:
mfs := &mockedFS{}
fs = mfs
// Make sure fs is restored after this test:
defer func() {
fs = oldFs
}()
// Test when filesystem.Stat() reports error:
mfs.reportErr = true
if _, err := getSize("hello.go"); err == nil {
t.Error("Expected error, but err is nil!")
}
// Test when no error and size is returned:
mfs.reportErr = false
mfs.reportSize = 123
if size, err := getSize("hello.go"); err != nil {
t.Errorf("Expected no error, got: %v", err)
} else if size != 123 {
t.Errorf("Expected size %d, got: %d", 123, size)
}
}