Mock filesystems for better testing

Using a mock filesystem can help reduce test flakiness and speed up tests. A regular filesystem introduces two problems for tests. The first problem is that a shared resource ‘the filesystem’ is now shared between tests that may be running concurrently. The second problem is that this shared resource has state, leading to potential dependencies between tests. By implementing a mock filesystem, we avoid these issues. It creates an isolated environment for each test, eliminating shared resources and state. This results in faster, more reliable tests.

An example of this in Go. Lets say we have two functions, one that writes file contents and one that reads those contents. Both the code and the tests write files directly to the filesystem:

// main.go
package main

import "os"

func WriteLog(filename, message string) {
	f, _ := os.Create(filename)
	defer f.Close()

	f.WriteString(message)
}

func CheckLogMessage(filename, message string) bool {
	f, _ := os.Open(filename)
	defer f.Close()

	buf := make([]byte, len(message))
	_, _ = f.Read(buf)
	return string(buf) == message
}

And a test that looks like this:

// main_test.go
package main

import "testing"

func TestExampleWriteLog(t *testing.T) {
	WriteLog("test.log", "Hello, world!")
	equals := CheckLogMessage("test.log", "Hello, world!")

	if !equals {
		t.Errorf("not equal")
	}
}

Now, let's implement this using a mock filesystem. This will allow us to run the tests without actually writing to and reading from the real filesystem, avoiding the problems discussed above.

In our main.go we use [afero.Fs](https://github.com/spf13/afero), a drop in replacement for a regular filesystem. Now in the read and write functions, instead of interacting directly with the os package, we interact with our interface.

package main

import (
	"github.com/spf13/afero"
)

func WriteLog(fs afero.Fs, filename, message string) {
	f, _ := fs.Create(filename)
	defer f.Close()

	f.WriteString(message)
}

func CheckLogMessage(fs afero.Fs, filename, message string) bool {
	f, _ := fs.Open(filename)
	defer f.Close()

	buf := make([]byte, len(message))
	_, _ = f.Read(buf)
	return string(buf) == message
}

func main() {
	var fs = afero.NewOsFs()

	WriteLog(fs, "test.log", "Hello, world!")
	equals := CheckLogMessage(fs, "test.log", "Hello, world!")
	println(equals)
}

This interface lets us pass in a mock filesystem in our tests:

package main

import (
	"testing"

	"github.com/spf13/afero"
)

func TestExampleWriteLog(t *testing.T) {
	fs := afero.NewMemMapFs()

	WriteLog(fs, "test.log", "Hello, world!")
	equals := CheckLogMessage(fs, "test.log", "Hello, world!")

	if !equals {
		t.Errorf("not equal")
	}
}

This change lets us run our tests without the risk of concurrency issues, disk space issues, or cleanup issues. All of these help reduce flakiness, and make tests faster at the same time!