Reputation:
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) }
Upvotes: 7
Views: 14561
Reputation: 1
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
Upvotes: 10
Reputation: 417767
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.
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.
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.
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)
}
}
Upvotes: 14