mirror of
https://github.com/jimeh/go-golden.git
synced 2026-02-19 03:16:38 +00:00
This includes sanitizing golden file names to avoid characters and names which are invalid on some operating systems. So this should now work on Linux, macOS, and Window.
360 lines
11 KiB
Go
360 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 {
|
|
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 {
|
|
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) {
|
|
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 {
|
|
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 {
|
|
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) {
|
|
global.SetP(t, name, data)
|
|
}
|
|
|
|
// Update returns true when golden is set to update golden files. Used to
|
|
// determine if golden.Set() or golden.Write() 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. Used to
|
|
// determine if golden.Set() or golden.Write() 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()
|
|
}
|