diff --git a/README.md b/README.md index b3794df..be31f62 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ - [Matchers](#matchers) - [match.Any](#matchany) - [match.Custom](#matchcustom) + - [match.Type\[ExpectedType\]](#matchtype) - [Configuration](#configuration) - [Update Snapshots](#update-snapshots) - [Clean obsolete Snapshots](#clean-obsolete-snapshots) @@ -127,6 +128,7 @@ Currently `go-snaps` has two build in matchers - `match.Any` - `match.Custom` +- `match.Type[ExpectedType]` #### match.Any @@ -183,7 +185,24 @@ match.Custom("path",myFunc). ErrOnMissingPath(bool) // determines whether the matcher will err in case of a missing path, default true ``` -You can see more [examples](./examples/matchJSON_test.go#L93). +#### match.Type + +Type matcher evaluates types that are passed in a snapshot and it replaces any targeted path with a placeholder in the form of ``. + +```go +match.Type[string]("user.info") +// or with multiple paths +match.Type[float64]("user.age", "data.items") +``` + +Type matcher provides a method for setting an option + +```go +match.Type[string]("user.info"). + ErrOnMissingPath(bool) // determines whether the matcher will err in case of a missing path, default true +``` + +You can see more [examples](./examples/matchJSON_test.go#L96). ## Configuration diff --git a/examples/__snapshots__/matchJSON_test.snap b/examples/__snapshots__/matchJSON_test.snap index 271dfef..4d854a3 100755 --- a/examples/__snapshots__/matchJSON_test.snap +++ b/examples/__snapshots__/matchJSON_test.snap @@ -88,3 +88,15 @@ } } --- + +[TestMatchers/type_matcher - 1] +{ + "data": "" +} +--- + +[TestMatchers/type_matcher - 2] +{ + "metadata": "" +} +--- diff --git a/examples/matchJSON_test.go b/examples/matchJSON_test.go index 1ee579d..c66bb01 100644 --- a/examples/matchJSON_test.go +++ b/examples/matchJSON_test.go @@ -178,4 +178,13 @@ func TestMatchers(t *testing.T) { snaps.MatchJSON(t, body, match.Any("data.createdAt")) }) + + t.Run("type matcher", func(t *testing.T) { + snaps.MatchJSON(t, `{"data":10}`, match.Type[float64]("data")) + snaps.MatchJSON( + t, + `{"metadata":{"timestamp":"1687108093142"}}`, + match.Type[map[string]interface{}]("metadata"), + ) + }) } diff --git a/match/any_test.go b/match/any_test.go index b9c8631..902b0c3 100644 --- a/match/any_test.go +++ b/match/any_test.go @@ -12,9 +12,9 @@ func TestAnyMatcher(t *testing.T) { a := Any(p...) test.True(t, a.errOnMissingPath) - test.Equal(t, a.placeholder, "") - test.Equal(t, a.paths, p) - test.Equal(t, a.name, "Any") + test.Equal(t, "", a.placeholder) + test.Equal(t, p, a.paths) + test.Equal(t, "Any", a.name) }) t.Run("should allow overriding values", func(t *testing.T) { @@ -22,9 +22,9 @@ func TestAnyMatcher(t *testing.T) { a := Any(p...).ErrOnMissingPath(false).Placeholder("hello") test.False(t, a.errOnMissingPath) - test.Equal(t, a.placeholder, "hello") - test.Equal(t, a.paths, p) - test.Equal(t, a.name, "Any") + test.Equal(t, "hello", a.placeholder) + test.Equal(t, p, a.paths) + test.Equal(t, "Any", a.name) }) t.Run("JSON", func(t *testing.T) { @@ -74,17 +74,19 @@ func TestAnyMatcher(t *testing.T) { test.Equal(t, expected, string(res)) }) - t.Run("should replace value and return new json", func(t *testing.T) { - a := Any( - "user.email", - "date", - "missing.key", - ).ErrOnMissingPath( - false, - ).Placeholder(10) - res, errs := a.JSON(j) - - expected := `{ + t.Run( + "should replace value and return new json with different placeholder", + func(t *testing.T) { + a := Any( + "user.email", + "date", + "missing.key", + ).ErrOnMissingPath( + false, + ).Placeholder(10) + res, errs := a.JSON(j) + + expected := `{ "user": { "name": "mock-user", "email": 10 @@ -92,8 +94,9 @@ func TestAnyMatcher(t *testing.T) { "date": 10 }` - test.Equal(t, 0, len(errs)) - test.Equal(t, expected, string(res)) - }) + test.Equal(t, 0, len(errs)) + test.Equal(t, expected, string(res)) + }, + ) }) } diff --git a/match/type.go b/match/type.go new file mode 100644 index 0000000..fc5cb2a --- /dev/null +++ b/match/type.go @@ -0,0 +1,93 @@ +package match + +import ( + "errors" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type typeMatcher[ExpectedType any] struct { + paths []string + errOnMissingPath bool + name string + expectedType interface{} +} + +/* +Type matcher evaluates types that are passed in a snapshot + +It replaces any targeted path with placeholder in the form of `` + + match.Type[string]("user.info") + // or with multiple paths + match.Type[float64]("user.age", "data.items") +*/ +func Type[ExpectedType any](paths ...string) *typeMatcher[ExpectedType] { + return &typeMatcher[ExpectedType]{ + paths: paths, + errOnMissingPath: true, + name: "Type", + expectedType: *new(ExpectedType), + } +} + +// ErrOnMissingPath determines if matcher will fail in case of trying to access a json path +// that doesn't exist +func (t *typeMatcher[T]) ErrOnMissingPath(e bool) *typeMatcher[T] { + t.errOnMissingPath = e + return t +} + +func (t typeMatcher[ExpectedType]) JSON(s []byte) ([]byte, []MatcherError) { + var errs []MatcherError + json := s + + for _, path := range t.paths { + r := gjson.GetBytes(json, path) + if !r.Exists() { + if t.errOnMissingPath { + errs = append(errs, MatcherError{ + Reason: errors.New("path does not exist"), + Matcher: t.name, + Path: path, + }) + } + continue + } + + if _, ok := r.Value().(ExpectedType); !ok { + errs = append(errs, MatcherError{ + Reason: fmt.Errorf("expected type %T, received %T", *new(ExpectedType), r.Value()), + Matcher: t.name, + Path: path, + }) + + continue + } + + j, err := sjson.SetBytesOptions( + json, + path, + fmt.Sprintf("", r.Value()), + &sjson.Options{ + Optimistic: true, + ReplaceInPlace: true, + }, + ) + if err != nil { + errs = append(errs, MatcherError{ + Reason: err, + Matcher: t.name, + Path: path, + }) + + continue + } + + json = j + } + + return json, errs +} diff --git a/match/type_test.go b/match/type_test.go new file mode 100644 index 0000000..371a127 --- /dev/null +++ b/match/type_test.go @@ -0,0 +1,91 @@ +package match + +import ( + "reflect" + "testing" + + "github.com/gkampitakis/go-snaps/internal/test" +) + +func TestTypeMatcher(t *testing.T) { + t.Run("should create a type matcher", func(t *testing.T) { + p := []string{"test.1", "test.2"} + tm := Type[string](p...) + + test.True(t, tm.errOnMissingPath) + test.Equal(t, "Type", tm.name) + test.Equal(t, p, tm.paths) + test.Equal(t, reflect.TypeOf("").String(), reflect.TypeOf(tm.expectedType).String()) + }) + + t.Run("should allow overriding values", func(t *testing.T) { + p := []string{"test.1", "test.2"} + tm := Type[string](p...) + + tm.ErrOnMissingPath(false) + + test.False(t, tm.errOnMissingPath) + test.Equal(t, "Type", tm.name) + test.Equal(t, p, tm.paths) + test.Equal(t, reflect.TypeOf("").String(), reflect.TypeOf(tm.expectedType).String()) + }) + + t.Run("JSON", func(t *testing.T) { + j := []byte(`{ + "user": { + "name": "mock-user", + "email": "mock-email", + "age": 29 + }, + "date": "16/10/2022" + }`) + + t.Run("should return error in case of missing path", func(t *testing.T) { + tm := Type[string]("user.2") + res, errs := tm.JSON(j) + + test.Equal(t, j, res) + test.Equal(t, 1, len(errs)) + + err := errs[0] + + test.Equal(t, "path does not exist", err.Reason.Error()) + test.Equal(t, "Type", err.Matcher) + test.Equal(t, "user.2", err.Path) + }) + + t.Run("should aggregate errors", func(t *testing.T) { + tm := Type[string]("user.2", "user.3") + res, errs := tm.JSON(j) + + test.Equal(t, j, res) + test.Equal(t, 2, len(errs)) + }) + + t.Run("should evaluate passed type and replace json", func(t *testing.T) { + tm := Type[string]("user.name", "date") + res, errs := tm.JSON(j) + + expected := `{ + "user": { + "name": "", + "email": "mock-email", + "age": 29 + }, + "date": "" + }` + + test.Nil(t, errs) + test.Equal(t, expected, string(res)) + }) + + t.Run("should return error with type mismatch", func(t *testing.T) { + tm := Type[int]("user.name", "user.age") + _, errs := tm.JSON(j) + + test.Equal(t, 2, len(errs)) + test.Equal(t, "expected type int, received string", errs[0].Reason.Error()) + test.Equal(t, "expected type int, received float64", errs[1].Reason.Error()) + }) + }) +} diff --git a/snaps/skip_test.go b/snaps/skip_test.go index 30ee80c..8d86b4c 100644 --- a/snaps/skip_test.go +++ b/snaps/skip_test.go @@ -145,10 +145,9 @@ func TestSkip(t *testing.T) { // This is for populating skippedTests.values and following the normal flow SkipNow(mockT) - test.True(t, testSkipped("TestMock/Skip", runOnly)) - test.Equal( + test.True(t, testSkipped("TestMock/Skip - 1000", runOnly)) + test.True( t, - true, testSkipped("TestMock/Skip/child_should_also_be_skipped", runOnly), ) test.False(t, testSkipped("TestAnotherTest", runOnly)) @@ -174,10 +173,10 @@ func TestSkip(t *testing.T) { // This is for populating skippedTests.values and following the normal flow SkipNow(mockT) - test.True(t, testSkipped("Test", runOnly)) - test.True(t, testSkipped("Test/chid", runOnly)) - test.False(t, testSkipped("TestMock", runOnly)) - test.False(t, testSkipped("TestMock/child", runOnly)) + test.True(t, testSkipped("Test - 1", runOnly)) + test.True(t, testSkipped("Test/child - 1", runOnly)) + test.False(t, testSkipped("TestMock - 1", runOnly)) + test.False(t, testSkipped("TestMock/child - 1", runOnly)) }) t.Run("should use regex match for runOnly", func(t *testing.T) { diff --git a/snaps/snapshot.go b/snaps/snapshot.go index 0b8cad1..5a29487 100644 --- a/snaps/snapshot.go +++ b/snaps/snapshot.go @@ -83,21 +83,17 @@ type syncRegistry struct { // Returns the id of the test in the snapshot // Form [ - ] func (s *syncRegistry) getTestID(tName, snapPath string) string { - occurrence := 1 s.Lock() if _, exists := s.values[snapPath]; !exists { s.values[snapPath] = make(map[string]int) } - if c, exists := s.values[snapPath][tName]; exists { - occurrence = c + 1 - } - - s.values[snapPath][tName] = occurrence + s.values[snapPath][tName]++ + c := s.values[snapPath][tName] s.Unlock() - return fmt.Sprintf("[%s - %d]", tName, occurrence) + return fmt.Sprintf("[%s - %d]", tName, c) } func newRegistry() *syncRegistry {