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.
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.
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