Example code for testing the filesystem in Golang

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

Example code for testing the filesystem in Golang

问题

我正在尝试为一个与文件系统交互的函数编写单元测试,并且希望在测试过程中能够模拟文件系统。

下面的代码是作为回答给出的,你可以在测试过程中创建一个文件系统接口,但是我对Go语言还不熟悉,不太清楚如何使用它。

请问有人能够提供一个示例,展示如何在测试中使用这个接口吗?

var fs fileSystem = osFS{}

type fileSystem interface {
    Open(name string) (file, error)
    Stat(name string) (os.FileInfo, error)
}

type file interface {
    io.Closer
    io.Reader
    io.ReaderAt
    io.Seeker
    Stat() (os.FileInfo, error)
}

// osFS implements fileSystem using the local disk.
type osFS struct{}

func (osFS) Open(name string) (file, error)        { return os.Open(name) }
func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) }
英文:

I'm trying to write a unit test for a function that will interact with the filesystem and I'd like to be able to mock the filesystem during testing.

The code below was given as the answer to this question, where you would create a filesystem interface to use during testing, but I'm new to Go and am struggling to figure out how to use it.

Would someone be able to provide an example of how this interface would be used in a test please?

var fs fileSystem = osFS{}

type fileSystem interface {
    Open(name string) (file, error)
    Stat(name string) (os.FileInfo, error)
}

type file interface {
    io.Closer
    io.Reader
    io.ReaderAt
    io.Seeker
    Stat() (os.FileInfo, error)
}

// osFS implements fileSystem using the local disk.
type osFS struct{}

func (osFS) Open(name string) (file, error)        { return os.Open(name) }
func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) }

答案1

得分: 14

你不要翻译代码部分,只需要翻译其他内容。以下是翻译好的内容:

重要的一点是,你只能模拟文件系统,如果与文件系统交互的代码通过上述提供的文件系统接口(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。在这样做之前,建议保存其旧值,并在测试完成后恢复旧值:进行“清理”。

由于我们完全想测试getSize()(包括错误情况),我们用控制它是否应返回错误的能力来保护我们的mockedFS,并且还可以告诉它在我们不想要任何错误的情况下返回什么。

在进行测试时,我们可以操纵mockedFS的“状态”以使其符合我们的需求。

测试代码

废话不多说,以下是完整的测试代码:

type mockedFS struct {
    // 嵌入,这样我们只需要“覆盖”测试函数使用的部分
    osFS

    reportErr  bool  // 告诉这个模拟的FS在我们的测试中是否应返回错误
    reportSize int64 // 告诉Stat()在我们的测试中应该返回什么大小
}

type mockedFileInfo struct {
    // 嵌入这个,这样我们只需要添加测试函数使用的方法
    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
    // 创建并“安装”模拟的fs:
    mfs := &mockedFS{}
    fs = mfs
    // 确保在此测试完成后恢复fs:
    defer func() {
        fs = oldFs
    }()

    // 当filesystem.Stat()报告错误时进行测试:
    mfs.reportErr = true
    if _, err := getSize("hello.go"); err == nil {
        t.Error("Expected error, but err is nil!")
    }

    // 当没有错误并且返回大小时进行测试:
    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)
    }
}
英文:

One important thing you must not forget: you can only mock the file system if the code that interacts with the file system does so via the above presented file system interface (filesystem), using the fs global variable (or some other filesystem value that the test code can change, e.g. a passed fs parameter).

Let's see such an example function:

func getSize(name string) (int64, error) {
	stat, err := fs.Stat(name)
	if err != nil {
		return 0, err
	}
	return stat.Size(), nil
}

This simple getSize() function returns the size of a file specified by its name, returning the error if filesystem.Stat() fails (returns an error).

And now let's write some unit tests that fully cover this getSize() function.

What we will need

We need a mocked version of filesystem, mocked so that it does not actually interact with the filesystem, but returns sensible data when methods of filesystem are called (filesystem.Stat() in our case). To easiest mock filesystem (or any interface), we will embed filesystem in our mockedFS, so we "inherit" all its methods, and we will only need to mock what is actually used by the testable code. Note that calling other methods would result in runtime panic, as we won't really give a sensible, non-nil value to this embedded filesystem, but for the sake of tests it is not needed.

Since filesystem returns a value of os.FileInfo (besides an error), which is an interface (and its implementation is not exported from the os package), we will also need to mock os.FileInfo. This will be mockedFileInfo, and we will do it very similarly to mocking filesystem: we'll embed the interface type os.FileInfo, so actually we'll only need to implement FileInfo.Size(), because that is the only method called by the testable getSize() function.

Preparing / Setting up the mocked filesystem

Once we have the mocked types, we have to set them up. Since getSize() uses the global fs variable to interact with the filesystem, we need to assign a value of our mockedFS to this global fs variable. Before doing so it's recommended to save its old value, and properly restore the old value once we're done with the test: "cleanup".

Since we fully want to test getSize() (including the error case), we armour our mockedFS with the ability to control whether it should return an error, and also the ability to tell it what to return in case we don't want any errors.

When doing the tests, we can manipulate the "state" of the mockedFS to bend its behavior to our needs.

And the test(ing) code

Without further ado, the full testing code:

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

答案2

得分: 7

另一个选项是testing/fstest包:

package main
import "testing/fstest"

func main() {
   m := fstest.MapFS{
      "hello.txt": {
         Data: []byte("hello, world"),
      },
   }
   b, e := m.ReadFile("hello.txt")
   if e != nil {
      panic(e)
   }
   println(string(b) == "hello, world")
}

https://golang.org/pkg/testing/fstest

英文:

Another option is the testing/fstest package:

package main
import "testing/fstest"

func main() {
   m := fstest.MapFS{
      "hello.txt": {
         Data: []byte("hello, world"),
      },
   }
   b, e := m.ReadFile("hello.txt")
   if e != nil {
      panic(e)
   }
   println(string(b) == "hello, world")
}

https://golang.org/pkg/testing/fstest

huangapple
  • 本文由 发表于 2017年5月11日 17:55:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/43912124.html
匿名

发表评论

匿名网友

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

确定