Golang Mock File System for unit testing by afero

eye-catch Golang

When writing unit tests for file related, we need to create dummy data and put the file into a specific file so that the production code can handle it. Creating a test directory and somehow passing it to the production code is one of the ways for unit tests. However, there are some cases where the test directory is not deleted and thus not initialized for unit tests.

By using afero package, we can avoid such a case and easily write unit tests.

Sponsored links

Create a file to the memory

Let’s see an example of creating a file.

package useafero

import (
    "errors"
    "fmt"
    "io"
    "os"
    "strconv"
    "strings"

    "github.com/spf13/afero"
)

type FileHandler struct {
    FileSystem afero.Fs
}

func (f *FileHandler) Create(path string, content string) error {
    exist, err := afero.Exists(f.FileSystem, path)
    if err != nil {
        return fmt.Errorf("failed to check path existence: %w", err)
    }

    if exist {
        if err = f.FileSystem.Rename(path, path+"_backup"); err != nil {
            return fmt.Errorf("failed to rename a file: %w", err)
        }
    }

    file, err := f.FileSystem.Create(path)
    if err != nil {
        return fmt.Errorf("failed to create a file: %w", err)
    }

    _, err = file.WriteString(content)
    if err != nil {
        return fmt.Errorf("failed to write content: %w", err)
    }

    return nil
}

FileHandler has to require afero.Fs interface so that we can mock the file system in our unit tests.

It requires a file path and the content to be written. Let’s write the following unit tests.

  • A file is created with the content
  • A backup file is created if the specified file already exists
package useafero_test

import (
    "errors"
    "os"
    "play-with-go-lang/test/useafero"

    "testing"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
    "github.com/spf13/afero"
)

func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "afero test suite")
}

var _ = Describe("afero test", func() {
    var handler useafero.FileHandler

    BeforeEach(func() {
        handler = useafero.FileHandler{
            FileSystem: afero.NewMemMapFs(),
        }
    })

    Describe("Create", func() {
        It("create a file", func() {
            err := handler.Create("/unknown11a/tmp/abc.txt", "a\nb\nc\n")
            Expect(err).ShouldNot(HaveOccurred())
        })

        It("create a backup file if the file already exists", func() {
            exist, err := afero.Exists(handler.FileSystem, "/unknown11a/tmp/abc.txt_backup")
            Expect(err).ShouldNot(HaveOccurred())
            Expect(exist).Should(BeFalse())

            err = handler.Create("/unknown11a/tmp/abc.txt", "a\nb\nc\n")
            Expect(err).ShouldNot(HaveOccurred())

            err = handler.Create("/unknown11a/tmp/abc.txt", "a\nb\nc\n")
            Expect(err).ShouldNot(HaveOccurred())

            exist, err = afero.Exists(handler.FileSystem, "/unknown11a/tmp/abc.txt_backup")
            Expect(err).ShouldNot(HaveOccurred())
            Expect(exist).Should(BeTrue())
        })
    })
})

A nice point of this package is that it uses memory for the file system. It means that we don’t have to care about removing the file after each test. We can check if the backup file is created when Create is called twice with the same path.

Sponsored links

Create a test file in advance for unit tests

Creating a file is simple. Let’s have a look at the second example for reading a file. This method loads a file and calculates the sum written in the file.

func (f *FileHandler) ReadToGetSum(path string) (int, error) {
    exist, err := afero.Exists(f.FileSystem, path)
    if err != nil {
        return 0, fmt.Errorf("failed to check path existence: %w", err)
    }

    if !exist {
        return 0, os.ErrNotExist
    }

    file, err := f.FileSystem.Open(path)
    if err != nil {
        return 0, fmt.Errorf("failed to open a file: %w", err)
    }

    buffer := make([]byte, 10)
    content := ""

    for {
        size, err := file.Read(buffer)
        if err != nil {
            if errors.Is(err, io.EOF) {
                break
            }
            return 0, fmt.Errorf("failed to read content: %w", err)
        }

        content += string(buffer[0:size])
    }

    lines := strings.Split(content, "\n")
    sum := 0
    for _, value := range lines {
        intValue, err := strconv.ParseInt(value, 10, 64)
        if err != nil {
            return 0, fmt.Errorf("unexpected format: %w", err)
        }
        sum += int(intValue)
    }

    return sum, nil
}

We will write the following tests.

  • Returns an error when the file doesn’t exist
  • Returns an error when the file format is unexpected
  • Success case
Describe("ReadToGetSum", func() {
    It("error when a file doesn't exist", func() {
        _, err := handler.ReadToGetSum("/unknown11a/tmp/data.txt")
        Expect(errors.Is(err, os.ErrNotExist)).Should(BeTrue())
    })

    It("error when file format is unexpected", func() {
        file, err := handler.FileSystem.Create("/unknown11a/tmp/data.txt")
        Expect(err).ShouldNot(HaveOccurred())

        file.WriteString("1 1\n 2\n")

        _, err = handler.ReadToGetSum("/unknown11a/tmp/data.txt")
        Expect(err.Error()).Should(ContainSubstring("unexpected format"))
    })

    It("succeeds", func() {
        file, err := handler.FileSystem.Create("/unknown11a/tmp/data.txt")
        Expect(err).ShouldNot(HaveOccurred())

        file.WriteString("1\n2\n3")

        sum, err := handler.ReadToGetSum("/unknown11a/tmp/data.txt")
        Expect(err).ShouldNot(HaveOccurred())
        Expect(sum).Should(Equal(6))
    })
})

The first one is easy. We just pass a path that doesn’t exist.

The second one requires a dummy file. We can create a dummy file on the memory with handler.FileSystem.Create("file_path") because afero.NewMemMapFs() is set to handler.FileSystem.

file, err := handler.FileSystem.Create("/unknown11a/tmp/data.txt")
Expect(err).ShouldNot(HaveOccurred())

Since it returns a file object, we can write the actual content to the dummy file.

file.WriteString("1 1\n 2\n")

At last, we call the method that we want to test. That’s easy.

For the third test, it’s basically the same but the content is in the expected format.

Comments

Copied title and URL