Skip to content

Commit 74db334

Browse files
committed
add custom retry configuration
Adds the ability to configure retry behaviour for any test spec, using either a constant interval or an exponential backoff. In doing so, reworks the framework to have all calls to testing.T.Errorf() happen in the Scenario.Run() method, as well as have that method handle all retries. gdt-kube will need to be refactored to remove the retry behaviour implemented in that plugin. Also, this finally fixes the way that failures are tested by using the strategy outlined in golang/go#39903. Issue #26 Signed-off-by: Jay Pipes <[email protected]>
1 parent ecee172 commit 74db334

22 files changed

+550
-134
lines changed

README.md

+59-43
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111

1212
`gdt` is a testing library that allows test authors to cleanly describe tests
1313
in a YAML file. `gdt` reads YAML files that describe a test's assertions and
14-
then builds a set of Golang structures that the standard Golang
14+
then builds a set of Go structures that the standard Go
1515
[`testing`](https://golang.org/pkg/testing/) package and standard `go test`
1616
tool can execute.
1717

1818
## Introduction
1919

20-
Writing functional tests in Golang can be overly verbose and tedious. When the
21-
code that tests some part of an application is verbose or tedious, then it
22-
becomes difficult to read the tests and quickly understand the assertions the
23-
test is making.
20+
Writing functional tests in Go can be overly verbose and tedious. When the code
21+
that tests some part of an application is verbose or tedious, then it becomes
22+
difficult to read the tests and quickly understand the assertions the test is
23+
making.
2424

2525
The more difficult it is to understand the test assertions or the test setups
2626
and assumptions, the greater the chance that the test improperly validates the
@@ -34,7 +34,7 @@ describe a functional test's **assumptions** and **assertions** in a
3434
declarative format.
3535

3636
Separating the *description* of a test's assumptions (setup) and assertions
37-
from the Golang code that actually performs the test assertions leads to tests
37+
from the Go code that actually performs the test assertions leads to tests
3838
that are easier to read and understand. This allows developers to spend *more
3939
time writing code* and less time copy/pasting boilerplate test code. Due to the
4040
easier test comprehension, `gdt` also encourages writing greater quality and
@@ -138,9 +138,9 @@ var _ = Describe("Books API Types", func() {
138138
```
139139

140140

141-
This is perfectly fine for simple unit tests of Golang code. However, once the
142-
tests begin to call multiple APIs or packages, the Ginkgo Golang tests start to
143-
get cumbersome. Consider the following example of *functionally* testing the
141+
This is perfectly fine for simple unit tests of Go code. However, once the
142+
tests begin to call multiple APIs or packages, the Ginkgo Go tests start to get
143+
cumbersome. Consider the following example of *functionally* testing the
144144
failure modes for a simple HTTP REST API endpoint
145145
([`failure_test.go`](https://github.com/gdt-dev/gdt-examples/blob/main/http/api/failure_test.go)):
146146

@@ -256,7 +256,7 @@ var _ = Describe("Books API - GET /books failures", func() {
256256
```
257257

258258
The above test code obscures what is being tested by cluttering the test
259-
assertions with the Golang closures and accessor code. Compare the above with
259+
assertions with the Go closures and accessor code. Compare the above with
260260
how `gdt` allows the test author to describe the same assertions
261261
([`failures.yaml`](https://github.com/gdt-dev/gdt-examples/blob/main/http/tests/api/failures.yaml)):
262262

@@ -284,7 +284,7 @@ No more closures and boilerplate function code getting in the way of expressing
284284
the assertions, which should be the focus of the test.
285285

286286
The more intricate the assertions being verified by the test, generally the
287-
more verbose and cumbersome the Golang test code tends to become. First and
287+
more verbose and cumbersome the Go test code tends to become. First and
288288
foremost, tests should be *readable*. If they are not readable, then the test's
289289
assertions are not *understandable*. And tests that cannot easily be understood
290290
are often the source of bit rot and technical debt. Worse, tests that aren't
@@ -300,7 +300,7 @@ Consider a Ginkgo test case that checks the following behaviour:
300300
* The newly-created book's ID field is a valid UUID
301301
* The newly-created book's publisher has an address containing a known state code
302302

303-
A typical implementation of a Ginkgo Golang test might look like this
303+
A typical implementation of a Ginkgo test might look like this
304304
([`create_then_get_test.go`](https://github.com/gdt-dev/gdt-examples/blob/main/http/api/create_then_get_test.go)):
305305

306306
```go
@@ -423,8 +423,7 @@ All `gdt` scenarios have the following fields:
423423
missing or empty, the filename is used as the name
424424
* `description`: (optional) string with longer description of the test file
425425
contents
426-
* `defaults`: (optional) is a map, keyed by a plugin name, of default options
427-
and configuration values for that plugin.
426+
* `defaults`: (optional) is a map of default options and configuration values
428427
* `fixtures`: (optional) list of strings indicating named fixtures that will be
429428
started before any of the tests in the file are run
430429
* `skip-if`: (optional) list of [`Spec`][basespec] specializations that will be
@@ -433,26 +432,29 @@ All `gdt` scenarios have the following fields:
433432
* `tests`: list of [`Spec`][basespec] specializations that represent the
434433
runnable test units in the test scenario.
435434

436-
[basespec]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/types/spec.go#L27-L44
435+
[basespec]: https://github.com/gdt-dev/gdt/blob/ecee17249e1fa10147cf9191be0358923da44094/types/spec.go#L30
437436

438437
The scenario's `tests` field is the most important and the [`Spec`][basespec]
439438
objects that it contains are the meat of a test scenario.
440439

441-
## `gdt` test spec structure
440+
### `gdt` test spec structure
442441

443442
A spec represents a single *action* that is taken and zero or more
444443
*assertions* that represent what you expect to see resulting from that action.
445444

446-
Each spec is a specialized class of the base [`Spec`][basespec] that deals with
447-
a particular type of test. For example, there is a `Spec` class called `exec`
448-
that allows you to execute arbitrary commands and assert expected result codes
449-
and output. There is a `Spec` class called `http` that allows you to call an
450-
HTTP URL and assert that the response looks like what you expect. Depending on
451-
how you define your test units, `gdt` will parse the YAML definition into one
452-
of these specialized `Spec` classes.
445+
`gdt` plugins each define a specialized subclass of the base [`Spec`][basespec]
446+
that contains fields that are specific to that type of test.
453447

454-
The base `Spec` class has the following fields (and thus all `Spec` specialized
455-
classes inherit these fields):
448+
For example, there is an [`exec`][exec-plugin] plugin that allows you to
449+
execute arbitrary commands and assert expected result codes and output. There
450+
is an [`http`][http-plugin] that allows you to call an HTTP URL and assert that
451+
the response looks like what you expect. There is a [`kube`][kube-plugin]
452+
plugin that allows you to interact with a Kubernetes API, etc.
453+
454+
`gdt` examines the YAML file that defines your test scenario and uses these
455+
plugins to parse individual test specs.
456+
457+
All test specs have the following fields:
456458

457459
* `name`: (optional) string describing the test unit.
458460
* `description`: (optional) string with longer description of the test unit.
@@ -462,24 +464,49 @@ classes inherit these fields):
462464
complete within.
463465
* `timeout.expected`: a bool indicating that the test unit is expected to not
464466
complete before `timeout.after`. This is really only useful in unit testing.
467+
* `retry`: (optional) an object containing retry configurationu for the test
468+
unit. Some plugins will automatically attempt to retry the test action when
469+
an assertion fails. This field allows you to control this retry behaviour for
470+
each individual test.
471+
* `retry.interval`: (optional) a string duration of time that the test plugin
472+
will retry the test action in the event assertions fail. The default interval
473+
for retries is plugin-dependent.
474+
* `retry.attempts`: (optional) an integer indicating the number of times that a
475+
plugin will retry the test action in the event assertions fail. The default
476+
number of attempts for retries is plugin-dependent.
477+
* `retry.exponential`: (optional) a boolean indicating an exponential backoff
478+
should be applied to the retry interval. The default is is plugin-dependent.
465479
* `wait` (optional) an object containing [wait information][wait] for the test
466480
unit.
467481
* `wait.before`: a string duration of time that gdt should wait before
468482
executing the test unit's action.
469483
* `wait.after`: a string duration of time that gdt should wait after executing
470484
the test unit's action.
485+
* `on`: (optional) an object describing actions to take upon certain
486+
conditions.
487+
* `on.fail`: (optional) an object describing an action to take when any
488+
assertion fails for the test action.
489+
* `on.fail.exec`: a string with the exact command to execute upon test
490+
assertion failure. You may execute more than one command but must include the
491+
`on.fail.shell` field to indicate that the command should be run in a shell.
492+
* `on.fail.shell`: (optional) a string with the specific shell to use in executing the
493+
command to run upon test assertion failure. If empty (the default), no shell
494+
is used to execute the command and instead the operating system's `exec` family
495+
of calls is used.
471496

497+
[exec-plugin]: https://github.com/gdt-dev/gdt/tree/ecee17249e1fa10147cf9191be0358923da44094/plugin/exec
498+
[http-plugin]: https://github.com/gdt-dev/http
499+
[kube-plugin]: https://github.com/gdt-dev/kube
472500
[timeout]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/types/timeout.go#L11-L22
473501
[wait]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/types/wait.go#L11-L25
474502

475-
### `exec` test spec structure
503+
#### `exec` test spec structure
476504

477-
An exec spec is a specialization of the base [`Spec`][basespec] that allows
478-
test authors to execute arbitrary commands and assert that the command results
479-
in an expected result code or output.
505+
The `exec` plugin's test spec allows test authors to execute arbitrary commands and
506+
assert that the command results in an expected result code or output.
480507

481-
The [exec `Spec`][execspec] class has the following fields (in addition to all
482-
the base `Spec` fields listed above):
508+
In addition to all the base `Spec` fields listed above, the `exec` plugin's
509+
test spec also contains these fields:
483510

484511
* `exec`: a string with the exact command to execute. You may execute more than
485512
one command but must include the `shell` field to indicate that the command
@@ -514,17 +541,6 @@ the base `Spec` fields listed above):
514541
least one* must be present in `stderr`.
515542
* `assert.err.none`: (optional) a string or list of strings of which *none
516543
should be present* in `stderr`.
517-
* `on`: (optional) an object describing actions to take upon certain
518-
conditions.
519-
* `on.fail`: (optional) an object describing an action to take when any
520-
assertion fails for the test action.
521-
* `on.fail.exec`: a string with the exact command to execute upon test
522-
assertion failure. You may execute more than one command but must include the
523-
`on.fail.shell` field to indicate that the command should be run in a shell.
524-
* `on.fail.shell`: (optional) a string with the specific shell to use in executing the
525-
command to run upon test assertion failure. If empty (the default), no shell
526-
is used to execute the command and instead the operating system's `exec` family
527-
of calls is used.
528544

529545
[execspec]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/exec/spec.go#L11-L34
530546
[pipeexpect]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/exec/assertions.go#L15-L26
@@ -533,7 +549,7 @@ the base `Spec` fields listed above):
533549

534550
`gdt` was inspired by [Gabbi](https://github.com/cdent/gabbi), the excellent
535551
Python declarative testing framework. `gdt` tries to bring the same clear,
536-
concise test definitions to the world of Golang functional testing.
552+
concise test definitions to the world of Go functional testing.
537553

538554
The Go gopher logo, from which gdt's logo was derived, was created by Renee
539555
French.

errors/parse.go

+30-2
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,24 @@ var (
5353
ErrExpectedScalarOrSequence = fmt.Errorf(
5454
"%w: expected scalar or sequence of scalars field", ErrParse,
5555
)
56-
// ErrParseTimeout indicates that the timeout specification was not valid.
56+
// ErrExpectedTimeout indicates that the timeout specification was not
57+
// valid.
5758
ErrExpectedTimeout = fmt.Errorf(
5859
"%w: expected timeout specification", ErrParse,
5960
)
60-
// ErrParseWait indicates that the wait specification was not valid.
61+
// ErrExpectedWait indicates that the wait specification was not valid.
6162
ErrExpectedWait = fmt.Errorf(
6263
"%w: expected wait specification", ErrParse,
6364
)
65+
// ErrExpectedRetry indicates that the retry specification was not valid.
66+
ErrExpectedRetry = fmt.Errorf(
67+
"%w: expected retry specification", ErrParse,
68+
)
69+
// ErrInvalidRetryAttempts indicates that the retry attempts was not
70+
// positive.
71+
ErrInvalidRetryAttempts = fmt.Errorf(
72+
"%w: invalid retry attempts", ErrParse,
73+
)
6474
// ErrFileNotFound is returned when a file path does not exist for a
6575
// create/apply/delete target.
6676
ErrFileNotFound = fmt.Errorf(
@@ -158,6 +168,24 @@ func ExpectedWaitAt(node *yaml.Node) error {
158168
)
159169
}
160170

171+
// ExpectedRetryAt returns an ErrExpectedRetry error annotated with the
172+
// line/column of the supplied YAML node.
173+
func ExpectedRetryAt(node *yaml.Node) error {
174+
return fmt.Errorf(
175+
"%w at line %d, column %d",
176+
ErrExpectedRetry, node.Line, node.Column,
177+
)
178+
}
179+
180+
// InvalidRetryAttempts returns an ErrInvalidRetryAttempts error annotated with
181+
// the line/column of the supplied YAML node.
182+
func InvalidRetryAttempts(node *yaml.Node, attempts int) error {
183+
return fmt.Errorf(
184+
"%w of %d at line %d, column %d",
185+
ErrInvalidRetryAttempts, attempts, node.Line, node.Column,
186+
)
187+
}
188+
161189
// UnknownSourceType returns an ErrUnknownSourceType error describing the
162190
// supplied parameter type.
163191
func UnknownSourceType(source interface{}) error {

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.21
44

55
require (
66
github.com/PaesslerAG/jsonpath v0.1.1
7+
github.com/cenkalti/backoff v2.2.1+incompatible
78
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
89
github.com/google/uuid v1.3.0
910
github.com/samber/lo v1.38.1

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v
33
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
44
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
55
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
6+
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
7+
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
68
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
79
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
810
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

plugin/exec/eval.go

-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ func (s *Spec) Eval(ctx context.Context, t *testing.T) *result.Result {
3131
}
3232
a := newAssertions(s.Assert, ec, outbuf, errbuf)
3333
if !a.OK(ctx) {
34-
for _, fail := range a.Failures() {
35-
t.Error(fail)
36-
}
3734
if s.On != nil {
3835
if s.On.Fail != nil {
3936
outbuf.Reset()

0 commit comments

Comments
 (0)