feat(do): add new Do function to simplify golden usage (#11)

Add new Do function whihc ecapsulates the common pattern of writing to
golden file if the update mechnism evaluates to true, followed by
immediately reading the file. Compresses four lines into one.
This commit is contained in:
2025-03-24 13:17:17 +00:00
committed by GitHub
parent 62e8344ff3
commit bc86ba7a6d
5 changed files with 339 additions and 97 deletions

View File

@@ -33,6 +33,7 @@ SHELL := env \
#
# Tools
#
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)

View File

@@ -28,15 +28,12 @@ import "github.com/jimeh/go-golden"
```go
func TestExampleMyStruct(t *testing.T) {
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
want := golden.Do(t, got)
assert.Equal(t, want, got)
assert.Equal(t, want, got)
}
```
@@ -44,6 +41,15 @@ The above example will read/write to:
- `testdata/TestExampleMyStruct.golden`
The call to `golden.Do()` is equivalent to:
```go
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
```
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.

View File

@@ -10,6 +10,9 @@ import (
"github.com/stretchr/testify/require"
)
// The tests in this file are examples from the README and the package-level Go
// documentation.
type MyStruct struct {
Foo string `json:"foo,omitempty"`
}
@@ -21,10 +24,7 @@ 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)
want := golden.Do(t, got)
assert.Equal(t, want, got)
}
@@ -46,10 +46,7 @@ func TestExampleMyStructTabular(t *testing.T) {
got, err := json.Marshal(tt.obj)
require.NoError(t, err)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
want := golden.Do(t, got)
assert.Equal(t, want, got)
})
@@ -64,13 +61,11 @@ 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)
}
wantJSON := golden.DoP(t, "json", gotJSON)
wantXML := golden.DoP(t, "xml", gotXML)
assert.Equal(t, golden.GetP(t, "json"), gotJSON)
assert.Equal(t, golden.GetP(t, "xml"), gotXML)
assert.Equal(t, wantJSON, gotJSON)
assert.Equal(t, wantXML, gotXML)
}
// TestExampleMyStructTabularP reads/writes the following golden file:
@@ -92,13 +87,11 @@ func TestExampleMyStructTabularP(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)
}
wantJSON := golden.DoP(t, "json", gotJSON)
wantXML := golden.DoP(t, "xml", gotXML)
assert.Equal(t, golden.GetP(t, "json"), gotJSON)
assert.Equal(t, golden.GetP(t, "xml"), gotXML)
assert.Equal(t, wantJSON, gotJSON)
assert.Equal(t, wantXML, gotXML)
})
}
}

195
golden.go
View File

@@ -1,31 +1,35 @@
// 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
// Golden file names are based on the name of the test function and any sub-test
// 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.
// characters might be in a sub-test'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)
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
// require.NoError(t, err)
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
// want := golden.Do(t, got)
//
// assert.Equal(t, want, got)
// assert.Equal(t, want, got)
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStruct.golden
//
// The call to golden.Do() is equivalent to:
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
//
// 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.
@@ -33,29 +37,26 @@
// # 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:
// ensuring each sub-test gets its 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)
// 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)
// want := golden.Do(t, got)
//
// assert.Equal(t, want, got)
// })
// }
// assert.Equal(t, want, got)
// })
// }
// }
//
// The above example will read/write to:
@@ -70,16 +71,14 @@
// instance.
//
// func TestExampleMyStructP(t *testing.T) {
// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
// 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)
// }
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
//
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// }
//
// The above example will read/write to:
@@ -90,27 +89,25 @@
// 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)
// 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)
// }
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
//
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
// })
// }
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// })
// }
// }
//
// The above example will read/write to:
@@ -148,6 +145,15 @@ var (
DefaultUpdateFunc = EnvUpdateFunc
)
// Do is a convenience function for calling Update(), Set(), and Get() in a
// single call. If Update() returns true, data will be written to the golden
// file using Set(), before reading it back with Get().
func Do(t TestingT, data []byte) []byte {
t.Helper()
return Default.Do(t, data)
}
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func File(t TestingT) string {
@@ -174,6 +180,15 @@ func Set(t TestingT, data []byte) {
Default.Set(t, data)
}
// DoP is a convenience function for calling Update(), SetP(), and GetP() in a
// single call. If Update() returns true, data will be written to the golden
// file using SetP(), before reading it back with GetP().
func DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
return Default.DoP(t, name, 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 TestingT, name string) string {
@@ -228,7 +243,7 @@ type Golden struct {
FileMode os.FileMode
// Suffix determines the filename suffix for all golden files. Typically
// this should be ".golden", but can be changed here if needed.
// this would be ".golden".
Suffix string
// Dirname is the name of the top-level directory at the root of the package
@@ -241,9 +256,7 @@ type Golden struct {
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.
// New returns a new *Golden instance with default values correctly populated.
func New() *Golden {
return &Golden{
DirMode: DefaultDirMode,
@@ -254,9 +267,24 @@ func New() *Golden {
}
}
// Do is a convenience function for calling Update(), Set(), and Get() in a
// single call. If Update() returns true, data will be written to the golden
// file using Set(), before reading it back with Get().
func (s *Golden) Do(t TestingT, data []byte) []byte {
t.Helper()
if s.Update() {
s.Set(t, data)
}
return s.Get(t)
}
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func (s *Golden) File(t TestingT) string {
t.Helper()
return s.file(t, "")
}
@@ -264,6 +292,8 @@ func (s *Golden) File(t TestingT) string {
// 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 TestingT) []byte {
t.Helper()
return s.get(t, "")
}
@@ -271,12 +301,33 @@ func (s *Golden) Get(t TestingT) []byte {
// determined by t.Name(). If writing fails it will fail the test by calling
// t.Fatal() with error details.
func (s *Golden) Set(t TestingT, data []byte) {
t.Helper()
s.set(t, "", data)
}
// DoP is a convenience function for calling Update(), SetP(), and GetP() in a
// single call. If Update() returns true, data will be written to the golden
// file using SetP(), before reading it back with GetP().
func (s *Golden) DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
if s.Update() {
s.SetP(t, name, data)
}
return s.GetP(t, name)
}
// 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 TestingT, name string) string {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
@@ -291,6 +342,8 @@ func (s *Golden) FileP(t TestingT, name string) string {
// 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 TestingT, name string) []byte {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
@@ -305,6 +358,8 @@ func (s *Golden) GetP(t TestingT, name string) []byte {
// 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 TestingT, name string, data []byte) {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
@@ -312,9 +367,19 @@ func (s *Golden) SetP(t TestingT, name string, data []byte) {
s.set(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 set a new
// UpdateFunc value on *Golden.
func (s *Golden) Update() bool {
return s.UpdateFunc()
}
func (s *Golden) file(t TestingT, name string) string {
if t.Name() == "" {
t.Fatalf("golden: could not determine filename for: %+v", t)
t.Fatalf("golden: could not determine filename")
}
base := []string{s.Dirname, filepath.FromSlash(t.Name())}
@@ -360,13 +425,3 @@ func (s *Golden) set(t TestingT, name string, data []byte) {
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()
}

View File

@@ -100,6 +100,102 @@ func TestNew(t *testing.T) {
assert.Equal(t, wantFuncName, gotFuncName)
}
func TestDo(t *testing.T) {
t.Cleanup(func() {
err := os.RemoveAll(filepath.Join("testdata", "TestDo"))
require.NoError(t, err)
err = os.Remove(filepath.Join("testdata", "TestDo.golden"))
require.NoError(t, err)
})
//
// Test when Update is false
//
content := []byte("This is the golden file for TestDo")
err := os.MkdirAll("testdata", 0o755)
require.NoError(t, err)
err = os.WriteFile(
filepath.Join("testdata", "TestDo.golden"),
content, 0o600,
)
require.NoError(t, err)
newContent := []byte("This should not be written")
t.Setenv("GOLDEN_UPDATE", "false")
got := Do(t, newContent)
assert.Equal(t, content, got)
// Verify file wasn't changed
fileContent, err := os.ReadFile(
filepath.Join("testdata", "TestDo.golden"),
)
require.NoError(t, err)
assert.Equal(t, content, fileContent)
//
// Test when Update is true
//
updatedContent := []byte("This is the updated content for TestDo")
t.Setenv("GOLDEN_UPDATE", "true")
got = Do(t, updatedContent)
assert.Equal(t, updatedContent, got)
// Verify file was updated
fileContent, err = os.ReadFile(
filepath.Join("testdata", "TestDo.golden"),
)
require.NoError(t, err)
assert.Equal(t, updatedContent, fileContent)
//
// Test with sub-tests
//
tests := []struct {
name string
content []byte
}{
{
name: "simple",
content: []byte("Simple content for sub-test"),
},
{
name: "complex/path",
content: []byte("Complex path content for sub-test"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with Update true
t.Setenv("GOLDEN_UPDATE", "true")
got := Do(t, tt.content)
assert.Equal(t, tt.content, got)
// Verify file was written with correct content
f := File(t)
fileContent, err := os.ReadFile(f)
require.NoError(t, err)
assert.Equal(t, tt.content, fileContent)
// Test with Update false
t.Setenv("GOLDEN_UPDATE", "false")
newContent := []byte(
"This should not be written in sub-test",
)
got = Do(t, newContent)
assert.Equal(t, tt.content, got)
// Verify file wasn't changed
f = File(t)
fileContent, err = os.ReadFile(f)
require.NoError(t, err)
assert.Equal(t, tt.content, fileContent)
})
}
}
func TestFile(t *testing.T) {
got := File(t)
@@ -275,6 +371,97 @@ func TestSet(t *testing.T) {
}
}
func TestDoP(t *testing.T) {
t.Cleanup(func() {
err := os.RemoveAll(filepath.Join("testdata", "TestDoP"))
require.NoError(t, err)
})
//
// Test when Update is false
//
name := "test-format"
content := []byte("This is the golden file for TestDoP")
err := os.MkdirAll(filepath.Join("testdata", "TestDoP"), 0o755)
require.NoError(t, err)
goldenFile := filepath.Join("testdata", "TestDoP", name+".golden")
err = os.WriteFile(goldenFile, content, 0o600)
require.NoError(t, err)
newContent := []byte("This should not be written")
t.Setenv("GOLDEN_UPDATE", "false")
got := DoP(t, name, newContent)
assert.Equal(t, content, got)
// Verify file wasn't changed
fileContent, err := os.ReadFile(goldenFile)
require.NoError(t, err)
assert.Equal(t, content, fileContent)
//
// Test when Update is true
//
updatedContent := []byte("This is the updated content for TestDoP")
t.Setenv("GOLDEN_UPDATE", "true")
got = DoP(t, name, updatedContent)
assert.Equal(t, updatedContent, got)
// Verify file was updated
fileContent, err = os.ReadFile(goldenFile)
require.NoError(t, err)
assert.Equal(t, updatedContent, fileContent)
//
// Test with sub-tests
//
tests := []struct {
testName string
name string
content []byte
}{
{
testName: "json format",
name: "json",
content: []byte(`{"key": "value"}`),
},
{
testName: "xml format",
name: "xml",
content: []byte(`<root><key>value</key></root>`),
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
// Test with Update true
t.Setenv("GOLDEN_UPDATE", "true")
got := DoP(t, tt.name, tt.content)
assert.Equal(t, tt.content, got)
// Verify file was written with correct content
f := FileP(t, tt.name)
fileContent, err := os.ReadFile(f)
require.NoError(t, err)
assert.Equal(t, tt.content, fileContent)
// Test with Update false
t.Setenv("GOLDEN_UPDATE", "false")
newContent := []byte(
"This should not be written in sub-test",
)
got = DoP(t, tt.name, newContent)
assert.Equal(t, tt.content, got)
// Verify file wasn't changed
f = FileP(t, tt.name)
fileContent, err = os.ReadFile(f)
require.NoError(t, err)
assert.Equal(t, tt.content, fileContent)
})
}
}
func TestFileP(t *testing.T) {
got := FileP(t, "sub-name")
assert.Equal(t,