Golang testing — gotest.tools fs
Published on Sep 14, 2018 by Vincent Demeester.
Introduction
Let’s continue the gotest.tools serie, this time with the fs package.
Package fs provides tools for creating temporary files, and testing the contents and structure of a directory.
This package is heavily using functional arguments (as we saw in functional arguments for
wonderful builders). Functional arguments is, in a nutshell, a combinaison of two Go
features : variadic functions (... operation in a function signature) and the fact
that func are first class citizen. This looks more or less like that.
type Config struct {} func MyFn(ops ...func(*Config)) *Config { c := &Config{} // with default values for _, op := range ops { op(c) } return c } // Calling it conf := MyFn(withFoo, withBar("baz"))
The fs package has too main purpose :
- create folders and files required for testing in a simple manner
- compare two folders structure (and content)
Create folder structures
Sometimes, you need to create folder structures (and files) in tests. Doing i/o work
takes time so try to limit the number of tests that needs to do that, especially in unit
tests. Doing it in tests adds a bit of boilerplate that could be avoid. As stated before :
One of the most important characteristic of a unit test (and any type of test really) is readability. This means it should be easy to read but most importantly it should clearly show the intent of the test. The setup (and cleanup) of the tests should be as small as possible to avoid the noise.
In a test you usually end up using ioutil function to create what you need. This looks
somewhat like the following.
path, err := ioutil.TempDir("", "bar") if err != nil { // or using `assert.Assert` t.Fatal(err) } if err := os.Mkdir(filepath.Join(path, "foo"), os.FileMode(0755)); err != nil { t.Fatal(err) } if err := ioutil.WriteFile(filepath.Join(path, "foo", "bar"), []byte("content"), os.FileMode(0777)); err != nil { t.Fatal(err) } defer os.RemoveAll(path) // to clean up at the end of the test
The fs package intends to help reduce the noise and comes with a bunch function to create
folder structure :
- two main function
NewFileandNewDir - a bunch of operators :
WithFile,WithDir, …
func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir { // … } func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File { // … }
The With* function are all satisfying the PathOp interface, making NewFile and
NewDir extremely composable. Let’s first see how our above example would look like using
the fs package, and then, we’ll look a bit more at the main PathOp function…
dir := fs.NewDir(t, "bar", fs.WithDir("foo", fs.WithFile("bar", fs.WithContent("content"), fs.WithMode(os.FileMode(0777))), )) defer dir.Remove()
It’s clean and simple to read. The intent is well described and there is not that much of
noise. fs functions tends to have sane and safe defaults value (for os.FileMode
for example). Let’s list the main, useful, PathOp provided by gotest.tools/fs.
WithDircreates a sub-directory in the directory at path.WithFilecreates a file in the directory at path with content.WithSymlinkcreates a symlink in the directory which links to target. Target must be a path relative to the directory.WithHardlinkcreates a link in the directory which links to target. Target must be a path relative to the directory.WithContentandWWithByteswrite content to a file at Path (from astringor a[]byteslice).WithModesets the file mode on the directory or file at path.WithTimestampssets the access and modification times of the file system object at path.FromDircopies the directory tree from the source path into the new Dir. This is pretty useful when you have a huge folder structure already present in youtestdatafolder or elsewhere.AsUserchanges ownership of the file system object at Path.
Also, note that PathOp being an function type, you can provide your own implementation
for specific use-cases. Your function just has to satisfy PathOp signature.
type PathOp func(path Path) error
Compare folder structures
Sometimes, the code you’re testing is creating a folder structure, and you would like to
be able to tests that, with the given arguments, it creates the specified structure. fs
allows you to do that too.
The package provides a Equal function, which returns a Comparison, that the assert
package understand. It works by comparing a Manifest type provided by the test and a
Manifest representation of the specified folder.
Equal compares a directory to the expected structured described by a manifest and returns success if they match. If they do not match the failure message will contain all the differences between the directory structure and the expected structure defined by the Manifest.
A Manifest stores the expected structure and properties of files and directories in a
file-system. You can create a Manifest using either the functions Expected or
ManifestFromDir.
We’re going to focus on the Expected function, as ManifestFromDir does pretty much
what you would expected : it takes the specified path, and returns a Manifest that
represent this folder.
func Expected(t assert.TestingT, ops ...PathOp) Manifest
Expected is close to NewDir function : it takes the same PathOp functional
arguments. This makes creating a Manifest straightforward, as it’s working the same. Any
function that satisfy PathOp can be used for Manifest the exact same way you’re using
them on fs.NewDir.
There is a few additional functions that are only useful with Manifest :
MatchAnyFileContentupdates a Manifest so that the file at path may contain any content.MatchAnyFileModeupdates a Manifest so that the resource at path will match any file mode.MatchContentIgnoreCarriageReturnignores cariage return discrepancies.MatchExtraFilesupdates a Manifest to allow a directory to contain unspecified files.
path := operationWhichCreatesFiles() expected := fs.Expected(t, fs.WithFile("one", "", fs.WithBytes(golden.Get(t, "one.golden")), fs.WithMode(0600)), fs.WithDir("data", fs.WithFile("config", "", fs.MatchAnyFileContent)), ) assert.Assert(t, fs.Equal(path, expected))
The following example compares the result of operationWhichCreatesFiles to the expected
Manifest. As you can see it also integrates well with other part of the gotest.tools
library, with the golden package in this example.
Conclusion…
… that’s a wrap. In my opinion, this is one the most useful package provided by
gotest.tools after assert. It allows to create simple or complex folder structure
without the noise that usually comes with it.