Skip to content

Commit 9b70932

Browse files
authored
Merge pull request #5 from fred1268/release-1.3.0
Implement setup and teardown
2 parents 31d999f + c94fa29 commit 9b70932

File tree

5 files changed

+148
-39
lines changed

5 files changed

+148
-39
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ A test file contains an array of tests, each of them containing:
207207
208208
> Please also note that `endpoint` and `payload` can use environment variable substitution using the ${env:XXX} syntax (see previous note about environment variable substitution).
209209
210-
> Lastly, please note that `endpoint` and `response` can use captured variable (i.e. variables inside a captured response, see `"capture":true`). For instance, to use the `id` field returned inside of a `user` object in test `mytest`, you will use `${mytest.user.id}`. In the example above, we used `${cap121004.id}` to retrieve the ID of the returned response in test `cap121004`. Captured response also works with arrays.
210+
> Lastly, please note that `endpoint`, `payload` and `response` can use captured variable (i.e. variables inside a captured response, see `"capture":true`). For instance, to use the `id` field returned inside of a `user` object in test `mytest`, you will use `${mytest.user.id}`. In the example above, we used `${cap121004.id}` to retrieve the ID of the returned response in test `cap121004`. Captured response also works with arrays.
211211
212212
### Payload and Response files
213213

@@ -226,6 +226,14 @@ As we saw earlier, for each test, you will have to define the expected response.
226226

227227
> Please note that, in the case of non-JSON responses, you can use regular expressions (see test 121007). In that case, make sure the `expected.response` field is set to a proper, compilable, regular expression. Be mindful that you will need to escape the `\ (backslash)` character using `\\`. For instance `\s+[wW]eight` will be written `\\s+[wW]eight`, in order to match one or more whitespace characters, followed by weight or Weight.
228228
229+
## Setup and Teardown
230+
231+
okapi will always try to load and execute the `setup.test.json` file before any other tests, and the `teardown.test.json` after all other tests. All tests in the `setup.test.json` file are automatically captured (independently of the test's `capture` flag). The captured variables will be available under the `setup.testname.xxx...` name (like the other test, but with a `setup` prefix). Also, they will be globally available, including to the `teardown.test.json` file.
232+
233+
Usually, you will want to have your `teardown.test.json` undo all changes done by the `setup.test.json` file so that your whole test suite (i.e. directory) is idempotent. This is an important caracteristics of a good test suite.
234+
235+
> Please note that both of these files are optional.
236+
229237
## Running okapi :giraffe:
230238

231239
To launch okapi, please run the following:

testing/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package testing
22

33
import (
4+
"fmt"
45
"runtime"
6+
"strings"
57

68
"github.com/fred1268/go-clap/clap"
79
)
@@ -20,6 +22,7 @@ type Config struct {
2022
Verbose bool `clap:"--verbose,-v"`
2123
Parallel bool `clap:"--parallel,-p"`
2224
FileParallel bool `clap:"--file-parallel"`
25+
setupCapture map[string]any
2326
}
2427

2528
// LoadConfig returns okapi's configuration from the
@@ -38,5 +41,8 @@ func LoadConfig(args []string) (*Config, error) {
3841
if _, err := clap.Parse(args, &cfg); err != nil {
3942
return nil, err
4043
}
44+
if cfg.File != "" && !strings.HasSuffix(cfg.File, ".test.json") {
45+
cfg.File = fmt.Sprintf("%s.test.json", cfg.File)
46+
}
4147
return &cfg, nil
4248
}

testing/loader.go

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,48 @@ func readJSONDependencies(directory string, requests []*APIRequest) error {
5151
return nil
5252
}
5353

54+
func loadTest(cfg *Config, uniqueTests map[string]*APIRequest, filename string) ([]*APIRequest, error) {
55+
content, err := os.ReadFile(path.Join(cfg.Directory, filename))
56+
if err != nil {
57+
return nil, fmt.Errorf("cannot read test file '%s': %w", filename, err)
58+
}
59+
var tests struct {
60+
Tests []*APIRequest
61+
}
62+
if err = json.NewDecoder(bytes.NewReader(content)).Decode(&tests); err != nil {
63+
return nil, fmt.Errorf("cannot decode json file '%s': %w", filename, err)
64+
}
65+
for _, test := range tests.Tests {
66+
if test.Payload == "@file" {
67+
test.atFile = true
68+
}
69+
if test.Expected.Response == "@file" {
70+
test.Expected.atFile = true
71+
}
72+
}
73+
for _, test := range tests.Tests {
74+
t, ok := uniqueTests[test.Name]
75+
if !ok {
76+
uniqueTests[test.Name] = test
77+
continue
78+
}
79+
log.Printf("Warning: two tests with the same name (%s)\n", test.Name)
80+
if t.hasFileDepencies() {
81+
if test.hasFileDepencies() {
82+
log.Printf("Potential conflict: two tests with the same name (%s) are using @file\n", test.Name)
83+
}
84+
} else {
85+
// replace test without @file with this one
86+
// doesn't matter if it has @file or not
87+
uniqueTests[test.Name] = test
88+
}
89+
}
90+
if err := readJSONDependencies(cfg.Directory, tests.Tests); err != nil {
91+
return nil, err
92+
}
93+
return tests.Tests, nil
94+
}
95+
5496
// LoadTests reads all test files in the provided directory and
5597
// returns them sorted by file.
5698
//
@@ -67,54 +109,23 @@ func LoadTests(cfg *Config) (map[string][]*APIRequest, error) {
67109
if !strings.HasSuffix(file.Name(), ".test.json") {
68110
continue
69111
}
112+
if file.Name() == "setup.test.json" || file.Name() == "teardown.test.json" {
113+
continue
114+
}
70115
if cfg.File != "" && cfg.File != file.Name() {
71116
continue
72117
}
73-
content, err := os.ReadFile(path.Join(cfg.Directory, file.Name()))
118+
tests, err := loadTest(cfg, uniqueTests, file.Name())
74119
if err != nil {
75-
return nil, fmt.Errorf("cannot read test file '%s': %w", file.Name(), err)
76-
}
77-
var tests struct {
78-
Tests []*APIRequest
79-
}
80-
if err = json.NewDecoder(bytes.NewReader(content)).Decode(&tests); err != nil {
81-
return nil, fmt.Errorf("cannot decode json file '%s': %w", file.Name(), err)
82-
}
83-
for _, test := range tests.Tests {
84-
if test.Payload == "@file" {
85-
test.atFile = true
86-
}
87-
if test.Expected.Response == "@file" {
88-
test.Expected.atFile = true
89-
}
90-
}
91-
for _, test := range tests.Tests {
92-
t, ok := uniqueTests[test.Name]
93-
if !ok {
94-
uniqueTests[test.Name] = test
95-
continue
96-
}
97-
log.Printf("Warning: two tests with the same name (%s)\n", test.Name)
98-
if t.hasFileDepencies() {
99-
if test.hasFileDepencies() {
100-
log.Printf("Potential conflict: two tests with the same name (%s) using @file\n", test.Name)
101-
}
102-
} else {
103-
// replace test without @file with this one
104-
// doesn't matter if it has @file or not
105-
uniqueTests[test.Name] = test
106-
}
107-
}
108-
if err := readJSONDependencies(cfg.Directory, tests.Tests); err != nil {
109120
return nil, err
110121
}
111-
if len(tests.Tests) == 0 {
122+
if len(tests) == 0 {
112123
log.Printf("Skipping '%s': no tests found in file\n", file.Name())
113124
continue
114125
}
115126
if cfg.Test != "" {
116127
var test *APIRequest
117-
for _, t := range tests.Tests {
128+
for _, t := range tests {
118129
if cfg.Test == t.Name {
119130
test = t
120131
break
@@ -126,7 +137,7 @@ func LoadTests(cfg *Config) (map[string][]*APIRequest, error) {
126137
}
127138
continue
128139
}
129-
allTests[file.Name()] = tests.Tests
140+
allTests[file.Name()] = tests
130141
}
131142
return allTests, nil
132143
}

testing/setup.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package testing
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/fred1268/okapi/testing/internal/log"
12+
tos "github.com/fred1268/okapi/testing/internal/os"
13+
)
14+
15+
func load(ctx context.Context, cfg *Config, clients map[string]*Client, name string) error {
16+
if cfg.setupCapture == nil {
17+
cfg.setupCapture = make(map[string]any)
18+
}
19+
uniqueTests := make(map[string]*APIRequest)
20+
tests, err := loadTest(cfg, uniqueTests, fmt.Sprintf("%s.test.json", name))
21+
if err != nil {
22+
if errors.Is(err, os.ErrNotExist) {
23+
return nil
24+
}
25+
return err
26+
}
27+
if cfg.Verbose {
28+
log.Printf(fmt.Sprintf("%s tests executed\n", name))
29+
}
30+
for _, test := range tests {
31+
client := clients[test.Server]
32+
if client == nil {
33+
log.Fatalf("invalid server '%s' for test '%s'\n", test.Server, test.Name)
34+
continue
35+
}
36+
test.Endpoint = tos.SubstituteCapturedVariable(test.Endpoint, cfg.setupCapture)
37+
test.Payload = tos.SubstituteCapturedVariable(test.Payload, cfg.setupCapture)
38+
test.Expected.Response = tos.SubstituteCapturedVariable(test.Expected.Response, cfg.setupCapture)
39+
response, err := client.Test(ctx, test, cfg.Verbose)
40+
if err != nil {
41+
if !errors.Is(err, ErrStatusCodeMismatched) && !errors.Is(err, ErrResponseMismatched) {
42+
log.Fatalf(fmt.Sprintf("Cannot run %s test '%s': %v\n", name, test.Name, err))
43+
}
44+
}
45+
if name == "setup" {
46+
var r interface{}
47+
err = json.Unmarshal([]byte(strings.ToLower(response.Response)), &r)
48+
if err != nil {
49+
continue
50+
}
51+
if obj, ok := r.(map[string]any); ok {
52+
cfg.setupCapture[test.Name] = obj
53+
}
54+
}
55+
}
56+
return nil
57+
}
58+
59+
// Setup reads the setup.test.json test file and executes all
60+
// the tests within the file.
61+
//
62+
// Results of these tests are captured into a setup object and
63+
// thus can be accessed using `setup.testname.xxx...`.
64+
func Setup(ctx context.Context, cfg *Config, clients map[string]*Client) error {
65+
return load(ctx, cfg, clients, "setup")
66+
}
67+
68+
// Teardown reads the teardown.test.json test file and executes
69+
// all the tests within the file.
70+
//
71+
// These tests should revert what has been done in setup in order
72+
// to make the test suite idempotent.
73+
func Teardown(ctx context.Context, cfg *Config, clients map[string]*Client) error {
74+
return load(ctx, cfg, clients, "teardown")
75+
}

testing/test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ func worker(ctx context.Context, in chan []*testIn, out chan *testOut, done chan
5555
case runs := <-in:
5656
captures := make(map[string]any)
5757
for _, run := range runs {
58+
if run == runs[0] {
59+
captures["setup"] = run.config.setupCapture
60+
}
5861
run.test.Endpoint = os.SubstituteCapturedVariable(run.test.Endpoint, captures)
5962
run.test.Payload = os.SubstituteCapturedVariable(run.test.Payload, captures)
6063
run.test.Expected.Response = os.SubstituteCapturedVariable(run.test.Expected.Response, captures)
@@ -154,6 +157,9 @@ func Run(ctx context.Context, cfg *Config) error {
154157
}
155158
wg.Add(1)
156159
go printer(ctx, allTests, out, &wg)
160+
if err := Setup(ctx, cfg, clients); err != nil {
161+
return err
162+
}
157163
for key, tests := range allTests {
158164
fileStart := time.Now()
159165
var tins []*testIn
@@ -184,6 +190,9 @@ func Run(ctx context.Context, cfg *Config) error {
184190
close(done)
185191
close(in)
186192
close(out)
193+
if err := Teardown(ctx, cfg, clients); err != nil {
194+
return err
195+
}
187196
count := 0
188197
for _, value := range allTests {
189198
count += len(value)

0 commit comments

Comments
 (0)