mirror of
https://github.com/jimeh/go-golden.git
synced 2026-02-19 11:16:47 +00:00
375 lines
11 KiB
Go
375 lines
11 KiB
Go
// Package golden is yet another package for working with *.golden test files,
|
|
// with a focus on simplicity through it's default behavior.
|
|
//
|
|
// Golden file names are based on the name of the test function and any subtest
|
|
// names by calling t.Name(). File names are sanitized to ensure they're
|
|
// compatible with Linux, macOS and Windows systems regardless of what crazy
|
|
// characters might be in a subtest's name.
|
|
//
|
|
// # Usage
|
|
//
|
|
// Typical usage should look something like this:
|
|
//
|
|
// func TestExampleMyStruct(t *testing.T) {
|
|
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
|
|
// require.NoError(t, err)
|
|
//
|
|
// if golden.Update() {
|
|
// golden.Set(t, got)
|
|
// }
|
|
// want := golden.Get(t)
|
|
//
|
|
// assert.Equal(t, want, got)
|
|
// }
|
|
//
|
|
// The above example will read/write to:
|
|
//
|
|
// testdata/TestExampleMyStruct.golden
|
|
//
|
|
// To update the golden file (have golden.Update() return true), simply set the
|
|
// GOLDEN_UPDATE environment variable to one of "1", "y", "t", "yes", "on", or
|
|
// "true" when running tests.
|
|
//
|
|
// # Sub-Tests
|
|
//
|
|
// As the golden filename is based on t.Name(), it works with sub-tests too,
|
|
// ensuring each sub-test gets it's own golden file. For example:
|
|
//
|
|
// func TestExampleMyStructTabular(t *testing.T) {
|
|
// tests := []struct {
|
|
// name string
|
|
// obj *MyStruct
|
|
// }{
|
|
// {name: "empty struct", obj: &MyStruct{}},
|
|
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
|
|
// }
|
|
// for _, tt := range tests {
|
|
// t.Run(tt.name, func(t *testing.T) {
|
|
// got, err := json.Marshal(tt.obj)
|
|
// require.NoError(t, err)
|
|
//
|
|
// if golden.Update() {
|
|
// golden.Set(t, got)
|
|
// }
|
|
// want := golden.Get(t)
|
|
//
|
|
// assert.Equal(t, want, got)
|
|
// })
|
|
// }
|
|
// }
|
|
//
|
|
// The above example will read/write to:
|
|
//
|
|
// testdata/TestExampleMyStructTabular/empty_struct.golden
|
|
// testdata/TestExampleMyStructTabular/full_struct.golden
|
|
//
|
|
// # Multiple Golden Files in a Single Test
|
|
//
|
|
// The "P" suffixed methods, GetP(), SetP(), and FileP(), all take a name
|
|
// argument which allows using specific golden files within a given *testing.T
|
|
// instance.
|
|
//
|
|
// func TestExampleMyStructP(t *testing.T) {
|
|
// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
|
|
// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
|
|
//
|
|
// if golden.Update() {
|
|
// golden.SetP(t, "json", gotJSON)
|
|
// golden.SetP(t, "xml", gotXML)
|
|
// }
|
|
//
|
|
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
|
|
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
|
|
// }
|
|
//
|
|
// The above example will read/write to:
|
|
//
|
|
// testdata/TestExampleMyStructP/json.golden
|
|
// testdata/TestExampleMyStructP/xml.golden
|
|
//
|
|
// This works with tabular tests too of course:
|
|
//
|
|
// func TestExampleMyStructTabularP(t *testing.T) {
|
|
// tests := []struct {
|
|
// name string
|
|
// obj *MyStruct
|
|
// }{
|
|
// {name: "empty struct", obj: &MyStruct{}},
|
|
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
|
|
// }
|
|
// for _, tt := range tests {
|
|
// t.Run(tt.name, func(t *testing.T) {
|
|
// gotJSON, _ := json.Marshal(tt.obj)
|
|
// gotXML, _ := xml.Marshal(tt.obj)
|
|
//
|
|
// if golden.Update() {
|
|
// golden.SetP(t, "json", gotJSON)
|
|
// golden.SetP(t, "xml", gotXML)
|
|
// }
|
|
//
|
|
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
|
|
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
|
|
// })
|
|
// }
|
|
// }
|
|
//
|
|
// The above example will read/write to:
|
|
//
|
|
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
|
|
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
|
|
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
|
|
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
|
|
package golden
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
DefaultDirMode = 0o755
|
|
DefaultFileMode = 0o644
|
|
DefaultSuffix = ".golden"
|
|
DefaultDirname = "testdata"
|
|
)
|
|
|
|
var DefaultUpdateFunc = EnvUpdateFunc
|
|
|
|
var global = New()
|
|
|
|
// File returns the filename of the golden file for the given *testing.T
|
|
// instance as determined by t.Name().
|
|
func File(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
return global.File(t)
|
|
}
|
|
|
|
// Get returns the content of the golden file for the given *testing.T instance
|
|
// as determined by t.Name(). If no golden file can be found/read, it will fail
|
|
// the test by calling t.Fatal().
|
|
func Get(t *testing.T) []byte {
|
|
t.Helper()
|
|
|
|
return global.Get(t)
|
|
}
|
|
|
|
// Set writes given data to the golden file for the given *testing.T instance as
|
|
// determined by t.Name(). If writing fails it will fail the test by calling
|
|
// t.Fatal() with error details.
|
|
func Set(t *testing.T, data []byte) {
|
|
t.Helper()
|
|
|
|
global.Set(t, data)
|
|
}
|
|
|
|
// FileP returns the filename of the specifically named golden file for the
|
|
// given *testing.T instance as determined by t.Name().
|
|
func FileP(t *testing.T, name string) string {
|
|
t.Helper()
|
|
|
|
return global.FileP(t, name)
|
|
}
|
|
|
|
// GetP returns the content of the specifically named golden file belonging
|
|
// to the given *testing.T instance as determined by t.Name(). If no golden file
|
|
// can be found/read, it will fail the test with t.Fatal().
|
|
//
|
|
// This is very similar to Get(), but it allows multiple different golden files
|
|
// to be used within the same one *testing.T instance.
|
|
func GetP(t *testing.T, name string) []byte {
|
|
t.Helper()
|
|
|
|
return global.GetP(t, name)
|
|
}
|
|
|
|
// SetP writes given data of the specifically named golden file belonging to
|
|
// the given *testing.T instance as determined by t.Name(). If writing fails it
|
|
// will fail the test with t.Fatal() detailing the error.
|
|
//
|
|
// This is very similar to Set(), but it allows multiple different golden files
|
|
// to be used within the same one *testing.T instance.
|
|
func SetP(t *testing.T, name string, data []byte) {
|
|
t.Helper()
|
|
|
|
global.SetP(t, name, data)
|
|
}
|
|
|
|
// Update returns true when golden is set to update golden files. Should be used
|
|
// to determine if golden.Set() or golden.SetP() should be called or not.
|
|
//
|
|
// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE"
|
|
// environment variable is set to a truthy value. To customize create a custom
|
|
// *Golden instance with New() and set a new UpdateFunc value.
|
|
func Update() bool {
|
|
return global.Update()
|
|
}
|
|
|
|
// Golden handles all interactions with golden files. The top-level package
|
|
// functions all just proxy through to a default global *Golden instance.
|
|
type Golden struct {
|
|
// DirMode determines the file system permissions of any folders created to
|
|
// hold golden files.
|
|
DirMode os.FileMode
|
|
|
|
// FileMode determines the file system permissions of any created or updated
|
|
// golden files written to disk.
|
|
FileMode os.FileMode
|
|
|
|
// Suffix determines the filename suffix for all golden files. Typically
|
|
// this should be ".golden", but can be changed here if needed.
|
|
Suffix string
|
|
|
|
// Dirname is the name of the top-level directory at the root of the package
|
|
// which holds all golden files. Typically this should "testdata", but can
|
|
// be changed here if needed.
|
|
Dirname string
|
|
|
|
// UpdateFunc is used to determine if golden files should be updated or
|
|
// not. Its boolean return value is returned by Update().
|
|
UpdateFunc UpdateFunc
|
|
}
|
|
|
|
// New returns a new *Golden instance with default values correctly
|
|
// populated. This is ideally how you should create a custom *Golden, and then
|
|
// modify the relevant fields as you see fit.
|
|
func New() *Golden {
|
|
return &Golden{
|
|
DirMode: DefaultDirMode,
|
|
FileMode: DefaultFileMode,
|
|
Suffix: DefaultSuffix,
|
|
Dirname: DefaultDirname,
|
|
UpdateFunc: DefaultUpdateFunc,
|
|
}
|
|
}
|
|
|
|
// File returns the filename of the golden file for the given *testing.T
|
|
// instance as determined by t.Name().
|
|
func (s *Golden) File(t *testing.T) string {
|
|
return s.file(t, "")
|
|
}
|
|
|
|
// Get returns the content of the golden file for the given *testing.T instance
|
|
// as determined by t.Name(). If no golden file can be found/read, it will fail
|
|
// the test by calling t.Fatal().
|
|
func (s *Golden) Get(t *testing.T) []byte {
|
|
return s.get(t, "")
|
|
}
|
|
|
|
// Set writes given data to the golden file for the given *testing.T instance as
|
|
// determined by t.Name(). If writing fails it will fail the test by calling
|
|
// t.Fatal() with error details.
|
|
func (s *Golden) Set(t *testing.T, data []byte) {
|
|
s.set(t, "", data)
|
|
}
|
|
|
|
// FileP returns the filename of the specifically named golden file for the
|
|
// given *testing.T instance as determined by t.Name().
|
|
func (s *Golden) FileP(t *testing.T, name string) string {
|
|
if name == "" {
|
|
if t != nil {
|
|
t.Fatal("golden: name cannot be empty")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
return s.file(t, name)
|
|
}
|
|
|
|
// GetP returns the content of the specifically named golden file belonging
|
|
// to the given *testing.T instance as determined by t.Name(). If no golden file
|
|
// can be found/read, it will fail the test with t.Fatal().
|
|
//
|
|
// This is very similar to Get(), but it allows multiple different golden files
|
|
// to be used within the same one *testing.T instance.
|
|
func (s *Golden) GetP(t *testing.T, name string) []byte {
|
|
if name == "" {
|
|
t.Fatal("golden: name cannot be empty")
|
|
|
|
return nil
|
|
}
|
|
|
|
return s.get(t, name)
|
|
}
|
|
|
|
// SetP writes given data of the specifically named golden file belonging to
|
|
// the given *testing.T instance as determined by t.Name(). If writing fails it
|
|
// will fail the test with t.Fatal() detailing the error.
|
|
//
|
|
// This is very similar to Set(), but it allows multiple different golden files
|
|
// to be used within the same one *testing.T instance.
|
|
func (s *Golden) SetP(t *testing.T, name string, data []byte) {
|
|
if name == "" {
|
|
t.Fatal("golden: name cannot be empty")
|
|
}
|
|
|
|
s.set(t, name, data)
|
|
}
|
|
|
|
func (s *Golden) file(t *testing.T, name string) string {
|
|
if t.Name() == "" {
|
|
t.Fatalf("golden: could not determine filename for: %+v", t)
|
|
|
|
return ""
|
|
}
|
|
|
|
base := []string{s.Dirname, filepath.FromSlash(t.Name())}
|
|
if name != "" {
|
|
base = append(base, name)
|
|
}
|
|
|
|
f := filepath.Clean(filepath.Join(base...) + s.Suffix)
|
|
|
|
dirty := strings.Split(f, string(os.PathSeparator))
|
|
clean := make([]string, 0, len(dirty))
|
|
for _, s := range dirty {
|
|
clean = append(clean, sanitizeFilename(s))
|
|
}
|
|
|
|
return strings.Join(clean, string(os.PathSeparator))
|
|
}
|
|
|
|
func (s *Golden) get(t *testing.T, name string) []byte {
|
|
f := s.file(t, name)
|
|
|
|
b, err := ioutil.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatalf("golden: failed reading %s: %s", f, err.Error())
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
func (s *Golden) set(t *testing.T, name string, data []byte) {
|
|
f := s.file(t, name)
|
|
dir := filepath.Dir(f)
|
|
|
|
t.Logf("golden: writing .golden file: %s", f)
|
|
|
|
err := os.MkdirAll(dir, s.DirMode)
|
|
if err != nil {
|
|
t.Fatalf("golden: failed to create directory: %s", err.Error())
|
|
|
|
return
|
|
}
|
|
|
|
err = ioutil.WriteFile(f, data, s.FileMode)
|
|
if err != nil {
|
|
t.Fatalf("golden: filed to write file: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// Update returns true when golden is set to update golden files. Should be used
|
|
// to determine if golden.Set() or golden.SetP() should be called or not.
|
|
//
|
|
// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE"
|
|
// environment variable is set to a truthy value. To customize set a new
|
|
// UpdateFunc value on *Golden.
|
|
func (s *Golden) Update() bool {
|
|
return s.UpdateFunc()
|
|
}
|