Example code for testing the filesystem in Golang
go, unit-testing
Solution
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)
}
}
Problem
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) } ```