From e11f263b077a5e1701bc7e70f28deedfe0c59adb Mon Sep 17 00:00:00 2001 From: gkampitakis Date: Tue, 16 May 2023 22:56:09 +0100 Subject: [PATCH 1/3] feat: support configurable snapshot dir and filename --- snaps/matchJSON.go | 29 ++++++++++++++++++++++- snaps/matchSnapshot.go | 52 +++++++++++++++++++++++++++++++----------- snaps/snapshot.go | 51 +++++++++++++++++++++++++++++++++++++---- snaps/snapshot_test.go | 2 +- snaps/utils.go | 4 +++- 5 files changed, 117 insertions(+), 21 deletions(-) diff --git a/snaps/matchJSON.go b/snaps/matchJSON.go index 2044221..35537cf 100644 --- a/snaps/matchJSON.go +++ b/snaps/matchJSON.go @@ -20,6 +20,26 @@ var ( errInvalidJSON = errors.New("invalid json") ) +/* +MatchJSON verifies the input matches the most recent snap file. +Input can be a valid json string or []byte or whatever value can be passed +successfully on `json.Marshal`. + + MatchJSON(t, `{"user":"mock-user","age":10,"email":"mock@email.com"}`) + MatchJSON(t, []byte(`{"user":"mock-user","age":10,"email":"mock@email.com"}`)) + MatchJSON(t, User{10, "mock-email"}) + +MatchJSON also supports passing matchers as a third argument. Those matchers can act either as +validators or placeholders for data that might change on each invocation e.g. dates. + + MatchJSON(t, User{created: time.Now(), email: "mock-email"}, match.Any("created")) +*/ +func (c *config) MatchJSON(t testingT, input interface{}, matchers ...match.JSONMatcher) { + t.Helper() + + matchJSON(c, t, input, matchers...) +} + /* MatchJSON verifies the input matches the most recent snap file. Input can be a valid json string or []byte or whatever value can be passed @@ -36,7 +56,14 @@ validators or placeholders for data that might change on each invocation e.g. da */ func MatchJSON(t testingT, input interface{}, matchers ...match.JSONMatcher) { t.Helper() - dir, snapPath := snapDirAndName() + + matchJSON(&defaultConfig, t, input, matchers...) +} + +func matchJSON(c *config, t testingT, input interface{}, matchers ...match.JSONMatcher) { + t.Helper() + + dir, snapPath := snapDirAndName(c) testID := testsRegistry.getTestID(t.Name(), snapPath) j, err := validateJSON(input) diff --git a/snaps/matchSnapshot.go b/snaps/matchSnapshot.go index a0f62f0..cc4fc2d 100644 --- a/snaps/matchSnapshot.go +++ b/snaps/matchSnapshot.go @@ -7,27 +7,53 @@ import ( "github.com/kr/pretty" ) -// MatchSnapshot verifies the values match the most recent snap file -// -// You can pass multiple values -// -// MatchSnapshot(t, 10, "hello world") -// -// or call MatchSnapshot multiples times inside a test -// -// MatchSnapshot(t, 10) -// MatchSnapshot(t, "hello world") -// -// The difference is the latter will create multiple entries. +/* +MatchSnapshot verifies the values match the most recent snap file +You can pass multiple values + + MatchSnapshot(t, 10, "hello world") + +or call MatchSnapshot multiples times inside a test + + MatchSnapshot(t, 10) + MatchSnapshot(t, "hello world") + +The difference is the latter will create multiple entries. +*/ +func (c *config) MatchSnapshot(t testingT, values ...interface{}) { + t.Helper() + + matchSnapshot(c, t, values...) +} + +/* +MatchSnapshot verifies the values match the most recent snap file +You can pass multiple values + + MatchSnapshot(t, 10, "hello world") + +or call MatchSnapshot multiples times inside a test + + MatchSnapshot(t, 10) + MatchSnapshot(t, "hello world") + +The difference is the latter will create multiple entries. +*/ func MatchSnapshot(t testingT, values ...interface{}) { t.Helper() + matchSnapshot(&defaultConfig, t, values...) +} + +func matchSnapshot(c *config, t testingT, values ...interface{}) { + t.Helper() + if len(values) == 0 { t.Log(colors.Sprint(colors.Yellow, "[warning] MatchSnapshot call without params\n")) return } - dir, snapPath := snapDirAndName() + dir, snapPath := snapDirAndName(c) testID := testsRegistry.getTestID(t.Name(), snapPath) snapshot := takeSnapshot(values) prevSnapshot, err := getPrevSnapshot(testID, snapPath) diff --git a/snaps/snapshot.go b/snaps/snapshot.go index 0e87d00..e492faf 100644 --- a/snaps/snapshot.go +++ b/snaps/snapshot.go @@ -21,6 +21,44 @@ var ( updatedMsg = colors.Sprint(colors.Green, updateSymbol+"Snapshot updated") ) +type config struct { + filename string + snapsDir string +} + +// Specify folder name where snapshots are stored +// +// default: __snapshots__ +// +// this doesn't change the file extension +func Filename(name string) func(*config) { + return func(c *config) { + c.filename = name + } +} + +// Specify folder name where snapshots are stored +// +// default: __snapshots__ +func Dir(dir string) func(*config) { + return func(c *config) { + c.snapsDir = dir + } +} + +// Create snaps with configuration +// +// e.g WithConfig(Filename("my_test")).MatchSnapshot(t, "hello world") +func WithConfig(args ...func(*config)) *config { + s := defaultConfig + + for _, arg := range args { + arg(&s) + } + + return &s +} + func handleError(t testingT, err interface{}) { t.Helper() t.Error(err) @@ -136,14 +174,17 @@ func updateSnapshot(testID, snapshot, snapPath string) error { Returns the dir for snapshots [where the tests run + /snapsDirName] and the name [dir + /snapsDirName + /.snapsExtName] */ -func snapDirAndName() (dir, name string) { +func snapDirAndName(c *config) (string, string) { callerPath := baseCaller(2) base := filepath.Base(callerPath) + dir := filepath.Join(filepath.Dir(callerPath), c.snapsDir) + filename := c.filename + if filename == "" { + filename = strings.TrimSuffix(base, filepath.Ext(base)) + } + name := filepath.Join(dir, filename+snapsExt) - dir = filepath.Join(filepath.Dir(callerPath), snapsDir) - name = filepath.Join(dir, strings.TrimSuffix(base, filepath.Ext(base))+snapsExt) - - return + return dir, name } // Matches a specific testID diff --git a/snaps/snapshot_test.go b/snaps/snapshot_test.go index bf5a23a..94af4fa 100644 --- a/snaps/snapshot_test.go +++ b/snaps/snapshot_test.go @@ -134,7 +134,7 @@ func TestSnapPathAndFile(t *testing.T) { func() { // This is for emulating being called from a func so we can find the correct file // of the caller - path, file = snapDirAndName() + path, file = snapDirAndName(&defaultConfig) }() test.Contains(t, path, filepath.FromSlash("/snaps/__snapshots__")) diff --git a/snaps/utils.go b/snaps/utils.go index e607c36..aa6b4dd 100644 --- a/snaps/utils.go +++ b/snaps/utils.go @@ -17,6 +17,9 @@ var ( envVar = os.Getenv("UPDATE_SNAPS") shouldUpdate = envVar == "true" && !isCI shouldClean = shouldUpdate || envVar == "clean" && !isCI + defaultConfig = config{ + snapsDir: "__snapshots__", + } ) const ( @@ -28,7 +31,6 @@ const ( skipSymbol = "⟳ " enterSymbol = "↳ " - snapsDir = "__snapshots__" snapsExt = ".snap" ) From a5af4ccdb029abcaab1cf04534d54d2f9cb3ba0f Mon Sep 17 00:00:00 2001 From: gkampitakis Date: Fri, 19 May 2023 22:45:40 +0100 Subject: [PATCH 2/3] fix: add absolute path on snapsDir --- snaps/snapshot.go | 27 ++++++++++++---- snaps/snapshot_test.go | 73 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/snaps/snapshot.go b/snaps/snapshot.go index e492faf..402fe39 100644 --- a/snaps/snapshot.go +++ b/snaps/snapshot.go @@ -40,6 +40,8 @@ func Filename(name string) func(*config) { // Specify folder name where snapshots are stored // // default: __snapshots__ +// +// Accepts absolute paths func Dir(dir string) func(*config) { return func(c *config) { c.snapsDir = dir @@ -171,20 +173,31 @@ func updateSnapshot(testID, snapshot, snapPath string) error { } /* -Returns the dir for snapshots [where the tests run + /snapsDirName] -and the name [dir + /snapsDirName + /.snapsExtName] +Returns the dir for snapshots + - if no config provided returns the directory where tests are running + - if snapsDir is relative path just gets appended to directory where tests are running + - if snapsDir is absolute path then we are returning this path + +Returns the filename + - if no config provided we use the test file name with `.snap` extension + - if filename provided we return the filename with `.snap` extension */ func snapDirAndName(c *config) (string, string) { - callerPath := baseCaller(2) - base := filepath.Base(callerPath) - dir := filepath.Join(filepath.Dir(callerPath), c.snapsDir) + // skips current func, the wrapper match* and the exported Match* func + callerPath := baseCaller(3) + + dir := c.snapsDir + if !filepath.IsAbs(dir) { + dir = filepath.Join(filepath.Dir(callerPath), c.snapsDir) + } + filename := c.filename if filename == "" { + base := filepath.Base(callerPath) filename = strings.TrimSuffix(base, filepath.Ext(base)) } - name := filepath.Join(dir, filename+snapsExt) - return dir, name + return dir, filepath.Join(dir, filename+snapsExt) } // Matches a specific testID diff --git a/snaps/snapshot_test.go b/snaps/snapshot_test.go index 94af4fa..85ae481 100644 --- a/snaps/snapshot_test.go +++ b/snaps/snapshot_test.go @@ -126,19 +126,66 @@ func TestAddNewSnapshot(t *testing.T) { } func TestSnapPathAndFile(t *testing.T) { - var ( - path string - file string - ) - - func() { - // This is for emulating being called from a func so we can find the correct file - // of the caller - path, file = snapDirAndName(&defaultConfig) - }() - - test.Contains(t, path, filepath.FromSlash("/snaps/__snapshots__")) - test.Contains(t, file, filepath.FromSlash("/snaps/__snapshots__/snapshot_test.snap")) + t.Run("should return default path and file", func(t *testing.T) { + var ( + dir string + name string + ) + + func() { + // This is for emulating being called from a func so we can find the correct file + // of the caller + func() { + dir, name = snapDirAndName(&defaultConfig) + }() + }() + + test.Contains(t, dir, filepath.FromSlash("/snaps/__snapshots__")) + test.Contains(t, name, filepath.FromSlash("/snaps/__snapshots__/snapshot_test.snap")) + }) + + t.Run("should return path and file from config", func(t *testing.T) { + var ( + dir string + name string + ) + + func() { + // This is for emulating being called from a func so we can find the correct file + // of the caller + func() { + dir, name = snapDirAndName(&config{ + filename: "my_file", + snapsDir: "my_snapshot_dir", + }) + }() + }() + + // returns the current file's path /snaps/* + test.Contains(t, dir, filepath.FromSlash("/snaps/my_snapshot_dir")) + test.Contains(t, name, filepath.FromSlash("/snaps/my_snapshot_dir/my_file.snap")) + }) + + t.Run("should return absolute path", func(t *testing.T) { + var ( + dir string + name string + ) + + func() { + // This is for emulating being called from a func so we can find the correct file + // of the caller + func() { + dir, name = snapDirAndName(&config{ + filename: "my_file", + snapsDir: "/path_to/my_snapshot_dir", + }) + }() + }() + + test.Contains(t, dir, filepath.FromSlash("/path_to/my_snapshot_dir")) + test.Contains(t, name, filepath.FromSlash("/path_to/my_snapshot_dir/my_file.snap")) + }) } func TestUpdateSnapshot(t *testing.T) { From 05183ab1312af7a23659970b8a8ad6a2243e826c Mon Sep 17 00:00:00 2001 From: gkampitakis Date: Mon, 22 May 2023 21:25:43 +0100 Subject: [PATCH 3/3] fix: add examples and docs --- README.md | 56 +++++++++++++------ examples/__snapshots__/custom_file.snap | 4 ++ .../__snapshots__/matchSnapshot_test.snap | 4 ++ .../absolute_path/matchSnapshot_test.snap | 4 ++ examples/matchSnapshot_test.go | 28 ++++++++++ examples/special_data/different_name.snap | 4 ++ examples/testdata/matchSnapshot_test.snap | 10 ++++ 7 files changed, 93 insertions(+), 17 deletions(-) create mode 100755 examples/__snapshots__/custom_file.snap create mode 100755 examples/absolute_path/matchSnapshot_test.snap create mode 100755 examples/special_data/different_name.snap create mode 100755 examples/testdata/matchSnapshot_test.snap diff --git a/README.md b/README.md index 991bab3..b3794df 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ App Preview New

- > matchJSON and matchers are still under development that means their API can change. Use with caution and please share feedback for improvements. ## Highlights @@ -30,8 +29,9 @@ - [MatchSnapshot](#matchsnapshot) - [MatchJSON](#matchjson) - [Matchers](#matchers) - - [match.Any](#matchany) - - [match.Custom](#matchcustom) + - [match.Any](#matchany) + - [match.Custom](#matchcustom) +- [Configuration](#configuration) - [Update Snapshots](#update-snapshots) - [Clean obsolete Snapshots](#clean-obsolete-snapshots) - [Skipping Tests](#skipping-tests) @@ -92,7 +92,6 @@ name is the test file name with extension `.snap`. So for example if your test is called `test_simple.go` when you run your tests, a snapshot file will be created at `./__snapshots__/test_simple.snaps`. - ## MatchJSON `MatchJSON` can be used to capture data that can represent a valid json. @@ -120,7 +119,7 @@ JSON will be saved in snapshot in pretty format for more readability and determi `MatchJSON`'s third argument can accept a list of matchers. Matchers are functions that can act as property matchers and test values. -You can pass a path of the property you want to match and test. +You can pass a path of the property you want to match and test. The path syntax is a series of keys separated by a dot. The dot and colon can be escaped with `\`. @@ -131,7 +130,7 @@ Currently `go-snaps` has two build in matchers #### match.Any -Any matcher acts as a placeholder for any value. It replaces any targeted path with a +Any matcher acts as a placeholder for any value. It replaces any targeted path with a placeholder string. ```go @@ -186,6 +185,28 @@ match.Custom("path",myFunc). You can see more [examples](./examples/matchJSON_test.go#L93). +## Configuration + +`go-snaps` allows passing configuration for overriding + +- the directory where snapshots are stored, _relative or absolute path_ +- the filename where snapshots are stored + +```go +t.Run("snapshot tests", func(t *testing.T) { + snaps.WithConfig(snaps.Filename("my_custom_name"), snaps.Dir("my_dir")).MatchSnapshot(t, "Hello Word") + + s := snaps.WithConfig( + snaps.Dir("my_dir"), + snaps.Filename("json_file"), + ) + + s.MatchJSON(t, `{"hello":"world"}`) +}) +``` + +You can see more on [examples](/examples/matchSnapshot_test.go#L67) + ## Update Snapshots You can update your failing snapshots by setting `UPDATE_SNAPS` env variable to true. @@ -198,6 +219,7 @@ If you don't want to update all failing snapshots, or you want to update one of them you can you use the `-run` flag to target the test/s you want. For more information for `go test` flags you can run + ```go go help testflag ``` @@ -211,15 +233,15 @@ go help testflag `go-snaps` can identify obsolete snapshots. -In order to enable this functionality you need to use the `TestMain(t*testing.M)` -and call `snaps.Clean(t)`. This will also print a **Snapshot Summary**. (if running tests +In order to enable this functionality you need to use the `TestMain(t*testing.M)` +and call `snaps.Clean(t)`. This will also print a **Snapshot Summary**. (if running tests with verbose flag `-v`) -If you want to remove the obsolete snap files and snapshots you can run +If you want to remove the obsolete snap files and snapshots you can run tests with `UPDATE_SNAPS=true` env variable. -The reason for using `TestMain`, is because `go-snaps` needs to be sure that all tests -are finished so it can keep track which snapshots were not called. +The reason for using `TestMain`, is because `go-snaps` needs to be sure that all tests +are finished so it can keep track which snapshots were not called. **Example:** @@ -239,10 +261,10 @@ For more information around [TestMain](https://pkg.go.dev/testing#hdr-Main). ### Skipping Tests If you want to skip one test using `t.Skip`, `go-snaps` can't keep track -if the test was skipped or if it was removed. For that reason `go-snaps` exposes +if the test was skipped or if it was removed. For that reason `go-snaps` exposes a wrapper for `t.Skip`, `t.Skipf` and `t.SkipNow`, which keep tracks of skipped files. -You can skip, or only run specific tests by using the `-run` flag. `go-snaps` +You can skip, or only run specific tests by using the `-run` flag. `go-snaps` can identify which tests are being skipped and parse only the relevant tests for obsolete snapshots. @@ -259,7 +281,7 @@ For more information around [NO_COLOR](https://no-color.org). ## Snapshots Structure -Snapshots have the form +Snapshots have the form ```text [ TestName - Number ] @@ -294,12 +316,12 @@ This library used [Jest Snapshoting](https://jestjs.io/docs/snapshot-testing) an ## Notes 1. ⚠️ When running a specific test file by specifying a path -`go test ./my_test.go`, `go-snaps` can't track the path so it will mistakenly mark snapshots as obsolete. + `go test ./my_test.go`, `go-snaps` can't track the path so it will mistakenly mark snapshots as obsolete. 2. The order in which tests are written might not be the same order that snapshots are saved in the file. 3. If your snapshot data contain the termination characters `---` at the start of a line -and after a new line, `go-snaps` will "escape" them and save them as `/-/-/-/`. This -should not cause any diff issues (false-positives). + and after a new line, `go-snaps` will "escape" them and save them as `/-/-/-/`. This + should not cause any diff issues (false-positives). 4. Snapshots should be treated as code. The snapshot artifact should be committed alongside code changes, and reviewed as part of your code review process diff --git a/examples/__snapshots__/custom_file.snap b/examples/__snapshots__/custom_file.snap new file mode 100755 index 0000000..abc717f --- /dev/null +++ b/examples/__snapshots__/custom_file.snap @@ -0,0 +1,4 @@ + +[TestMatchSnapshot/withConfig/should_allow_changing_filename - 1] +snapshot data +--- diff --git a/examples/__snapshots__/matchSnapshot_test.snap b/examples/__snapshots__/matchSnapshot_test.snap index 9296a57..4c6702b 100755 --- a/examples/__snapshots__/matchSnapshot_test.snap +++ b/examples/__snapshots__/matchSnapshot_test.snap @@ -68,3 +68,7 @@ map[string]interface {}{ lastRead: 0, } --- + +[TestMatchSnapshot/withConfig - 1] +this should use the default config +--- diff --git a/examples/absolute_path/matchSnapshot_test.snap b/examples/absolute_path/matchSnapshot_test.snap new file mode 100755 index 0000000..bc22fdb --- /dev/null +++ b/examples/absolute_path/matchSnapshot_test.snap @@ -0,0 +1,4 @@ + +[TestMatchSnapshot/withConfig/should_allow_absolute_path - 1] +supporting absolute path +--- diff --git a/examples/matchSnapshot_test.go b/examples/matchSnapshot_test.go index a64b1e6..2e6d36c 100644 --- a/examples/matchSnapshot_test.go +++ b/examples/matchSnapshot_test.go @@ -3,6 +3,8 @@ package examples import ( "bytes" "os" + "path/filepath" + "runtime" "testing" "github.com/gkampitakis/go-snaps/snaps" @@ -61,6 +63,32 @@ func TestMatchSnapshot(t *testing.T) { t.Run(".*", func(t *testing.T) { snaps.MatchSnapshot(t, "ignore regex patterns on names") }) + + t.Run("withConfig", func(t *testing.T) { + t.Run("should allow changing filename", func(t *testing.T) { + snaps.WithConfig( + snaps.Filename("custom_file"), + ).MatchSnapshot(t, "snapshot data") + }) + + t.Run("should allow changing dir", func(t *testing.T) { + s := snaps.WithConfig(snaps.Dir("testdata")) + s.MatchSnapshot(t, "snapshot with different dir name") + s.MatchSnapshot(t, "another one", 1, 10) + }) + + t.Run("should allow absolute path", func(t *testing.T) { + _, b, _, _ := runtime.Caller(0) + basepath := filepath.Dir(b) + + snaps.WithConfig(snaps.Dir(basepath+"/absolute_path")). + MatchSnapshot(t, "supporting absolute path") + }) + + s := snaps.WithConfig(snaps.Dir("special_data"), snaps.Filename("different_name")) + s.MatchSnapshot(t, "different data than the rest") + snaps.MatchSnapshot(t, "this should use the default config") + }) } func TestSimpleTable(t *testing.T) { diff --git a/examples/special_data/different_name.snap b/examples/special_data/different_name.snap new file mode 100755 index 0000000..5aa4660 --- /dev/null +++ b/examples/special_data/different_name.snap @@ -0,0 +1,4 @@ + +[TestMatchSnapshot/withConfig - 1] +different data than the rest +--- diff --git a/examples/testdata/matchSnapshot_test.snap b/examples/testdata/matchSnapshot_test.snap new file mode 100755 index 0000000..16842dc --- /dev/null +++ b/examples/testdata/matchSnapshot_test.snap @@ -0,0 +1,10 @@ + +[TestMatchSnapshot/withConfig/should_allow_changing_dir - 1] +snapshot with different dir name +--- + +[TestMatchSnapshot/withConfig/should_allow_changing_dir - 2] +another one +int(1) +int(10) +---