Skip to content

Commit a30b0f9

Browse files
committed
Add expect.JSON comparator
This changeset adds expect.JSON[T any], which will allow easier testing of json output and hopefully remove a lot of boilerplate unmarshalling / assert in tests. It also adds an extensive doc.md document about comparators (plan is trim down the main documentation to a small set of simple examples, then link to these "advanced" docs for further reading), which will allow for easier documentation maintenance and more approachable reading. Signed-off-by: apostasie <[email protected]>
1 parent 6a34138 commit a30b0f9

File tree

3 files changed

+264
-5
lines changed

3 files changed

+264
-5
lines changed

mod/tigron/expect/comparators.go

+17
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
package expect
2020

2121
import (
22+
"encoding/json"
2223
"regexp"
2324
"testing"
2425

2526
"github.com/containerd/nerdctl/mod/tigron/internal/assertive"
2627
"github.com/containerd/nerdctl/mod/tigron/test"
28+
"github.com/containerd/nerdctl/mod/tigron/tig"
2729
)
2830

2931
// All can be used as a parameter for expected.Output to group a set of comparators.
@@ -69,3 +71,18 @@ func Match(reg *regexp.Regexp) test.Comparator {
6971
assertive.Match(assertive.WithFailLater(t), stdout, reg, info)
7072
}
7173
}
74+
75+
// JSON allows to verify that the output can be marshalled into T, and optionally can be further verified by a provided
76+
// method.
77+
func JSON[T any](obj T, verifier func(T, string, tig.T)) test.Comparator {
78+
return func(stdout, info string, t *testing.T) {
79+
t.Helper()
80+
81+
err := json.Unmarshal([]byte(stdout), &obj)
82+
assertive.ErrorIsNil(assertive.WithFailLater(t), err, "failed to unmarshal JSON from stdout")
83+
84+
if verifier != nil && err == nil {
85+
verifier(obj, info, t)
86+
}
87+
}
88+
}

mod/tigron/expect/comparators_test.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,45 @@
1818
package expect_test
1919

2020
import (
21+
"encoding/json"
2122
"regexp"
2223
"testing"
2324

2425
"github.com/containerd/nerdctl/mod/tigron/expect"
26+
"github.com/containerd/nerdctl/mod/tigron/internal/assertive"
27+
"github.com/containerd/nerdctl/mod/tigron/tig"
2528
)
2629

2730
func TestExpect(t *testing.T) {
2831
t.Parallel()
2932

30-
expect.Contains("b")("a b c", "info", t)
31-
expect.DoesNotContain("d")("a b c", "info", t)
32-
expect.Equals("a b c")("a b c", "info", t)
33-
expect.Match(regexp.MustCompile("[a-z ]+"))("a b c", "info", t)
33+
expect.Contains("b")("a b c", "contains works", t)
34+
expect.DoesNotContain("d")("a b c", "does not contain works", t)
35+
expect.Equals("a b c")("a b c", "equals work", t)
36+
expect.Match(regexp.MustCompile("[a-z ]+"))("a b c", "match works", t)
3437

3538
expect.All(
3639
expect.Contains("b"),
3740
expect.DoesNotContain("d"),
3841
expect.Equals("a b c"),
3942
expect.Match(regexp.MustCompile("[a-z ]+")),
40-
)("a b c", "info", t)
43+
)("a b c", "all", t)
44+
45+
type foo struct {
46+
Foo map[string]string `json:"foo"`
47+
}
48+
49+
data, err := json.Marshal(&foo{
50+
Foo: map[string]string{
51+
"foo": "bar",
52+
},
53+
})
54+
55+
assertive.ErrorIsNil(t, err)
56+
57+
expect.JSON(&foo{}, nil)(string(data), "json, no verifier", t)
58+
59+
expect.JSON(&foo{}, func(obj *foo, info string, t tig.T) {
60+
assertive.IsEqual(t, obj.Foo["foo"], "bar", info)
61+
})(string(data), "json, with verifier", t)
4162
}

mod/tigron/expect/doc.md

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Expectations
2+
3+
Attaching expectations to a test case is how the developer can express conditions on exit code, stdout, or stderr,
4+
to be verified for the test to pass.
5+
6+
The simplest way to do that is to use the helper `test.Expects(exitCode int, errors []error, outputCompare test.Comparator)`.
7+
8+
```go
9+
package main
10+
11+
import (
12+
"testing"
13+
14+
"go.farcloser.world/tigron/test"
15+
)
16+
17+
func TestMyThing(t *testing.T) {
18+
// Declare your test
19+
myTest := &test.Case{}
20+
21+
// Attach a command to run
22+
myTest.Command = test.Custom("ls")
23+
24+
// Set your expectations
25+
myTest.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil)
26+
27+
// Run it
28+
myTest.Run(t)
29+
}
30+
```
31+
32+
### Exit status expectations
33+
34+
The first parameter, `exitCode` should be set to one of the provided `expect.ExitCodeXXX` constants:
35+
- `expect.ExitCodeSuccess`: validates that the command ran and exited successfully
36+
- `expect.ExitCodeTimeout`: validates that the command did time out
37+
- `expect.ExitCodeSignaled`: validates that the command received a signal
38+
- `expect.ExitCodeGenericFail`: validates that the command failed (failed to start, or returned a non-zero exit code)
39+
- `expect.ExitCodeNoCheck`: does not enforce any verification at all on the command
40+
41+
... you may also pass explicit exit codes directly (> 0) if you want to precisely match them.
42+
43+
### Stderr expectations with []error
44+
45+
To validate that stderr contain specific information, you can pass a slice of `error` as `test.Expects`
46+
second parameter.
47+
48+
The command output on stderr is then verified to contain all stringified errors.
49+
50+
### Stdout expectations with Comparators
51+
52+
The last parameter of `test.Expects` accepts a `test.Comparator`, which allows testing the content of the command
53+
output on `stdout`.
54+
55+
The following ready-made `test.Comparator` generators are provided:
56+
- `expect.Contains(string)`: verifies that stdout contains the string parameter
57+
- `expect.DoesNotContain(string)`: negation of above
58+
- `expect.Equals(string)`: strict equality
59+
- `expect.Match(*regexp.Regexp)`: regexp matching
60+
- `expect.All(comparators ...Comparator)`: allows to bundle together a bunch of other comparators
61+
- `expect.JSON[T any](obj T, verifier func(T, string, tig.T))`: allows to verify the output is valid JSON and optionally
62+
pass `verifier(T, string, tig.T)` extra validation
63+
64+
### A complete example
65+
66+
```go
67+
package main
68+
69+
import (
70+
"testing"
71+
"errors"
72+
73+
"go.farcloser.world/tigron/tig"
74+
"go.farcloser.world/tigron/test"
75+
"go.farcloser.world/tigron/expect"
76+
)
77+
78+
type Thing struct {
79+
Name string
80+
}
81+
82+
func TestMyThing(t *testing.T) {
83+
// Declare your test
84+
myTest := &test.Case{}
85+
86+
// Attach a command to run
87+
myTest.Command = test.Custom("bash", "-c", "--", ">&2 echo thing; echo '{\"Name\": \"out\"}'; exit 42;")
88+
89+
// Set your expectations
90+
myTest.Expected = test.Expects(
91+
expect.ExitCodeGenericFail,
92+
[]error{errors.New("thing")},
93+
expect.All(
94+
expect.Contains("out"),
95+
expect.DoesNotContain("something"),
96+
expect.JSON(&Thing{}, func(obj *Thing, info string, t tig.T) {
97+
assert.Equal(t, obj.Name, "something", info)
98+
}),
99+
),
100+
)
101+
102+
// Run it
103+
myTest.Run(t)
104+
}
105+
```
106+
107+
### Custom stdout comparators
108+
109+
If you need to implement more advanced verifications on stdout that the ready-made comparators can't do,
110+
you can implement your own custom `test.Comparator`.
111+
112+
For example:
113+
114+
```go
115+
package whatever
116+
117+
import (
118+
"testing"
119+
120+
"gotest.tools/v3/assert"
121+
122+
"go.farcloser.world/tigron/tig"
123+
"go.farcloser.world/tigron/test"
124+
)
125+
126+
func TestMyThing(t *testing.T) {
127+
// Declare your test
128+
myTest := &test.Case{}
129+
130+
// Attach a command to run
131+
myTest.Command = test.Custom("ls")
132+
133+
// Set your expectations
134+
myTest.Expected = test.Expects(0, nil, func(stdout, info string, t tig.T){
135+
t.Helper()
136+
// Bla bla, do whatever advanced stuff and some asserts
137+
})
138+
139+
// Run it
140+
myTest.Run(t)
141+
}
142+
143+
// You can of course generalize your comparator into a generator if it is going to be useful repeatedly
144+
145+
func MyComparatorGenerator(param1, param2 any) test.Comparator {
146+
return func(stdout, info string, t tig.T) {
147+
t.Helper()
148+
// Do your thing...
149+
// ...
150+
}
151+
}
152+
153+
```
154+
155+
You can now pass along `MyComparator(comparisonString)` as the third parameter of `test.Expects`, or compose it with
156+
other comparators using `expect.All(MyComparator(comparisonString), OtherComparator(somethingElse))`
157+
158+
Note that you have access to an opaque `info` string, that provides a brief formatted header message that assert
159+
will use in case of failure to provide context on the error.
160+
You may of course ignore it and write your own message.
161+
162+
### Advanced expectations
163+
164+
You may want to have expectations that contain a certain piece of data that is being used in the command or at
165+
other stages of your test (like `Setup` for example).
166+
167+
To achieve that, you should write your own `test.Manager` instead of using the helper `test.Expects`.
168+
169+
A manager is a simple function which only role is to return a `test.Expected` struct.
170+
The `test.Manager` signature makes available `test.Data` and `test.Helpers` to you.
171+
172+
Here is an example, where we are using `data.Get("sometestdata")`.
173+
174+
```go
175+
package main
176+
177+
import (
178+
"errors"
179+
"testing"
180+
181+
"gotest.tools/v3/assert"
182+
183+
"go.farcloser.world/tigron/test"
184+
)
185+
186+
func TestMyThing(t *testing.T) {
187+
// Declare your test
188+
myTest := &test.Case{}
189+
190+
myTest.Setup = func(data test.Data, helpers test.Helpers){
191+
// Do things...
192+
// ...
193+
// Save this for later
194+
data.Set("something", "lalala")
195+
}
196+
197+
// Attach a command to run
198+
myTest.Command = test.Custom("somecommand")
199+
200+
// Set your fully custom expectations
201+
myTest.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
202+
// With a custom Manager you have access to both the test.Data and test.Helpers to perform more
203+
// refined verifications.
204+
return &test.Expected{
205+
ExitCode: 1,
206+
Errors: []error{
207+
errors.New("foobla"),
208+
},
209+
Output: func(stdout, info string, t tig.T) {
210+
t.Helper()
211+
212+
// Retrieve the data that was set during the Setup phase.
213+
assert.Assert(t, stdout == data.Get("sometestdata"), info)
214+
},
215+
}
216+
}
217+
218+
// Run it
219+
myTest.Run(t)
220+
}
221+
```

0 commit comments

Comments
 (0)