Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for Type matcher #64

Merged
merged 3 commits into from
Jun 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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