Skip to content

Commit 926e941

Browse files
authored
Implement BDD-style bifurcated evaluation of nested test branches (#14)
- Add bdd package to support proper BDD-style bifurcated evaluation of test branches - Update readme to include description, rationale and usage for the `bdd` package - Add code snippets and link in README - Update release notes for v1.1.0
1 parent 292139b commit 926e941

File tree

10 files changed

+508
-28
lines changed

10 files changed

+508
-28
lines changed

README.md

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# go-testpredicate
22

3-
Test assertions library using predicate-like syntax, producing extensive
4-
diagnostics output
3+
Test assertions library using a test predicate style syntax, and producing
4+
extensive diagnostics output
55

66
[![Latest](
77
https://img.shields.io/github/v/tag/maargenton/go-testpredicate?color=blue&label=latest&logo=go&logoColor=white&sort=semver)](
@@ -19,9 +19,10 @@ diagnostics output
1919

2020
---------------------------
2121

22-
Package `go-testpredicate` is a test assertions library exposing a
23-
predicate-like syntax that works with Go testing support to provide extensive
24-
diagnostics output and reduces the need to use a debugger on every failing test.
22+
Package `go-testpredicate` is a test assertions library exposing a test
23+
predicate style syntax for use with the built-in Go `testing` package, producing
24+
extensive diagnostics output and reducing the need to use a debugger on every
25+
failing test.
2526

2627
The library contains an extensive collection of built-in predicates covering:
2728

@@ -33,29 +34,40 @@ The library contains an extensive collection of built-in predicates covering:
3334
- set conditions on unordered collections
3435
- panic conditions on code fragment execution
3536

37+
It also includes a BDD-style bifurcated evaluation context, where each test
38+
section is potentially evaluated multiple times in order to evaluate each branch
39+
independently.
40+
3641

3742
## Installation
3843

39-
go get github.com/maargenton/go-testpredicate
44+
```
45+
go get github.com/maargenton/go-testpredicate
46+
```
47+
48+
Optionally, you can add predefined code snippets for your text editor or IDE to
49+
assist in writing your test code. Snippets for VSCode are available
50+
[here](docs/snippets.md)
4051

4152
## Usage
4253

4354
```go
44-
package examples_test
55+
package example_test
4556

4657
import (
4758
"testing"
4859

60+
"github.com/maargenton/go-testpredicate/pkg/bdd"
4961
"github.com/maargenton/go-testpredicate/pkg/require"
5062
"github.com/maargenton/go-testpredicate/pkg/verify"
5163
)
5264

5365
func TestExample(t *testing.T) {
54-
t.Run("Given ", func(t *testing.T) {
66+
bdd.Given(t, "something", func(t *bdd.T) {
5567
require.That(t, 123).ToString().Length().Eq(3)
5668

57-
t.Run("when ", func(t *testing.T) {
58-
t.Run("then ", func(t *testing.T) {
69+
t.When("doing something", func(t *bdd.T) {
70+
t.Then("something happens ", func(t *bdd.T) {
5971
verify.That(t, "123").Eq(123)
6072
verify.That(t, 123).ToString().Length().Eq(4)
6173
})
@@ -67,14 +79,14 @@ func TestExample(t *testing.T) {
6779
Output:
6880
```
6981
--- FAIL: TestExample (0.00s)
70-
--- FAIL: TestFoo/Given_ (0.00s)
71-
--- FAIL: TestFoo/Given_/when_ (0.00s)
72-
--- FAIL: TestFoo/Given_/when_/then_ (0.00s)
73-
usage_test.go:16:
82+
--- FAIL: TestExample/Given_something (0.00s)
83+
--- FAIL: TestExample/Given_something/when_doing_something (0.00s)
84+
--- FAIL: TestExample/Given_something/when_doing_something/then_something_happens_ (0.00s)
85+
example_test.go:17:
7486
expected: value == 123
7587
error: values of type 'string' and 'int' are never equal
7688
value: "123"
77-
usage_test.go:17:
89+
example_test.go:18:
7890
expected: length(value.String()) == 4
7991
value: 123
8092
string: "123"
@@ -206,3 +218,121 @@ func TestStringAPI(t *testing.T) {
206218
verify.That(t, "aBc").ToUpper().Eq("ABC")
207219
}
208220
```
221+
222+
## BDD-style bifurcated tests
223+
224+
### Rationale
225+
226+
First of all, the Go `testing` package is great and the fact that it is
227+
standard, built in and integrated with the Go tooling infrastructure is awesome.
228+
This is why the `go-testpredicate` packages strives to enhance it instead of
229+
replacing it, unlike many other testing packages.
230+
231+
If you look at other unit-testing packages, in other languages, you will find
232+
either traditional xUnit style packages relying on classes to define test suites
233+
and fixtures and test cases, or more recent testing packages (like
234+
[Catch-2](https://github.com/catchorg/Catch2) for C++) that provide, through
235+
other means, ways to define setup and test cases than run independently. The
236+
common pattern is that setup code, that may be shared by multiple test cases, is
237+
usually re-evaluated for every test case so that, despite their potentially
238+
mutating interactions with the setup, test cases don't affect each other.
239+
240+
Some great articles and blog posts have explained how the leverage nested
241+
`t.Run()` calls to structure tests in way that is closer to BDD-style given /
242+
when / then paradigm. Unfortunately, when using thees approaches, and especially
243+
with shared setup sections, the test cases are no longer independent, as all
244+
branches are run sequentially, going up and down each branch and into the next
245+
branch, without resetting the setup.
246+
247+
The `bdd` package in `go-testpredicate` provides a way to write tests with a
248+
BDD-style structure, using the built-in `testing.T`, but evaluating the test
249+
cases in a bifurcated fashion, repeating the evaluation of each entire branch
250+
for every leaf test case, so that test cases are independent from each other
251+
again.
252+
253+
### Usage overview
254+
255+
`bdd.Wrap()` or `bdd.Given()` are the root level function that setup and iterate
256+
through the bifurcated test evaluation context. They define blocks that receive
257+
a `bdd.T` instead of `testing.T`, but `bdd.T` is fully compatible with
258+
`testing.T` and can be used with any third party library that expect either the
259+
`testing.TB` interface or a subset of it (including out own `verify.That()` /
260+
`require.That()`).
261+
262+
Nested and sibling bifurcated branches are defined with `t.Run()` (on `bdd.T`)
263+
or `t.When()` / `t.Then()` for BDD style.
264+
265+
> **IMPORTANT:** In a bifurcated evaluation context, as defined by `bdd.T`, test
266+
> scenarios are run repeatedly in order to evaluate each branch (from root to
267+
> leaf) independently of each other. When a particular branch is being
268+
> evaluated, all the other forks and sub-branches are skipped; the other
269+
> branches are run in separated independent iterations of the scenario.
270+
271+
### Usage, traditional style
272+
273+
```go
274+
package example_test
275+
276+
import (
277+
"testing"
278+
"github.com/maargenton/go-testpredicate/pkg/bdd"
279+
)
280+
281+
func TesTraditional(t *testing.T) {
282+
283+
// Global immutable setup code can go here
284+
285+
bdd.Wrap(t, "Given something", func(t *bdd.T) {
286+
287+
// Local mutable setup code goes here
288+
289+
t.Run("something happens", func(t *bdd.T) {
290+
291+
// When this code runs, the code in following `t.Run()` blocks
292+
// will be skipped.
293+
})
294+
t.Run("something else happens", func(t *bdd.T) {
295+
296+
// When this code runs, all code in preceding `t.Run()` blocks
297+
// has been skipped and did not affect the local setup.
298+
})
299+
})
300+
}
301+
```
302+
303+
### Usage, BDD style
304+
305+
```go
306+
package bdd_test
307+
308+
import (
309+
"testing"
310+
"github.com/maargenton/go-testpredicate/pkg/bdd"
311+
)
312+
313+
func TestBDDStyle(t *testing.T) {
314+
315+
// Global immutable setup code can go here
316+
317+
bdd.Given(t, "something", func(t *bdd.T) {
318+
319+
// Local mutable setup code goes here
320+
321+
t.When("doing something", func(t *bdd.T) {
322+
323+
// or here
324+
325+
t.Then("something happens", func(t *bdd.T) {
326+
327+
// When this code runs, the code in the following `t.Then()`
328+
// blocks will be skipped.
329+
})
330+
t.Then("something else happens", func(t *bdd.T) {
331+
332+
// When this code runs, all code in preceding `t.Then()`
333+
// blocks has been skipped and did not affect the local setup.
334+
})
335+
})
336+
})
337+
}
338+
```

RELEASES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# v1.1.0
2+
3+
## Major Changes
4+
5+
- Add `bdd` sub-package to support BDD-style bifurcated execution of nested test
6+
sections.
7+
8+
19
# v1.0.0
210

311
## Major Changes

docs/snippets.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
## VS Code snippets
2+
3+
In VSCode, open the command palette (⌘⇧P / ^⇧P) and select '_Preferences:
4+
Configure User Snippets_', then '_go.json_' and add any or all of the following
5+
snippets:
6+
- `tfbdd`: inserts the definition of a test function with BDD-style given /when
7+
/ then blocks, using the bifurcated execution context define in the `bdd`
8+
sub-package.
9+
- `tftcs`: inserts the definition of a test function with data-driven test cases
10+
and BDD-style given /when / then blocks, using the bifurcated execution
11+
context define in the `bdd` sub-package.
12+
- `require.That` or `reth`: inserts a placeholder for require.That() statement.
13+
- `verify.That` or `veth`: inserts a placeholder for verify.That() statement.
14+
15+
```json
16+
"test function bdd": {
17+
"prefix": "tfbdd",
18+
"body": [
19+
"func Test$1(t *testing.T) {",
20+
"\tbdd.Given(t, \"${2:something}\", func(t *bdd.T) {",
21+
"\t\tt.When(\"${3:doing something}\", func(t *bdd.T) {",
22+
"\t\t\tt.Then(\"${4:something happens}\", func(t *bdd.T) {",
23+
"\t\t\t\trequire.That(t, ${5:\"123\"}).Eq(${6:123})",
24+
"\t\t\t\t$0",
25+
"\t\t\t})",
26+
"\t\t})",
27+
"\t})",
28+
"}",
29+
],
30+
"description": "Test function with BDD-style given/when/then and require.That"
31+
},
32+
"test function bdd with test cases": {
33+
"prefix": "tftcs",
34+
"body": [
35+
"func Test$1(t *testing.T) {",
36+
"\tvar tcs = []struct {",
37+
"\t\tname string",
38+
"\t}{",
39+
"\t\t{\"${2:test case}\"},",
40+
"\t}",
41+
"\t",
42+
"\tfor _, tc := range tcs {",
43+
"\t\tbdd.Given(t, tc.name, func(t *bdd.T) {",
44+
"\t\t\tt.When(\"${3:doing something}\", func(t *bdd.T) {",
45+
"\t\t\t\tt.Then(\"${4:something happens}\", func(t *bdd.T) {",
46+
"\t\t\t\t\trequire.That(t, ${5:\"123\"}).Eq(${6:123})",
47+
"\t\t\t\t\t$0",
48+
"\t\t\t\t})",
49+
"\t\t\t})",
50+
"\t\t})",
51+
"\t}",
52+
"}",
53+
],
54+
"description": "Test function with BDD-style given/when/then, test cases and require.That"
55+
},
56+
"verify": {
57+
"prefix": "verify.That",
58+
"body": [
59+
"verify.That(t, ${0:\"\"}).Eq(\"\")",
60+
],
61+
"description": "verifty.That()"
62+
},
63+
"require": {
64+
"prefix": "require.That",
65+
"body": [
66+
"require.That(t, ${0:\"\"}).Eq(\"\")",
67+
],
68+
"description": "verifty.That()"
69+
},
70+
```

pkg/bdd/bdd.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Package bdd defines a BDD-style bifurcated evaluation context, compatible
2+
// with the build-in testing package.
3+
package bdd
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
)
9+
10+
// T is a testing context, similar to testing.T, passed to the testing functions
11+
// in the BDD style bifurcated test evaluation. It forwards most of the
12+
// testing.T API by encapsulating the current test context as a `TB` interface;
13+
// only `Parallel()` is dropped as non-relevant.
14+
//
15+
// The `Run()` function has a similar interface as testing.T, but passing a
16+
// bdd.T object for the nested testing context, and evaluating all branches in a
17+
// BDD style bifurcated fashion, where each branch is fully and independently
18+
// re-evaluated for every leaf.
19+
//
20+
// Additional functions `When()` and `Then()` are syntactic sugar on top the
21+
// `Run()` function. `bdd.Given()` is the root function that initializes the
22+
// bifurcated evaluation context and runs all the branches.
23+
type T struct {
24+
TB
25+
t *testing.T
26+
tracker *tracker
27+
}
28+
29+
// Run defines a new fork in the current bifurcated evaluation context.
30+
func (b *T) Run(name string, f func(b *T)) bool {
31+
success := true
32+
if b.tracker.Active() {
33+
success = success && b.t.Run(name, func(t *testing.T) {
34+
f(&T{t, t, b.tracker.SubTracker()})
35+
})
36+
}
37+
return success
38+
}
39+
40+
// When adds syntactic sugar on top of `bdd.T.Run()` and prefixes the name
41+
// of the section with 'when ...'.
42+
func (b *T) When(name string, f func(b *T)) bool {
43+
name = fmt.Sprintf("when %v", name)
44+
return b.Run(name, f)
45+
}
46+
47+
// Then adds syntactic sugar on top of `bdd.T.Run()` and prefixes the name
48+
// of the section with 'then ...'.
49+
func (b *T) Then(name string, f func(b *T)) bool {
50+
name = fmt.Sprintf("then %v", name)
51+
return b.Run(name, f)
52+
}
53+
54+
// Wrap is the root function that wraps the top level testing.T context and
55+
// starts a bifurcated bdd.T evaluation context.
56+
func Wrap(t *testing.T, name string, f func(b *T)) bool {
57+
tracker := &tracker{}
58+
success := true
59+
for tracker.Next() {
60+
if tracker.Active() {
61+
s := t.Run(name, func(t *testing.T) {
62+
f(&T{t, t, tracker.SubTracker()})
63+
})
64+
success = success && s
65+
}
66+
}
67+
return success
68+
}
69+
70+
// Given adds syntactic sugar on top of `bdd.Wrap()` and prefixes the name
71+
// of the section with 'Given ...'.
72+
func Given(t *testing.T, name string, f func(b *T)) bool {
73+
name = fmt.Sprintf("Given %v", name)
74+
return Wrap(t, name, f)
75+
}

0 commit comments

Comments
 (0)