From df20b0076675bd9dda6f0163c88b64de7976bf72 Mon Sep 17 00:00:00 2001 From: Trevor Gattis Date: Wed, 25 Sep 2019 23:25:04 -0700 Subject: [PATCH 01/11] Restructured code and now requires >= Go v1.13 for error handling In order allow customizations of the golden library per unit test, the goldie library was restructured to not use global variables and adopt the function options pattern: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis This restructuring also made it easier to implement the following features: * Different fixture directories * A separate directory per unit test * A separate directory per subtest in the unit test * Erroring out if a template didn't get data that it needed * Diff output (classic or in color) * Using your own diff function for output Although a little non standard, the methods that a consumer will be using are now all in the `interface.go` file. In addition, error handling was cleaned up (now uses pointer receivers) and leverages the new Go v1.13 `errors` package. More unit testing is desired. --- README.md | 29 +++++- errors.go | 39 ++++++-- errors_test.go | 6 +- go.mod | 7 +- go.sum | 4 + goldie.go | 237 ++++++++++++++++++++++++++++++++++++------------- goldie_test.go | 161 ++++++++++++++++++++++++++------- interface.go | 141 +++++++++++++++++++++++++++++ 8 files changed, 516 insertions(+), 108 deletions(-) create mode 100644 interface.go diff --git a/README.md b/README.md index 422c022..1b5d084 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ what is present in the golden test file. ``` func TestExample(t *testing.T) { + g := goldie.New(t) recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "/example", nil) @@ -32,14 +33,14 @@ func TestExample(t *testing.T) { handler := http.HandlerFunc(ExampleHandler) handler.ServeHTTP() - goldie.Assert(t, "example", recorder.Body.Bytes()) + g.Assert(t, "example", recorder.Body.Bytes()) } ``` ## Using template golden file If some values in the golden file can change depending on the test, you can use golang -template in the golden file and pass the data to `goldie.AssertWithTemplate`. +template in the golden file and pass the data to `AssertWithTemplate`. ### example.golden ``` @@ -49,6 +50,8 @@ This is a {{ .Type }} file. ### Test ``` func TestTemplateExample(t *testing.T) { + g := goldie.New(t) + recorder := httptest.NewRecorder() req, err := http.NewRequest("POST", "/example/Golden", nil) @@ -63,7 +66,7 @@ func TestTemplateExample(t *testing.T) { Type: "Golden", } - goldie.AssertWithTemplate(t, "example", data, recorder.Body.Bytes()) + g.AssertWithTemplate(t, "example", data, recorder.Body.Bytes()) } ``` @@ -76,6 +79,25 @@ drop the `-update` flag. `go test ./...` +## Options +`goldie` supports a number of configuration options that will alter the behavior +of the library. These options should be passed into the `goldie.New()` method. + +``` +func TestNewExample(t *testing.T) { + g := goldie.New( + t, + goldie.WithFixtureDir("test-fixtures"), + goldie.WithNameSuffix(".golden.json"), + goldie.WithDiffEngine(goldie.ColoredDiff), + goldie.WithTestNameForDir(true), + ) + + g.Assert(t, "example", "my example data") +} + +``` + ## FAQ ### Do you need any help in the project? @@ -83,7 +105,6 @@ drop the `-update` flag. Yes, please! Pull requests are most welcome. On the wish list: - Unit tests. -- Better output for failed tests. A diff of some sort would be great. ### Why the name `goldie`? diff --git a/errors.go b/errors.go index a119764..877c1e1 100644 --- a/errors.go +++ b/errors.go @@ -8,14 +8,15 @@ type errFixtureNotFound struct { } // newErrFixtureNotFound returns a new instance of the error. -func newErrFixtureNotFound() errFixtureNotFound { - return errFixtureNotFound{ +func newErrFixtureNotFound() *errFixtureNotFound { + return &errFixtureNotFound{ + // TODO: flag name should be based on the variable value message: "Golden fixture not found. Try running with -update flag.", } } // Error returns the error message. -func (e errFixtureNotFound) Error() string { +func (e *errFixtureNotFound) Error() string { return e.message } @@ -26,13 +27,13 @@ type errFixtureMismatch struct { } // newErrFixtureMismatch returns a new instance of the error. -func newErrFixtureMismatch(message string) errFixtureMismatch { - return errFixtureMismatch{ +func newErrFixtureMismatch(message string) *errFixtureMismatch { + return &errFixtureMismatch{ message: message, } } -func (e errFixtureMismatch) Error() string { +func (e *errFixtureMismatch) Error() string { return e.message } @@ -42,12 +43,32 @@ type errFixtureDirectoryIsFile struct { } // newFixtureDirectoryIsFile returns a new instance of the error. -func newErrFixtureDirectoryIsFile(file string) errFixtureDirectoryIsFile { - return errFixtureDirectoryIsFile{ +func newErrFixtureDirectoryIsFile(file string) *errFixtureDirectoryIsFile { + return &errFixtureDirectoryIsFile{ file: file, } } -func (e errFixtureDirectoryIsFile) Error() string { +func (e *errFixtureDirectoryIsFile) Error() string { return fmt.Sprintf("fixture folder is a file: %s", e.file) } + +func (e *errFixtureDirectoryIsFile) File() string { + return e.file +} + +// errMissingKey is thrown when a value for a template is missing +type errMissingKey struct { + message string +} + +// newErrMissingKey returns a new instance of the error. +func newErrMissingKey(message string) *errMissingKey { + return &errMissingKey{ + message: message, + } +} + +func (e *errMissingKey) Error() string { + return e.message +} diff --git a/errors_test.go b/errors_test.go index 95713d4..98d1007 100644 --- a/errors_test.go +++ b/errors_test.go @@ -11,7 +11,7 @@ func TestErrFixtureNotFound(t *testing.T) { err := newErrFixtureNotFound() assert.Equal(t, expected, err.Error()) - assert.IsType(t, errFixtureNotFound{}, err) + assert.IsType(t, &errFixtureNotFound{}, err) } func TestErrFixtureMismatch(t *testing.T) { @@ -19,7 +19,7 @@ func TestErrFixtureMismatch(t *testing.T) { err := newErrFixtureMismatch(message) assert.Equal(t, message, err.Error()) - assert.IsType(t, errFixtureMismatch{}, err) + assert.IsType(t, &errFixtureMismatch{}, err) } func TestErrFixtureDirectoryIsFile(t *testing.T) { @@ -28,5 +28,5 @@ func TestErrFixtureDirectoryIsFile(t *testing.T) { err := newErrFixtureDirectoryIsFile(location) assert.Equal(t, message, err.Error()) - assert.IsType(t, errFixtureDirectoryIsFile{}, err) + assert.IsType(t, &errFixtureDirectoryIsFile{}, err) } diff --git a/go.mod b/go.mod index 4f9e52d..1e36286 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/sebdah/goldie go 1.12 -require github.com/stretchr/testify v1.3.0 +require ( + github.com/pkg/errors v0.8.1 + github.com/pmezard/go-difflib v1.0.0 + github.com/sergi/go-diff v1.0.0 + github.com/stretchr/testify v1.3.0 +) diff --git a/go.sum b/go.sum index 4347755..b3f9d52 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/goldie.go b/goldie.go index 3bec49e..7756115 100644 --- a/goldie.go +++ b/goldie.go @@ -11,38 +11,79 @@ package goldie import ( "bytes" "encoding/json" - "flag" "fmt" "io/ioutil" "os" "path/filepath" + "strings" "testing" "text/template" + + "errors" + + "github.com/pmezard/go-difflib/difflib" + "github.com/sergi/go-diff/diffmatchpatch" ) -var ( - // FixtureDir is the folder name for where the fixtures are stored. It's - // relative to the "go test" path. - FixtureDir = "fixtures" +type goldie struct { + fixtureDir string + fileNameSuffix string + filePerms os.FileMode + dirPerms os.FileMode + + diffProcessor DiffProcessor + diffFn DiffFn + ignoreTemplateErrors bool + useTestNameForDir bool + useSubTestNameForDir bool +} + +// === OptionProcessor =============================== - // FileNameSuffix is the suffix appended to the fixtures. Set to empty - // string to disable file name suffixes. - FileNameSuffix = ".golden" +func (g *goldie) WithFixtureDir(dir string) error { + g.fixtureDir = dir + return nil +} - // FlagName is the name of the command line flag for go test. - FlagName = "update" +func (g *goldie) WithNameSuffix(suffix string) error { + g.fileNameSuffix = suffix + return nil +} - // FilePerms is used to set the permissions on the golden fixture files. - FilePerms os.FileMode = 0644 +func (g *goldie) WithFilePerms(mode os.FileMode) error { + g.filePerms = mode + return nil +} - // DirPerms is used to set the permissions on the golden fixture folder. - DirPerms os.FileMode = 0755 +func (g *goldie) WithDirPerms(mode os.FileMode) error { + g.dirPerms = mode + return nil +} - // update determines if the actual received data should be written to the - // golden files or not. This should be true when you need to update the - // data, but false when actually running the tests. - update = flag.Bool(FlagName, false, "Update golden test file fixture") -) +func (g *goldie) WithDiffEngine(engine DiffProcessor) error { + g.diffProcessor = engine + return nil +} + +func (g *goldie) WithDiffFn(fn DiffFn) error { + g.diffFn = fn + return nil +} + +func (g *goldie) WithIgnoreTemplateErrors(ignoreErrors bool) error { + g.ignoreTemplateErrors = ignoreErrors + return nil +} + +func (g *goldie) WithTestNameForDir(use bool) error { + g.useTestNameForDir = use + return nil +} + +func (g *goldie) WithSubTestNameForDir(use bool) error { + g.useSubTestNameForDir = use + return nil +} // Assert compares the actual data received with the expected data in the // golden files. If the update flag is set, it will also update the golden @@ -51,26 +92,35 @@ var ( // `name` refers to the name of the test and it should typically be unique // within the package. Also it should be a valid file name (so keeping to // `a-z0-9\-\_` is a good idea). -func Assert(t *testing.T, name string, actualData []byte) { +func (g *goldie) Assert(t *testing.T, name string, actualData []byte) { if *update { - err := Update(name, actualData) + err := g.Update(t, name, actualData) if err != nil { t.Error(err) t.FailNow() } } - err := compare(name, actualData) + err := g.compare(t, name, actualData) if err != nil { - switch err.(type) { - case errFixtureNotFound: - t.Error(err) - t.FailNow() - case errFixtureMismatch: - t.Error(err) - default: - t.Error(err) + { + var e *errFixtureNotFound + if errors.As(err, &e) { + t.Error(err) + t.FailNow() + return + } } + + { + var e *errFixtureMismatch + if errors.As(err, &e) { + t.Error(err) + return + } + } + + t.Error(err) } } @@ -81,7 +131,7 @@ func Assert(t *testing.T, name string, actualData []byte) { // `name` refers to the name of the test and it should typically be unique // within the package. Also it should be a valid file name (so keeping to // `a-z0-9\-\_` is a good idea). -func AssertJson(t *testing.T, name string, actualJsonData interface{}) { +func (g *goldie) AssertJson(t *testing.T, name string, actualJsonData interface{}) { js, err := json.MarshalIndent(actualJsonData, "", " ") if err != nil { @@ -89,7 +139,7 @@ func AssertJson(t *testing.T, name string, actualJsonData interface{}) { t.FailNow() } - Assert(t, name, normalizeLF(js)) + g.Assert(t, name, normalizeLF(js)) } // normalizeLF normalizes line feed character set across os (es) @@ -112,26 +162,35 @@ func normalizeLF(d []byte) []byte { // `name` refers to the name of the test and it should typically be unique // within the package. Also it should be a valid file name (so keeping to // `a-z0-9\-\_` is a good idea). -func AssertWithTemplate(t *testing.T, name string, data interface{}, actualData []byte) { +func (g *goldie) AssertWithTemplate(t *testing.T, name string, data interface{}, actualData []byte) { if *update { - err := Update(name, actualData) + err := g.Update(t, name, actualData) if err != nil { t.Error(err) t.FailNow() } } - err := compareTemplate(name, data, actualData) + err := g.compareTemplate(t, name, data, actualData) if err != nil { - switch err.(type) { - case errFixtureNotFound: - t.Error(err) - t.FailNow() - case errFixtureMismatch: - t.Error(err) - default: - t.Error(err) + { + var e *errFixtureNotFound + if errors.As(err, &e) { + t.Error(err) + t.FailNow() + return + } } + + { + var e *errFixtureMismatch + if errors.As(err, &e) { + t.Error(err) + return + } + } + + t.Error(err) } } @@ -140,18 +199,18 @@ func AssertWithTemplate(t *testing.T, name string, data interface{}, actualData // This method does not need to be called from code, but it's exposed so that it // can be explicitly called if needed. The more common approach would be to // update using `go test -update ./...`. -func Update(name string, actualData []byte) error { - if err := ensureDir(filepath.Dir(goldenFileName(name))); err != nil { +func (g *goldie) Update(t *testing.T, name string, actualData []byte) error { + if err := g.ensureDir(filepath.Dir(g.goldenFileName(t, name))); err != nil { return err } - return ioutil.WriteFile(goldenFileName(name), actualData, FilePerms) + return ioutil.WriteFile(g.goldenFileName(t, name), actualData, g.filePerms) } // compare is reading the golden fixture file and compare the stored data with // the actual data. -func compare(name string, actualData []byte) error { - expectedData, err := ioutil.ReadFile(goldenFileName(name)) +func (g *goldie) compare(t *testing.T, name string, actualData []byte) error { + expectedData, err := ioutil.ReadFile(g.goldenFileName(t, name)) if err != nil { if os.IsNotExist(err) { @@ -162,21 +221,58 @@ func compare(name string, actualData []byte) error { } if !bytes.Equal(actualData, expectedData) { - return newErrFixtureMismatch( - fmt.Sprintf("Result did not match the golden fixture.\n"+ - "Expected: %s\n"+ + msg := "Result did not match the golden fixture.\n" + actual := string(actualData) + expected := string(expectedData) + + if g.diffFn != nil || g.diffProcessor != UndefinedDiff { + var d string + if g.diffFn != nil { + d = g.diffFn(actual, expected) + } else { + d = diff(g.diffProcessor, actual, expected) + } + + msg += "Diff is below:\n" + d + } else { + msg = fmt.Sprintf("%sExpected: %s\n"+ "Got: %s", - string(expectedData), - string(actualData))) + msg, + expected, + actual) + } + return newErrFixtureMismatch(msg) } return nil } +func diff(engine DiffProcessor, actual string, expected string) string { + var diff string + switch engine { + case ClassicDiff: + diff, _ = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(expected), + B: difflib.SplitLines(actual), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + case ColoredDiff: + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, expected, false) + diff = dmp.DiffPrettyText(diffs) + } + return diff +} + // compareTemplate is reading the golden fixture file and compare the stored // data with the actual data. -func compareTemplate(name string, data interface{}, actualData []byte) error { - expectedDataTmpl, err := ioutil.ReadFile(goldenFileName(name)) +func (g *goldie) compareTemplate(t *testing.T, name string, data interface{}, actualData []byte) error { + expectedDataTmpl, err := ioutil.ReadFile(g.goldenFileName(t, name)) if err != nil { if os.IsNotExist(err) { @@ -186,7 +282,11 @@ func compareTemplate(name string, data interface{}, actualData []byte) error { return fmt.Errorf("Expected %s to be nil", err.Error()) } - tmpl, err := template.New("test").Parse(string(expectedDataTmpl)) + missingKey := "error" + if g.ignoreTemplateErrors { + missingKey = "default" + } + tmpl, err := template.New("test").Option("missingkey=" + missingKey).Parse(string(expectedDataTmpl)) if err != nil { return fmt.Errorf("Expected %s to be nil", err.Error()) } @@ -194,7 +294,7 @@ func compareTemplate(name string, data interface{}, actualData []byte) error { var expectedData bytes.Buffer err = tmpl.Execute(&expectedData, data) if err != nil { - return fmt.Errorf("Expected %s to be nil", err.Error()) + return newErrMissingKey(fmt.Sprintf("Template error: %s", err.Error())) } if !bytes.Equal(actualData, expectedData.Bytes()) { @@ -210,12 +310,12 @@ func compareTemplate(name string, data interface{}, actualData []byte) error { } // ensureDir will create the fixture folder if it does not already exist. -func ensureDir(loc string) error { +func (g *goldie) ensureDir(loc string) error { s, err := os.Stat(loc) switch { case err != nil && os.IsNotExist(err): // the location does not exist, so make directories to there - return os.MkdirAll(loc, DirPerms) + return os.MkdirAll(loc, g.dirPerms) case err == nil && !s.IsDir(): return newErrFixtureDirectoryIsFile(loc) } @@ -224,6 +324,21 @@ func ensureDir(loc string) error { } // goldenFileName simply returns the file name of the golden file fixture. -func goldenFileName(name string) string { - return filepath.Join(FixtureDir, fmt.Sprintf("%s%s", name, FileNameSuffix)) +func (g *goldie) goldenFileName(t *testing.T, name string) string { + + dir := g.fixtureDir + + if g.useTestNameForDir { + dir = filepath.Join(dir, strings.Split(t.Name(), "/")[0]) + } + + if g.useSubTestNameForDir { + n := strings.Split(t.Name(), "/") + if len(n) > 1 { + + dir = filepath.Join(dir, n[1]) + } + } + + return filepath.Join(dir, fmt.Sprintf("%s%s", name, g.fileNameSuffix)) } diff --git a/goldie_test.go b/goldie_test.go index 09e5181..98e809f 100644 --- a/goldie_test.go +++ b/goldie_test.go @@ -1,6 +1,7 @@ package goldie import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -45,17 +46,13 @@ func TestGoldenFileName(t *testing.T) { } for _, test := range tests { - oldFixtureDir := FixtureDir - oldFileNameSuffix := FileNameSuffix + g := New(t, + WithFixtureDir(test.dir), + WithNameSuffix(test.suffix), + ) - FixtureDir = test.dir - FileNameSuffix = test.suffix - - filename := goldenFileName(test.name) + filename := g.goldenFileName(t, test.name) assert.Equal(t, test.expected, filename) - - FixtureDir = oldFixtureDir - FileNameSuffix = oldFileNameSuffix } } @@ -89,6 +86,8 @@ func TestEnsureDir(t *testing.T) { }, } + g := New(t) + for _, test := range tests { target := filepath.Join(os.TempDir(), test.dir) @@ -106,7 +105,7 @@ func TestEnsureDir(t *testing.T) { f.Close() } - err := ensureDir(target) + err := g.ensureDir(target) assert.IsType(t, test.err, err) } } @@ -126,15 +125,17 @@ func TestUpdate(t *testing.T) { }, } + g := New(t) + for _, test := range tests { - err := Update(test.name, test.data) + err := g.Update(t, test.name, test.data) assert.Equal(t, test.err, err) - data, err := ioutil.ReadFile(goldenFileName(test.name)) + data, err := ioutil.ReadFile(g.goldenFileName(t, test.name)) assert.Nil(t, err) assert.Equal(t, test.data, data) - err = os.RemoveAll(FixtureDir) + err = os.RemoveAll(g.fixtureDir) assert.Nil(t, err) } } @@ -159,14 +160,14 @@ func TestCompare(t *testing.T) { actualData: []byte("abc"), expectedData: []byte("abc"), update: false, - err: errFixtureNotFound{}, + err: &errFixtureNotFound{}, }, { name: "example", actualData: []byte("bc"), expectedData: []byte("abc"), update: true, - err: errFixtureMismatch{}, + err: &errFixtureMismatch{}, }, { name: "nil", @@ -177,16 +178,19 @@ func TestCompare(t *testing.T) { }, } + g := New(t) + for _, test := range tests { if test.update { - err := Update(test.name, test.expectedData) + err := g.Update(t, test.name, test.expectedData) assert.Nil(t, err) } - err := compare(test.name, test.actualData) + err := g.compare(t, test.name, test.actualData) assert.IsType(t, test.err, err) - err = os.RemoveAll(FixtureDir) + g.goldenFileName(t, test.name) + err = os.RemoveAll(filepath.Dir(g.goldenFileName(t, test.name))) assert.Nil(t, err) } } @@ -221,29 +225,40 @@ func TestCompareTemplate(t *testing.T) { expectedData: []byte("abc {{ .Name }}"), data: nil, update: false, - err: errFixtureNotFound{}, + err: &errFixtureNotFound{}, }, { name: "example", actualData: []byte("bc example"), expectedData: []byte("abc {{ .Name }}"), - data: nil, + data: data, update: true, - err: errFixtureMismatch{}, + err: &errFixtureMismatch{}, }, - } + { + name: "example", + actualData: []byte("bc example"), + expectedData: []byte("abc {{ .Name }}"), + data: nil, + update: true, + err: &errMissingKey{}, + }} + + g := New(t) for _, test := range tests { - if test.update { - err := Update(test.name, test.expectedData) - assert.Nil(t, err) - } + t.Run(test.name, func(t *testing.T) { + if test.update { + err := g.Update(t, test.name, test.expectedData) + assert.Nil(t, err) + } - err := compareTemplate(test.name, test.data, test.actualData) - assert.IsType(t, test.err, err) + err := g.compareTemplate(t, test.name, test.data, test.actualData) + assert.IsType(t, test.err, err) - err = os.RemoveAll(FixtureDir) - assert.Nil(t, err) + err = os.RemoveAll(g.fixtureDir) + assert.Nil(t, err) + }) } } @@ -267,3 +282,89 @@ func TestNormalizeLF(t *testing.T) { }) } } + +func TestDiffEngines(t *testing.T) { + type engine struct { + engine DiffProcessor + diff string + } + + tests := []struct { + name string + actual string + expected string + engines []engine + }{ + { + name: "lorem", + actual: "Lorem ipsum dolor.", + expected: "Lorem dolor sit amet.", + engines: []engine{ + {engine: ClassicDiff, diff: `--- Expected ++++ Actual +@@ -1 +1 @@ +-Lorem dolor sit amet. ++Lorem ipsum dolor. +`}, + {engine: ColoredDiff, diff: "Lorem \x1b[31mipsum \x1b[0mdolor\x1b[32m sit amet\x1b[0m."}, + }, + }, + } + + for _, tt := range tests { + for _, e := range tt.engines { + diff := diff(e.engine, tt.actual, tt.expected) + assert.Equal(t, e.diff, diff) + } + } + +} + +func TestNewExample(t *testing.T) { + tests := []struct { + fixtureDir string // This will get removed from the file system for each test + suffix string + subTestName string + filePrefix string + }{ + { + fixtureDir: "test-fixtures", + suffix: ".golden.json", + subTestName: "subtestname", + filePrefix: "example", + }, + } + + sampleData := []byte("sample data") + + for _, tt := range tests { + g := New(t, + WithFixtureDir(tt.fixtureDir), + WithNameSuffix(tt.suffix), + WithTestNameForDir(true), + WithSubTestNameForDir(true), + ) + + t.Run(tt.subTestName, func(t *testing.T) { + g.Update(t, tt.filePrefix, sampleData) + g.Assert(t, tt.filePrefix, sampleData) + }) + + fullpath := fmt.Sprintf("%s%s", + filepath.Join( + tt.fixtureDir, + "TestNewExample", + tt.subTestName, + tt.filePrefix, + ), + tt.suffix, + ) + + _, err := os.Stat(fullpath) + assert.Nil(t, err) + + os.RemoveAll(tt.fixtureDir) + assert.Nil(t, err) + } + +} diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..a5fcb84 --- /dev/null +++ b/interface.go @@ -0,0 +1,141 @@ +// Package goldie provides test assertions based on golden files. It's typically +// used for testing responses with larger data bodies. +// +// The concept is straight forward. Valid response data is stored in a "golden +// file". The actual response data will be byte compared with the golden file +// and the test will fail if there is a difference. +// +// Updating the golden file can be done by running `go test -update ./...`. +package goldie + +import ( + "flag" + "fmt" + "os" + "testing" +) + +const ( + // FixtureDir is the folder name for where the fixtures are stored. It's + // relative to the "go test" path. + defaultFixtureDir = "testdata" + + // FileNameSuffix is the suffix appended to the fixtures. Set to empty + // string to disable file name suffixes. + defaultFileNameSuffix = ".golden" + + // FilePerms is used to set the permissions on the golden fixture files. + defaultFilePerms os.FileMode = 0644 + + // DirPerms is used to set the permissions on the golden fixture folder. + defaultDirPerms os.FileMode = 0755 +) + +var ( + // update determines if the actual received data should be written to the + // golden files or not. This should be true when you need to update the + // data, but false when actually running the tests. + update = flag.Bool("update", false, "Update golden test file fixture") +) + +type Option func(OptionProcessor) error + +type Tester interface { + Assert(t *testing.T, name string, actualData []byte) + AssertJson(t *testing.T, name string, actualJsonData interface{}) + AssertWithTemplate(t *testing.T, name string, data interface{}, actualData []byte) + Update(name string, actualData []byte) error +} + +// DiffFn takes in an actual and expected and will return a diff string +// representing the differences between the two +type DiffFn func(actual string, expected string) string +type DiffProcessor int + +const ( + UndefinedDiff DiffProcessor = iota + ClassicDiff + ColoredDiff +) + +type OptionProcessor interface { + WithFixtureDir(dir string) error + WithNameSuffix(suffix string) error + WithFilePerms(mode os.FileMode) error + WithDirPerms(mode os.FileMode) error + + WithDiffEngine(engine DiffProcessor) error + WithDiffFn(fn DiffFn) error + WithIgnoreTemplateErrors(ignoreErrors bool) error + WithTestNameForDir(use bool) error + WithSubTestNameForDir(use bool) error +} + +func New(t *testing.T, options ...Option) *goldie { + g := goldie{ + fixtureDir: defaultFixtureDir, + fileNameSuffix: defaultFileNameSuffix, + filePerms: defaultFilePerms, + dirPerms: defaultDirPerms, + } + + var err error + for _, option := range options { + err = option(&g) + if err != nil { + t.Error(fmt.Errorf("Could not apply option: %w", err)) + } + } + + return &g +} + +// === OptionProcessor =============================== + +func WithFixtureDir(dir string) Option { + return func(o OptionProcessor) error { + return o.WithFixtureDir(dir) + } +} + +func WithNameSuffix(suffix string) Option { + return func(o OptionProcessor) error { + return o.WithNameSuffix(suffix) + } +} + +func WithFilePerms(mode os.FileMode) Option { + return func(o OptionProcessor) error { + return o.WithFilePerms(mode) + } +} + +func WithDirPerms(mode os.FileMode) Option { + return func(o OptionProcessor) error { + return o.WithDirPerms(mode) + } +} + +func WithDiffEngine(engine DiffProcessor) Option { + return func(o OptionProcessor) error { + return o.WithDiffEngine(engine) + } +} + +func WithIgnoreTemplateErrors(ignoreErrors bool) Option { + return func(o OptionProcessor) error { + return o.WithIgnoreTemplateErrors(ignoreErrors) + } +} + +func WithTestNameForDir(use bool) Option { + return func(o OptionProcessor) error { + return o.WithTestNameForDir(use) + } +} + +func WithSubTestNameForDir(use bool) Option { + return func(o OptionProcessor) error { + return o.WithSubTestNameForDir(use) + } +} From 0b2ec398db1153c54e107a9bea07b4ca2d2d4149 Mon Sep 17 00:00:00 2001 From: Trevor Gattis Date: Wed, 25 Sep 2019 23:45:13 -0700 Subject: [PATCH 02/11] Ensure struct implements interfaces --- goldie.go | 4 ++++ interface.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/goldie.go b/goldie.go index 7756115..6ba3702 100644 --- a/goldie.go +++ b/goldie.go @@ -25,6 +25,10 @@ import ( "github.com/sergi/go-diff/diffmatchpatch" ) +// Compile time assurance +var _ Tester = &goldie{} +var _ OptionProcessor = &goldie{} + type goldie struct { fixtureDir string fileNameSuffix string diff --git a/interface.go b/interface.go index a5fcb84..3c9f45e 100644 --- a/interface.go +++ b/interface.go @@ -44,7 +44,7 @@ type Tester interface { Assert(t *testing.T, name string, actualData []byte) AssertJson(t *testing.T, name string, actualJsonData interface{}) AssertWithTemplate(t *testing.T, name string, data interface{}, actualData []byte) - Update(name string, actualData []byte) error + Update(t *testing.T, name string, actualData []byte) error } // DiffFn takes in an actual and expected and will return a diff string From ff0152d79160020bdbc2cd48d82dbe471d3766da Mon Sep 17 00:00:00 2001 From: Trevor Gattis Date: Wed, 25 Sep 2019 23:47:57 -0700 Subject: [PATCH 03/11] A better way of enforcing the interface requirement --- goldie.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/goldie.go b/goldie.go index 6ba3702..5524fe9 100644 --- a/goldie.go +++ b/goldie.go @@ -26,8 +26,8 @@ import ( ) // Compile time assurance -var _ Tester = &goldie{} -var _ OptionProcessor = &goldie{} +var _ Tester = (*goldie)(nil) +var _ OptionProcessor = (*goldie)(nil) type goldie struct { fixtureDir string From c841ad4dbb60f7bb01860125753f62c7f723c6dd Mon Sep 17 00:00:00 2001 From: Trevor Gattis Date: Thu, 26 Sep 2019 08:42:53 -0700 Subject: [PATCH 04/11] Added more documentation, renamed a few things, exposed Diff fn --- goldie.go | 35 ++----------- goldie_test.go | 4 +- interface.go | 131 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 114 insertions(+), 56 deletions(-) diff --git a/goldie.go b/goldie.go index 5524fe9..9bd1d48 100644 --- a/goldie.go +++ b/goldie.go @@ -20,9 +20,6 @@ import ( "text/template" "errors" - - "github.com/pmezard/go-difflib/difflib" - "github.com/sergi/go-diff/diffmatchpatch" ) // Compile time assurance @@ -35,7 +32,7 @@ type goldie struct { filePerms os.FileMode dirPerms os.FileMode - diffProcessor DiffProcessor + diffEngine DiffEngine diffFn DiffFn ignoreTemplateErrors bool useTestNameForDir bool @@ -64,8 +61,8 @@ func (g *goldie) WithDirPerms(mode os.FileMode) error { return nil } -func (g *goldie) WithDiffEngine(engine DiffProcessor) error { - g.diffProcessor = engine +func (g *goldie) WithDiffEngine(engine DiffEngine) error { + g.diffEngine = engine return nil } @@ -229,12 +226,12 @@ func (g *goldie) compare(t *testing.T, name string, actualData []byte) error { actual := string(actualData) expected := string(expectedData) - if g.diffFn != nil || g.diffProcessor != UndefinedDiff { + if g.diffFn != nil || g.diffEngine != UndefinedDiff { var d string if g.diffFn != nil { d = g.diffFn(actual, expected) } else { - d = diff(g.diffProcessor, actual, expected) + d = Diff(g.diffEngine, actual, expected) } msg += "Diff is below:\n" + d @@ -251,28 +248,6 @@ func (g *goldie) compare(t *testing.T, name string, actualData []byte) error { return nil } -func diff(engine DiffProcessor, actual string, expected string) string { - var diff string - switch engine { - case ClassicDiff: - diff, _ = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(expected), - B: difflib.SplitLines(actual), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - - case ColoredDiff: - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(actual, expected, false) - diff = dmp.DiffPrettyText(diffs) - } - return diff -} - // compareTemplate is reading the golden fixture file and compare the stored // data with the actual data. func (g *goldie) compareTemplate(t *testing.T, name string, data interface{}, actualData []byte) error { diff --git a/goldie_test.go b/goldie_test.go index 98e809f..0016c07 100644 --- a/goldie_test.go +++ b/goldie_test.go @@ -285,7 +285,7 @@ func TestNormalizeLF(t *testing.T) { func TestDiffEngines(t *testing.T) { type engine struct { - engine DiffProcessor + engine DiffEngine diff string } @@ -313,7 +313,7 @@ func TestDiffEngines(t *testing.T) { for _, tt := range tests { for _, e := range tt.engines { - diff := diff(e.engine, tt.actual, tt.expected) + diff := Diff(e.engine, tt.actual, tt.expected) assert.Equal(t, e.diff, diff) } } diff --git a/interface.go b/interface.go index 3c9f45e..48bf75e 100644 --- a/interface.go +++ b/interface.go @@ -13,6 +13,9 @@ import ( "fmt" "os" "testing" + + "github.com/pmezard/go-difflib/difflib" + "github.com/sergi/go-diff/diffmatchpatch" ) const ( @@ -38,8 +41,11 @@ var ( update = flag.Bool("update", false, "Update golden test file fixture") ) +// Option defines the signature of a functional option method that can +// apply options to an OptionProcessor. type Option func(OptionProcessor) error +// Tester defines the methods that any golden tester should support. type Tester interface { Assert(t *testing.T, name string, actualData []byte) AssertJson(t *testing.T, name string, actualJsonData interface{}) @@ -48,48 +54,49 @@ type Tester interface { } // DiffFn takes in an actual and expected and will return a diff string -// representing the differences between the two +// representing the differences between the two. type DiffFn func(actual string, expected string) string -type DiffProcessor int + +// DiffEngine is used to enumerate the diff engine processors that are available +type DiffEngine int const ( - UndefinedDiff DiffProcessor = iota + // UndefinedDiff represents any undefined diff processor. If a new diff engine + // is implemented, it should be added to this enumeration and to the `diff` helper + // function. + UndefinedDiff DiffEngine = iota + + // ClassicDiff produces a diff similar to what the `diff` tool would produce. + // +++ Actual + // @@ -1 +1 @@ + // -Lorem dolor sit amet. + // +Lorem ipsum dolor. + // ClassicDiff + + // ColoredDiff produces a diff that will use red and green colors to distinguish + // the diffs between the two values. ColoredDiff ) +// OptionProcessor defines the functions that can be called to set values for +// a tester. To expand this list, add a function to this interface and then +// implement the generic option setter below. type OptionProcessor interface { + // WithFixtureDir sets the directory that will be used to store the fixtures. + // Defaults to `testdata`. WithFixtureDir(dir string) error WithNameSuffix(suffix string) error WithFilePerms(mode os.FileMode) error WithDirPerms(mode os.FileMode) error - WithDiffEngine(engine DiffProcessor) error + WithDiffEngine(engine DiffEngine) error WithDiffFn(fn DiffFn) error WithIgnoreTemplateErrors(ignoreErrors bool) error WithTestNameForDir(use bool) error WithSubTestNameForDir(use bool) error } -func New(t *testing.T, options ...Option) *goldie { - g := goldie{ - fixtureDir: defaultFixtureDir, - fileNameSuffix: defaultFileNameSuffix, - filePerms: defaultFilePerms, - dirPerms: defaultDirPerms, - } - - var err error - for _, option := range options { - err = option(&g) - if err != nil { - t.Error(fmt.Errorf("Could not apply option: %w", err)) - } - } - - return &g -} - // === OptionProcessor =============================== func WithFixtureDir(dir string) Option { @@ -98,44 +105,120 @@ func WithFixtureDir(dir string) Option { } } +// WithNameSuffix sets the file suffix to be used for the golden file. +// Defaults to `.golden` func WithNameSuffix(suffix string) Option { return func(o OptionProcessor) error { return o.WithNameSuffix(suffix) } } +// WithFilePerms sets the file permissions on the golden files that +// are created. Defaults to 0644. func WithFilePerms(mode os.FileMode) Option { return func(o OptionProcessor) error { return o.WithFilePerms(mode) } } +// WithDirPerms sets the directory permissions for the directories +// in which the golden files are created. Defaults to 0755. func WithDirPerms(mode os.FileMode) Option { return func(o OptionProcessor) error { return o.WithDirPerms(mode) } } -func WithDiffEngine(engine DiffProcessor) Option { +// WithDiffEngine sets the `diff` engine that will be used to generate +// the `diff` text. +func WithDiffEngine(engine DiffEngine) Option { return func(o OptionProcessor) error { return o.WithDiffEngine(engine) } } +// WithDiffFn sets the `diff` engine to be a function that implements +// the DiffFn signature. This allows for any customized diff logic +// you would like to create. +func WithDiffFn(fn DiffFn) Option { + return func(o OptionProcessor) error { + return o.WithDiffFn(fn) + } +} + +// WithIgnoreTemplateErrors allows template processing to ignore any variables +// in the template that do not have corresponding data values passed in. +// Default value is false. func WithIgnoreTemplateErrors(ignoreErrors bool) Option { return func(o OptionProcessor) error { return o.WithIgnoreTemplateErrors(ignoreErrors) } } +// WithTestNameForDir will create a directory with the test's name +// in the fixture directory to store all the golden files. func WithTestNameForDir(use bool) Option { return func(o OptionProcessor) error { return o.WithTestNameForDir(use) } } +// WithSubTestNameForDir will create a directory with the sub test's name +// to store all the golden files. If WithTestNameForDir is enabled, +// it will be in the test name's directory. Otherwise, it will be in the +// fixture directory. func WithSubTestNameForDir(use bool) Option { return func(o OptionProcessor) error { return o.WithSubTestNameForDir(use) } } + +// === Create new testers ================================== + +// New creates a new golden file tester. If there is an issue +// with applying any of the options, an error will be +// reported and t.FailNow() will be called. +func New(t *testing.T, options ...Option) *goldie { + g := goldie{ + fixtureDir: defaultFixtureDir, + fileNameSuffix: defaultFileNameSuffix, + filePerms: defaultFilePerms, + dirPerms: defaultDirPerms, + } + + var err error + for _, option := range options { + err = option(&g) + if err != nil { + t.Error(fmt.Errorf("Could not apply option: %w", err)) + t.FailNow() + } + } + + return &g +} + +// Diff generates a string that shows the difference between the actual +// and the expected. This method could be called in your own DiffFn +// in case you want to leverage any of the engines defined. +func Diff(engine DiffEngine, actual string, expected string) string { + var diff string + switch engine { + case ClassicDiff: + diff, _ = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(expected), + B: difflib.SplitLines(actual), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + case ColoredDiff: + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, expected, false) + diff = dmp.DiffPrettyText(diffs) + } + return diff +} From fdba170a643f9444c9faba7dcf192a86d84cc1b3 Mon Sep 17 00:00:00 2001 From: Trevor Gattis Date: Thu, 26 Sep 2019 09:01:37 -0700 Subject: [PATCH 05/11] Fix compilation error in the example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b5d084..0111205 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ func TestNewExample(t *testing.T) { goldie.WithTestNameForDir(true), ) - g.Assert(t, "example", "my example data") + g.Assert(t, "example", []byte("my example data")) } ``` From 70956eb1930034757e603bbd5fd13f915c73c38a Mon Sep 17 00:00:00 2001 From: Sebastian Dahlgren Date: Wed, 16 Oct 2019 20:04:56 +0200 Subject: [PATCH 06/11] [refactor] Move initialization near first time use --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0111205..1e29935 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ what is present in the golden test file. ``` func TestExample(t *testing.T) { - g := goldie.New(t) recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "/example", nil) @@ -33,6 +32,7 @@ func TestExample(t *testing.T) { handler := http.HandlerFunc(ExampleHandler) handler.ServeHTTP() + g := goldie.New(t) g.Assert(t, "example", recorder.Body.Bytes()) } ``` @@ -50,8 +50,6 @@ This is a {{ .Type }} file. ### Test ``` func TestTemplateExample(t *testing.T) { - g := goldie.New(t) - recorder := httptest.NewRecorder() req, err := http.NewRequest("POST", "/example/Golden", nil) @@ -66,6 +64,7 @@ func TestTemplateExample(t *testing.T) { Type: "Golden", } + g := goldie.New(t) g.AssertWithTemplate(t, "example", data, recorder.Body.Bytes()) } ``` From b75c64a94590ea070d1d1a27ecee613d336488da Mon Sep 17 00:00:00 2001 From: Sebastian Dahlgren Date: Wed, 16 Oct 2019 20:09:05 +0200 Subject: [PATCH 07/11] [refactor] Align comment widths --- goldie.go | 18 +++++++------- interface.go | 70 +++++++++++++++++++++++++++++----------------------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/goldie.go b/goldie.go index 9bd1d48..a1c70e3 100644 --- a/goldie.go +++ b/goldie.go @@ -1,5 +1,5 @@ -// Package goldie provides test assertions based on golden files. It's typically -// used for testing responses with larger data bodies. +// Package goldie provides test assertions based on golden files. It's +// typically used for testing responses with larger data bodies. // // The concept is straight forward. Valid response data is stored in a "golden // file". The actual response data will be byte compared with the golden file @@ -158,11 +158,11 @@ func normalizeLF(d []byte) []byte { } // Assert compares the actual data received with the expected data in the -// golden files after executing it as a template with data parameter. -// If the update flag is set, it will also update the golden file. -// `name` refers to the name of the test and it should typically be unique -// within the package. Also it should be a valid file name (so keeping to -// `a-z0-9\-\_` is a good idea). +// golden files after executing it as a template with data parameter. If the +// update flag is set, it will also update the golden file. `name` refers to +// the name of the test and it should typically be unique within the package. +// Also it should be a valid file name (so keeping to `a-z0-9\-\_` is a good +// idea). func (g *goldie) AssertWithTemplate(t *testing.T, name string, data interface{}, actualData []byte) { if *update { err := g.Update(t, name, actualData) @@ -197,8 +197,8 @@ func (g *goldie) AssertWithTemplate(t *testing.T, name string, data interface{}, // Update will update the golden fixtures with the received actual data. // -// This method does not need to be called from code, but it's exposed so that it -// can be explicitly called if needed. The more common approach would be to +// This method does not need to be called from code, but it's exposed so that +// it can be explicitly called if needed. The more common approach would be to // update using `go test -update ./...`. func (g *goldie) Update(t *testing.T, name string, actualData []byte) error { if err := g.ensureDir(filepath.Dir(g.goldenFileName(t, name))); err != nil { diff --git a/interface.go b/interface.go index 48bf75e..57b59fa 100644 --- a/interface.go +++ b/interface.go @@ -41,8 +41,8 @@ var ( update = flag.Bool("update", false, "Update golden test file fixture") ) -// Option defines the signature of a functional option method that can -// apply options to an OptionProcessor. +// Option defines the signature of a functional option method that can apply +// options to an OptionProcessor. type Option func(OptionProcessor) error // Tester defines the methods that any golden tester should support. @@ -57,16 +57,18 @@ type Tester interface { // representing the differences between the two. type DiffFn func(actual string, expected string) string -// DiffEngine is used to enumerate the diff engine processors that are available +// DiffEngine is used to enumerate the diff engine processors that are +// available. type DiffEngine int const ( - // UndefinedDiff represents any undefined diff processor. If a new diff engine - // is implemented, it should be added to this enumeration and to the `diff` helper - // function. + // UndefinedDiff represents any undefined diff processor. If a new diff + // engine is implemented, it should be added to this enumeration and to the + // `diff` helper function. UndefinedDiff DiffEngine = iota - // ClassicDiff produces a diff similar to what the `diff` tool would produce. + // ClassicDiff produces a diff similar to what the `diff` tool would + // produce. // +++ Actual // @@ -1 +1 @@ // -Lorem dolor sit amet. @@ -74,8 +76,8 @@ const ( // ClassicDiff - // ColoredDiff produces a diff that will use red and green colors to distinguish - // the diffs between the two values. + // ColoredDiff produces a diff that will use red and green colors to + // distinguish the diffs between the two values. ColoredDiff ) @@ -83,7 +85,9 @@ const ( // a tester. To expand this list, add a function to this interface and then // implement the generic option setter below. type OptionProcessor interface { - // WithFixtureDir sets the directory that will be used to store the fixtures. + // WithFixtureDir sets the directory that will be used to store the + // fixtures. + // // Defaults to `testdata`. WithFixtureDir(dir string) error WithNameSuffix(suffix string) error @@ -106,6 +110,7 @@ func WithFixtureDir(dir string) Option { } // WithNameSuffix sets the file suffix to be used for the golden file. +// // Defaults to `.golden` func WithNameSuffix(suffix string) Option { return func(o OptionProcessor) error { @@ -113,33 +118,37 @@ func WithNameSuffix(suffix string) Option { } } -// WithFilePerms sets the file permissions on the golden files that -// are created. Defaults to 0644. +// WithFilePerms sets the file permissions on the golden files that are +// created. +// +// Defaults to 0644. func WithFilePerms(mode os.FileMode) Option { return func(o OptionProcessor) error { return o.WithFilePerms(mode) } } -// WithDirPerms sets the directory permissions for the directories -// in which the golden files are created. Defaults to 0755. +// WithDirPerms sets the directory permissions for the directories in which the +// golden files are created. +// +// Defaults to 0755. func WithDirPerms(mode os.FileMode) Option { return func(o OptionProcessor) error { return o.WithDirPerms(mode) } } -// WithDiffEngine sets the `diff` engine that will be used to generate -// the `diff` text. +// WithDiffEngine sets the `diff` engine that will be used to generate the +// `diff` text. func WithDiffEngine(engine DiffEngine) Option { return func(o OptionProcessor) error { return o.WithDiffEngine(engine) } } -// WithDiffFn sets the `diff` engine to be a function that implements -// the DiffFn signature. This allows for any customized diff logic -// you would like to create. +// WithDiffFn sets the `diff` engine to be a function that implements the +// DiffFn signature. This allows for any customized diff logic you would like +// to create. func WithDiffFn(fn DiffFn) Option { return func(o OptionProcessor) error { return o.WithDiffFn(fn) @@ -148,6 +157,7 @@ func WithDiffFn(fn DiffFn) Option { // WithIgnoreTemplateErrors allows template processing to ignore any variables // in the template that do not have corresponding data values passed in. +// // Default value is false. func WithIgnoreTemplateErrors(ignoreErrors bool) Option { return func(o OptionProcessor) error { @@ -155,18 +165,17 @@ func WithIgnoreTemplateErrors(ignoreErrors bool) Option { } } -// WithTestNameForDir will create a directory with the test's name -// in the fixture directory to store all the golden files. +// WithTestNameForDir will create a directory with the test's name in the +// fixture directory to store all the golden files. func WithTestNameForDir(use bool) Option { return func(o OptionProcessor) error { return o.WithTestNameForDir(use) } } -// WithSubTestNameForDir will create a directory with the sub test's name -// to store all the golden files. If WithTestNameForDir is enabled, -// it will be in the test name's directory. Otherwise, it will be in the -// fixture directory. +// WithSubTestNameForDir will create a directory with the sub test's name to +// store all the golden files. If WithTestNameForDir is enabled, it will be in +// the test name's directory. Otherwise, it will be in the fixture directory. func WithSubTestNameForDir(use bool) Option { return func(o OptionProcessor) error { return o.WithSubTestNameForDir(use) @@ -175,9 +184,8 @@ func WithSubTestNameForDir(use bool) Option { // === Create new testers ================================== -// New creates a new golden file tester. If there is an issue -// with applying any of the options, an error will be -// reported and t.FailNow() will be called. +// New creates a new golden file tester. If there is an issue with applying any +// of the options, an error will be reported and t.FailNow() will be called. func New(t *testing.T, options ...Option) *goldie { g := goldie{ fixtureDir: defaultFixtureDir, @@ -198,9 +206,9 @@ func New(t *testing.T, options ...Option) *goldie { return &g } -// Diff generates a string that shows the difference between the actual -// and the expected. This method could be called in your own DiffFn -// in case you want to leverage any of the engines defined. +// Diff generates a string that shows the difference between the actual and the +// expected. This method could be called in your own DiffFn in case you want +// to leverage any of the engines defined. func Diff(engine DiffEngine, actual string, expected string) string { var diff string switch engine { From 5e07d7bda4ec7b7315c4b821802a023e369b06f0 Mon Sep 17 00:00:00 2001 From: Sebastian Dahlgren Date: Wed, 16 Oct 2019 20:12:21 +0200 Subject: [PATCH 08/11] [refactor] Remove duplicate package description This description is not needed as it's already defined in goldie.go --- interface.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/interface.go b/interface.go index 57b59fa..179d75c 100644 --- a/interface.go +++ b/interface.go @@ -1,11 +1,3 @@ -// Package goldie provides test assertions based on golden files. It's typically -// used for testing responses with larger data bodies. -// -// The concept is straight forward. Valid response data is stored in a "golden -// file". The actual response data will be byte compared with the golden file -// and the test will fail if there is a difference. -// -// Updating the golden file can be done by running `go test -update ./...`. package goldie import ( From 29774f3b5886bc8f724af53ef7f15bc093f7295b Mon Sep 17 00:00:00 2001 From: Sebastian Dahlgren Date: Wed, 16 Oct 2019 20:15:09 +0200 Subject: [PATCH 09/11] [refactor] Add missing function/method comments --- goldie.go | 28 ++++++++++++++++++++++++++++ interface.go | 7 +++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/goldie.go b/goldie.go index a1c70e3..b2beb4a 100644 --- a/goldie.go +++ b/goldie.go @@ -41,46 +41,74 @@ type goldie struct { // === OptionProcessor =============================== +// WithFixtureDir sets the fixture directory. +// +// Defaults to `testdata` func (g *goldie) WithFixtureDir(dir string) error { g.fixtureDir = dir return nil } +// WithNameSuffix sets the file suffix to be used for the golden file. +// +// Defaults to `.golden` func (g *goldie) WithNameSuffix(suffix string) error { g.fileNameSuffix = suffix return nil } +// WithFilePerms sets the file permissions on the golden files that are +// created. +// +// Defaults to 0644. func (g *goldie) WithFilePerms(mode os.FileMode) error { g.filePerms = mode return nil } +// WithDirPerms sets the directory permissions for the directories in which the +// golden files are created. +// +// Defaults to 0755. func (g *goldie) WithDirPerms(mode os.FileMode) error { g.dirPerms = mode return nil } +// WithDiffEngine sets the `diff` engine that will be used to generate the +// `diff` text. func (g *goldie) WithDiffEngine(engine DiffEngine) error { g.diffEngine = engine return nil } +// WithDiffFn sets the `diff` engine to be a function that implements the +// DiffFn signature. This allows for any customized diff logic you would like +// to create. func (g *goldie) WithDiffFn(fn DiffFn) error { g.diffFn = fn return nil } +// WithIgnoreTemplateErrors allows template processing to ignore any variables +// in the template that do not have corresponding data values passed in. +// +// Default value is false. func (g *goldie) WithIgnoreTemplateErrors(ignoreErrors bool) error { g.ignoreTemplateErrors = ignoreErrors return nil } +// WithTestNameForDir will create a directory with the test's name in the +// fixture directory to store all the golden files. func (g *goldie) WithTestNameForDir(use bool) error { g.useTestNameForDir = use return nil } +// WithSubTestNameForDir will create a directory with the sub test's name to +// store all the golden files. If WithTestNameForDir is enabled, it will be in +// the test name's directory. Otherwise, it will be in the fixture directory. func (g *goldie) WithSubTestNameForDir(use bool) error { g.useSubTestNameForDir = use return nil diff --git a/interface.go b/interface.go index 179d75c..ff9791b 100644 --- a/interface.go +++ b/interface.go @@ -77,10 +77,6 @@ const ( // a tester. To expand this list, add a function to this interface and then // implement the generic option setter below. type OptionProcessor interface { - // WithFixtureDir sets the directory that will be used to store the - // fixtures. - // - // Defaults to `testdata`. WithFixtureDir(dir string) error WithNameSuffix(suffix string) error WithFilePerms(mode os.FileMode) error @@ -95,6 +91,9 @@ type OptionProcessor interface { // === OptionProcessor =============================== +// WithFixtureDir sets the fixture directory. +// +// Defaults to `testdata` func WithFixtureDir(dir string) Option { return func(o OptionProcessor) error { return o.WithFixtureDir(dir) From 72925e9a0f1c99dffd353b59f65c1f5baa57629f Mon Sep 17 00:00:00 2001 From: Sebastian Dahlgren Date: Wed, 16 Oct 2019 20:21:24 +0200 Subject: [PATCH 10/11] [refactor] Co-loacte non-interface related functions This makes the interface.go file more purposeful and also ties similar logic together in goldie.go --- goldie.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++++-- interface.go | 79 ++-------------------------------------------------- 2 files changed, 78 insertions(+), 79 deletions(-) diff --git a/goldie.go b/goldie.go index b2beb4a..847e099 100644 --- a/goldie.go +++ b/goldie.go @@ -11,6 +11,7 @@ package goldie import ( "bytes" "encoding/json" + "flag" "fmt" "io/ioutil" "os" @@ -20,11 +21,33 @@ import ( "text/template" "errors" + + "github.com/pmezard/go-difflib/difflib" + "github.com/sergi/go-diff/diffmatchpatch" +) + +const ( + // FixtureDir is the folder name for where the fixtures are stored. It's + // relative to the "go test" path. + defaultFixtureDir = "testdata" + + // FileNameSuffix is the suffix appended to the fixtures. Set to empty + // string to disable file name suffixes. + defaultFileNameSuffix = ".golden" + + // FilePerms is used to set the permissions on the golden fixture files. + defaultFilePerms os.FileMode = 0644 + + // DirPerms is used to set the permissions on the golden fixture folder. + defaultDirPerms os.FileMode = 0755 ) -// Compile time assurance -var _ Tester = (*goldie)(nil) -var _ OptionProcessor = (*goldie)(nil) +var ( + // update determines if the actual received data should be written to the + // golden files or not. This should be true when you need to update the + // data, but false when actually running the tests. + update = flag.Bool("update", false, "Update golden test file fixture") +) type goldie struct { fixtureDir string @@ -39,6 +62,55 @@ type goldie struct { useSubTestNameForDir bool } +// === Create new testers ================================== + +// New creates a new golden file tester. If there is an issue with applying any +// of the options, an error will be reported and t.FailNow() will be called. +func New(t *testing.T, options ...Option) *goldie { + g := goldie{ + fixtureDir: defaultFixtureDir, + fileNameSuffix: defaultFileNameSuffix, + filePerms: defaultFilePerms, + dirPerms: defaultDirPerms, + } + + var err error + for _, option := range options { + err = option(&g) + if err != nil { + t.Error(fmt.Errorf("Could not apply option: %w", err)) + t.FailNow() + } + } + + return &g +} + +// Diff generates a string that shows the difference between the actual and the +// expected. This method could be called in your own DiffFn in case you want +// to leverage any of the engines defined. +func Diff(engine DiffEngine, actual string, expected string) string { + var diff string + switch engine { + case ClassicDiff: + diff, _ = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(expected), + B: difflib.SplitLines(actual), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + case ColoredDiff: + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, expected, false) + diff = dmp.DiffPrettyText(diffs) + } + return diff +} + // === OptionProcessor =============================== // WithFixtureDir sets the fixture directory. diff --git a/interface.go b/interface.go index ff9791b..e21f017 100644 --- a/interface.go +++ b/interface.go @@ -1,37 +1,13 @@ package goldie import ( - "flag" - "fmt" "os" "testing" - - "github.com/pmezard/go-difflib/difflib" - "github.com/sergi/go-diff/diffmatchpatch" ) -const ( - // FixtureDir is the folder name for where the fixtures are stored. It's - // relative to the "go test" path. - defaultFixtureDir = "testdata" - - // FileNameSuffix is the suffix appended to the fixtures. Set to empty - // string to disable file name suffixes. - defaultFileNameSuffix = ".golden" - - // FilePerms is used to set the permissions on the golden fixture files. - defaultFilePerms os.FileMode = 0644 - - // DirPerms is used to set the permissions on the golden fixture folder. - defaultDirPerms os.FileMode = 0755 -) - -var ( - // update determines if the actual received data should be written to the - // golden files or not. This should be true when you need to update the - // data, but false when actually running the tests. - update = flag.Bool("update", false, "Update golden test file fixture") -) +// Compile time assurance +var _ Tester = (*goldie)(nil) +var _ OptionProcessor = (*goldie)(nil) // Option defines the signature of a functional option method that can apply // options to an OptionProcessor. @@ -172,52 +148,3 @@ func WithSubTestNameForDir(use bool) Option { return o.WithSubTestNameForDir(use) } } - -// === Create new testers ================================== - -// New creates a new golden file tester. If there is an issue with applying any -// of the options, an error will be reported and t.FailNow() will be called. -func New(t *testing.T, options ...Option) *goldie { - g := goldie{ - fixtureDir: defaultFixtureDir, - fileNameSuffix: defaultFileNameSuffix, - filePerms: defaultFilePerms, - dirPerms: defaultDirPerms, - } - - var err error - for _, option := range options { - err = option(&g) - if err != nil { - t.Error(fmt.Errorf("Could not apply option: %w", err)) - t.FailNow() - } - } - - return &g -} - -// Diff generates a string that shows the difference between the actual and the -// expected. This method could be called in your own DiffFn in case you want -// to leverage any of the engines defined. -func Diff(engine DiffEngine, actual string, expected string) string { - var diff string - switch engine { - case ClassicDiff: - diff, _ = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(expected), - B: difflib.SplitLines(actual), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) - - case ColoredDiff: - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(actual, expected, false) - diff = dmp.DiffPrettyText(diffs) - } - return diff -} From b738de7a53b4ba5b4d6cba9f022c0eb753c0af0d Mon Sep 17 00:00:00 2001 From: Sebastian Dahlgren Date: Wed, 16 Oct 2019 20:24:42 +0200 Subject: [PATCH 11/11] [refactor] Add some more air to the code This is done to enhance readability. --- goldie.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/goldie.go b/goldie.go index 847e099..6ec2a38 100644 --- a/goldie.go +++ b/goldie.go @@ -91,6 +91,7 @@ func New(t *testing.T, options ...Option) *goldie { // to leverage any of the engines defined. func Diff(engine DiffEngine, actual string, expected string) string { var diff string + switch engine { case ClassicDiff: diff, _ = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ @@ -108,6 +109,7 @@ func Diff(engine DiffEngine, actual string, expected string) string { diffs := dmp.DiffMain(actual, expected, false) diff = dmp.DiffPrettyText(diffs) } + return diff }