Skip to content

Commit

Permalink
feat: add support for Type matcher (#64)
Browse files Browse the repository at this point in the history
* exploring support for type matcher

* fix: add unit tests and docs

* misc
  • Loading branch information
gkampitakis authored Jun 24, 2023
1 parent a2cf7ae commit a3fd6aa
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 35 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -127,6 +128,7 @@ Currently `go-snaps` has two build in matchers

- `match.Any`
- `match.Custom`
- `match.Type[ExpectedType]`

#### match.Any

Expand Down Expand Up @@ -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 `<Type:ExpectedType>`.

```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

Expand Down
12 changes: 12 additions & 0 deletions examples/__snapshots__/matchJSON_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,15 @@
}
}
---

[TestMatchers/type_matcher - 1]
{
"data": "<Type:float64>"
}
---

[TestMatchers/type_matcher - 2]
{
"metadata": "<Type:map[string]interface {}>"
}
---
9 changes: 9 additions & 0 deletions examples/matchJSON_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
})
}
43 changes: 23 additions & 20 deletions match/any_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ func TestAnyMatcher(t *testing.T) {
a := Any(p...)

test.True(t, a.errOnMissingPath)
test.Equal(t, a.placeholder, "<Any value>")
test.Equal(t, a.paths, p)
test.Equal(t, a.name, "Any")
test.Equal(t, "<Any value>", a.placeholder)
test.Equal(t, p, a.paths)
test.Equal(t, "Any", a.name)
})

t.Run("should allow overriding values", func(t *testing.T) {
p := []string{"test.1", "test.2"}
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) {
Expand Down Expand Up @@ -74,26 +74,29 @@ 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
},
"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))
},
)
})
}
93 changes: 93 additions & 0 deletions match/type.go
Original file line number Diff line number Diff line change
@@ -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 `<Type:ExpectedType>`
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("<Type:%T>", 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
}
91 changes: 91 additions & 0 deletions match/type_test.go
Original file line number Diff line number Diff line change
@@ -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": "<Type:string>",
"email": "mock-email",
"age": 29
},
"date": "<Type:string>"
}`

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())
})
})
}
13 changes: 6 additions & 7 deletions snaps/skip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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) {
Expand Down
10 changes: 3 additions & 7 deletions snaps/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,17 @@ type syncRegistry struct {
// Returns the id of the test in the snapshot
// Form [<test-name> - <occurrence>]
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 {
Expand Down

0 comments on commit a3fd6aa

Please sign in to comment.