The document introduces go-testdeep, a testing framework for Go that provides a custom comparison engine and operators to easily test nested structures, along with features like accurate error reports, table driven tests, and HTTP request testing. It explains how go-testdeep avoids boilerplate code through methods like Cmp() and shortcuts, allows powerful matching with 62 operators, and encapsulates the testing.T within a td.T instance for additional capabilities.
4. Go testing
Why a test framework?
To avoid boilerplate code, especially error reports �
�
import "testing"
func TestGetPerson(t *testing.T) {
person, err := GetPerson("Bob")
if err != nil {
t.Errorf("GetPerson returned error %s", err)
} else {
if person.Name != "Bob" {
t.Errorf(`Name: got=%q expected="Bob"`, person.Name)
}
if person.Age != 42 {
t.Errorf("Age: got=%s expected=42", person.Age)
}
}
}
4
5. Using a test framework
For example using go-testdeep �
�
import (
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestGetPerson(t *testing.T) {
person, err := GetPerson("Bob")
if td.CmpNoError(t, err, "GetPerson does not return an error") {
td.Cmp(t, person, Person{Name: "Bob", Age: 42}, "GetPerson returns Bob")
}
}
In most cases, there is not even test names �
�
func TestGetPerson(t *testing.T) {
person, err := GetPerson("Bob")
if td.CmpNoError(t, err) {
td.Cmp(t, person, Person{Name: "Bob", Age: 42})
}
}
5
6. Why go-testdeep instead of an existing framework?
Custom comparison engine allowing the use of powerful operators
Accurate and colored error reports
62 operators to match in all circumstances
Fully documented with plenty examples
Consistent API: got parameter is always before expected one
Very few basic functions, all others are operators shortcuts (args... allow to name tests):
Cmp(t TestingT, got, expected interface{}, args ...interface{}) bool
CmpError(t TestingT, got error, args ...interface{}) bool
CmpFalse(t TestingT, got interface{}, args ...interface{}) bool
CmpLax(t TestingT, got, expected interface{}, args ...interface{}) bool
CmpNoError(t TestingT, got error, args ...interface{}) bool
CmpNot(t TestingT, got, notExpected interface{}, args ...interface{}) bool
CmpNotPanic(t TestingT, fn func(), args ...interface{}) bool
CmpPanic(t TestingT, fn func(), expectedPanic interface{}, args ...interface{}) bool
CmpTrue(t TestingT, got interface{}, args ...interface{}) bool
6
7. Why go-testdeep instead of an existing framework? (part 2)
Unique anchoring feature, to easily test literals
JSON content testable like never before
Table driven tests are simple to write and maintain
Comparison engine can be configured to be lax, to ignore struct unexported fields, to treat
specifically some types, to display N errors before giving up
Efficient flattening of []X slices into ...interface{} of variadic functions
A function returning several values can be tested in one call, thanks to tuples
tdhttp helper allows to easily test HTTP APIs regardless of the web framework used
tdsuite helper handles consistent and flexible suites of tests
Probably some others reasons you will discover by yourself :) 7
8. Test names
As many testing frameworks, tests can optionally be named �
�
td.Cmp(t, got, "Bob", `Hey! got has to be "Bob" here!`)
Each Cmp* function cleverly accepts fmt.Fprintf or fmt.Fprint parameters in args
func Cmp(t TestingT, got, expected interface{}, args ...interface{}) bool
The doc says:
// "args..." are optional and allow to name the test. This name is
// used in case of failure to qualify the test. If len(args) > 1 and
// the first item of "args" is a string and contains a '%' rune then
// fmt.Fprintf is used to compose the name, else "args" are passed to
// fmt.Fprint. Do not forget it is the name of the test, not the
// reason of a potential failure.
So, no risk of mistake between Cmp and a (nonexistent) Cmpf: only use Cmp! �
�
td.Cmp(t, got, 12, "Check got is ", 12) → fmt.Fprint
td.Cmp(t, got, 12, "Check got is %d", 12) → fmt.Fprintf
td.Cmp(t, got, 12, lastErr) → fmt.Fprint
8
9. Custom comparison engine
Derived from reflect.DeepEqual and heavily modified to integrate operators handling
It allows go-testdeep to know exactly where a test fails in a big structure, and even to
continue testing in this structure to report several mismatches at the same time (up to 10
by default)
Reports are also very accurate and colorized, instead of awful diffs you see elsewhere…
service_test.go:449: Failed test 'Service is OK'
SERVICE.Owner.Age: values differ
got: 22
expected: 40 ≤ got ≤ 45
[under operator Between at service_test.go:451]
SERVICE.Owner.Name: values differ
got: "Bob"
expected: "Alice"
[under operator Struct at service_test.go:450]
This is how we got here:
testOneService() internal/service/service_test.go:449
TestService() internal/service/service_test.go:430
only displayed if call stack is deep
or if not at the root of the module
to help to find quickly the error
only displayed if the expected
value belongs to an operator
test name if provided
location & reason of failure
9
10. 62 operators to match in all circumstances
Some examples, see the expected (3rd) parameter �
�
here ↴
td.Cmp(t, age, td.Between(40, 45))
td.Cmp(t, headers, td.ContainsKey("X-Ovh"))
td.Cmp(t, err, td.Contains("Internal server error"))
td.Cmp(t, grants, td.Len(td.Gt(2)))
td.Cmp(t, price, td.N(float64(12.03), float64(0.01)))
td.Cmp(t, name, td.Re(`^[A-Z][A-Za-z-]+z`))
td.Cmp(t, ids, td.Set(789, 456, 123))
td.Cmp(t, tags, td.SuperMapOf(map[string]bool{"enabled": true, "shared": true}, nil))
All operators follow
All Contains Isa MapEach NotNil Smuggle SuperJSONOf
Any ContainsKey JSON N NotZero SStruct SuperMapOf
Array Delay JSONPointer NaN PPtr String SuperSetOf
ArrayEach Empty Keys Nil Ptr Struct SuperSliceOf
Bag Gt Lax None Re SubBagOf Tag
Between Gte Len Not ReAll SubJSONOf TruncTime
Cap HasPrefix Lt NotAny Set SubMapOf Values
Catch HasSuffix Lte NotEmpty Shallow SubSetOf Zero
Code Ignore Map NotNaN Slice SuperBagOf
10
11. Almost all operators have shortcuts
Always the same pattern
td.Cmp(t, got, td.HasPrefix(expectedPrefix), …) → td.CmpHasPrefix(t, got, expectedPrefix, …)
td.Cmp(t, got, td.HasSuffix(expectedSuffix), …) → td.CmpHasSuffix(t, got, expectedSuffix, …)
¯¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯¯
td.Cmp(t, got, td.NotEmpty(), …) → td.CmpNotEmpty(t, got, …)
¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯
You just understood CmpNot and CmpLax were in fact shortcuts :)
td.Cmp(t, got, td.Not(notExpected)) → td.CmpNot(t, got, notExpected)
td.Cmp(t, got, td.Lax(expected)) → td.CmpLax(t, got, expected)
¯¯¯ ¯¯¯
Using a shortcut is not mandatory, it could just be more readable in some cases (or not)
4 operators without shortcut: Catch, Delay, Ignore, Tag, because having a shortcut in these
cases is a nonsense 11
12. Matching nested structs/slices/maps
Take this structure returned by a GetPerson function:
type Person struct {
ID int64 `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Children []*Person `json:"children"`
}
We want to check:
• Bob, is 40 to 45 year-old
• has a non-zero ID
• has 2 children
◦ Alice, 20 year-old, and Brian, 18 year-old
◦ both with a non-zero ID too
◦ both without any children 12
13. Operators in nested structs/slices/maps — 1/4 classic way, like others
Like other frameworks, we can do �
�
got := GetPerson("Bob")
td.Cmp(t, got.ID, td.NotZero())
td.Cmp(t, got.Name, "Bob")
td.Cmp(t, got.Age, td.Between(40, 45))
if td.Cmp(t, got.Children, td.Len(2)) {
// Alice
td.Cmp(t, got.Children[0].ID, td.NotZero())
td.Cmp(t, got.Children[0].Name, "Alice")
td.Cmp(t, got.Children[0].Age, 20)
td.Cmp(t, got.Children[0].Children, td.Len(0))
// Brian
td.Cmp(t, got.Children[1].ID, td.NotZero())
td.Cmp(t, got.Children[1].Name, "Brian")
td.Cmp(t, got.Children[1].Age, 18)
td.Cmp(t, got.Children[1].Children, td.Len(0))
}
Exercise: replace the following shortcuts in the code above ↑ �
�
CmpNotZero(t, …) — CmpBetween(t, …) — CmpLen(t, …)
13
14. Operators in nested structs/slices/maps — 2/4 using SStruct operator
SStruct is the strict-Struct operator
SStruct(model interface{}, expectedFields StructFields)
Strict because omitted fields are checked against their zero value, instead of ignoring them
�
�
td.Cmp(t, GetPerson("Bob"),
td.SStruct(Person{Name: "Bob"},
td.StructFields{
"ID": td.NotZero(),
"Age": td.Between(40, 45),
"Children": td.Bag(
td.SStruct(&Person{Name: "Alice", Age: 20},
td.StructFields{"ID": td.NotZero()}),
td.SStruct(&Person{Name: "Brian", Age: 18},
td.StructFields{"ID": td.NotZero()}),
),
},
))
14
15. Operators in nested structs/slices/maps — 3/4 using JSON operator
JSON allows to compare the JSON representation (comments are allowed!) �
�
td.Cmp(t, GetPerson("Bob"), td.JSON(`
{
"id": $1, // ← placeholder (could be "$1" or $BobAge, see JSON operator doc)
"name": "Bob",
"age": Between(40, 45), // yes, most operators are embedable
"children": [
{
"id": NotZero(),
"name": "Alice",
"age": 20,
"children": Empty(), /* null is "empty" */
},
{
"id": NotZero(),
"name": "Brian",
"age": 18,
"children": Nil(),
}
]
}`,
td.Catch(&bobID, td.NotZero()), // $1 catches the ID of Bob on the fly and sets bobID var
))
15
16. Operators in nested structs/slices/maps — 3/4 using JSON + Bag ops
JSON allows to compare the JSON representation (comments are allowed!) �
�
td.Cmp(t, GetPerson("Bob"), td.JSON(`
{
"id": $1, // ← placeholder (could be "$1" or $BobAge, see JSON operator doc)
"name": "Bob",
"age": Between(40, 45), // yes, most operators are embedable
"children": Bag( // ← Bag HERE
{
"id": NotZero(),
"name": "Brian",
"age": 18,
"children": Nil(),
},
{
"id": NotZero(),
"name": "Alice",
"age": 20,
"children": Empty(), /* null is "empty" */
},
)
}`,
td.Catch(&bobID, td.NotZero()), // $1 catches the ID of Bob on the fly and sets bobID var
))
16
17. Operators in nested structs/slices/maps — 4/4 using anchoring
Anchoring feature allows to put operators directly in literals
To keep track of anchors, a td.T instance is needed �
�
assert := td.Assert(t)
assert.Cmp(GetPerson("Bob"),
Person{
ID: assert.A(td.Catch(&bobID, td.NotZero())).(int64),
Name: "Bob",
Age: assert.A(td.Between(40, 45)).(int),
Children: []*Person{
{
ID: assert.A(td.NotZero(), int64(0)).(int64),
Name: "Alice",
Age: 20,
},
{
ID: assert.A(td.NotZero(), int64(0)).(int64),
Name: "Brian",
Age: 18,
},
},
})
17
18. Anatomy of an anchor
Anchors are created using A (or its alias Anchor) method of *td.T
It generates a specific value that can be retrieved during the comparison process
func (t *T) A(operator TestDeep, model ...interface{}) interface{}
│ └ mandatory if the type can not be guessed from the operator
└ the operator to use
// model is not needed when operator knows the type behind the operator
assert.A(td.Between(40, 45)).(int)
// model is mandatory if the type behind the operator cannot be guessed
assert.A(td.NotZero(), int64(666)).(int64)
// for reflect lovers, they can use the longer version
assert.A(td.NotZero(), reflect.TypeOf(int64(666))).(int64)
Conflicts are possible, so be careful with 8 and 16 bits types
Work for pointers, slices, maps, but not available for bool types
Specific handling is needed for structs, see AddAnchorableStructType function 18
19. Encapsulating testing.T
Instead of doing �
�
func TestVals(t *testing.T) {
got := GetPerson("Bob")
td.Cmp(t, got.Age, td.Between(40, 45))
td.Cmp(t, got.Children, td.Len(2))
}
one can build a td.T instance encapsulating the testing.T one �
�
func TestVals(t *testing.T) {
assert := td.Assert(t)
got := GetPerson("Bob")
assert.Cmp(got.Age, td.Between(40, 45))
assert.Cmp(got.Children, td.Len(2))
}
Building a td.T instance provides some advantages over using td.Cmp* functions directly19
20. td.T — Introduction — 1/6
type T struct {
testing.TB // implemented by *testing.T
Config ContextConfig // defaults to td.DefaultContextConfig
}
See testing.TB interface, ContextConfig struct and DefaultContextConfig variable
Construction:
func NewT(t testing.TB, config ...ContextConfig) *T // inherit properties from t
func Assert(t testing.TB, config ...ContextConfig) *T // test failures are not fatal
func Require(t testing.TB, config ...ContextConfig) *T // t.Fatal if a test fails
func AssertRequire(t testing.TB, config ...ContextConfig) (*T, *T) // Assert() + Require()
Configuring *td.T instance (return a new instance):
func (t *T) BeLax(enable ...bool) *T // enable/disable strict type comparison
func (t *T) FailureIsFatal(enable ...bool) *T // enable/disable failure "fatality"
func (t *T) IgnoreUnexported(types ...interface{}) *T // ignore unexported fields of some structs
func (t *T) RootName(rootName string) *T // change data root name, "DATA" by default
func (t *T) UseEqual(types ...interface{}) *T // delegate cmp to Equal() method if available
20
22. td.T — Shortcuts — 3/6
Shortcuts, as for td functions, follow always the same pattern:
t.Cmp(got, td.HasPrefix(expected), …) → t.HasPrefix(got, expected, …)
t.Cmp(got, td.HasSuffix(expected), …) → t.HasSuffix(got, expected, …)
¯¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯¯
t.Cmp(got, td.NotEmpty(), …) → t.NotEmpty(t, got, …)
¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯
So yes, T.Not is in fact a shortcut:
t.Cmp(got, td.Not(notExpected)) → t.Not(got, notExpected)
¯¯¯ ¯¯¯
The only exception is T.CmpLax method, shortcut of td.Lax operator, it is more relevant
than a T.Lax method
Same 4 operators without shortcut: Catch, Delay, Ignore, Tag, because having a shortcut in
these cases is a nonsense 22
23. td.T — Anchoring — 4/6
Anchoring related methods:
func (t *T) A(operator TestDeep, model ...interface{}) interface{}
func (t *T) Anchor(operator TestDeep, model ...interface{}) interface{}
func (t *T) AnchorsPersistTemporarily() func()
func (t *T) DoAnchorsPersist() bool
func (t *T) ResetAnchors()
func (t *T) SetAnchorsPersist(persist bool)
A and Anchor allow both to anchor an operator, the first is just shorter to write
By default, anchoring is effective only for the next Cmp* call, but this can be overridden
thanks to SetAnchorsPersist and AnchorsPersistTemporarily
Useful for helpers writers or table driven tests afficionados :) 23
25. td.T — Advanced usage — 6/6
WithCmpHooks allows to register comparison functions for some types �
�
assert = assert.WithCmpHooks(
func (got, expected reflect.Value) bool {
return td.EqDeeply(got.Interface(), expected.Interface())
},
(time.Time).Equal, // bypasses the UseEqual flag
)
x := 123
assert.Cmp(reflect.ValueOf(x), reflect.ValueOf(123)) // succeeds
WithSmuggleHooks allows to register functions to alter the data before comparing it �
�
assert = assert.WithSmuggleHooks(
func (got int) bool { return got != 0 }, // each int is changed to a bool
strconv.Atoi, // each string is converted to an int
)
assert.Cmp("123", 123) // succeeds
Smuggle hooks are run just before Cmp hooks and are not run again for their returned
value 25
26. Table driven tests — the heaven of go-testdeep operators
�
�
var personTests = []struct {
name string
expectedErr td.TestDeep
expectedPerson td.TestDeep
}{
{"Bob", nil, td.JSON(`{"name":"Bob","age":41,"id":NotZero(),"children":Len(2)}`)},
{"Marcel", td.String("User not found"), td.Nil()},
{"Alice", nil, td.SStruct(&Person{Name: "Alice", Age: 20}, td.StructFields{"ID": td.NotZero()})},
{"Brian", nil, td.SStruct(&Person{Name: "Brian", Age: 18}, td.StructFields{"ID": td.NotZero()})},
}
=== RUN TestGetPerson
func TestGetPerson(t *testing.T) { === RUN TestGetPerson/Bob
assert := td.Assert(t) === RUN TestGetPerson/Marcel
for _, pt := range personTests { === RUN TestGetPerson/Alice
assert.Run(pt.name, func(assert *td.T) { === RUN TestGetPerson/Brian
person, err := GetPerson(pt.name) --- PASS: TestGetPerson (0.00s)
assert.Cmp(err, pt.expectedErr) --- PASS: TestGetPerson/Bob (0.00s)
assert.Cmp(person, pt.expectedPerson) --- PASS: TestGetPerson/Marcel (0.00s)
}) --- PASS: TestGetPerson/Alice (0.00s)
} --- PASS: TestGetPerson/Brian (0.00s)
} PASS
26
27. Operators types
There is two kinds of operators: classic ones and smuggler ones
A smuggler operator is an operator able to transform the value (by changing its value or
even its type) before comparing it
Smuggler operators follows:
Cap Contains JSONPointer Lax PPtr Smuggle Values
Catch ContainsKey Keys Len Ptr Tag
Some examples �
�
td.Cmp(t, slice, td.Len(td.Between(3, 4)))
td.Cmp(t, headers, td.ContainsKey(td.HasPrefix("X-Ovh")))
td.Cmp(t, &age, td.Ptr(td.Gt(18)))
td.Cmp(t, ageStr, td.Smuggle(strconv.Atoi, td.Catch(&age, td.Gt(18))))
td.Cmp(t, body1, td.Smuggle(json.RawMessage{}, td.JSON(`{"name": $br}`, td.Tag("br", "Brian"))))
td.Cmp(t, body2, td.Smuggle("Service.Owner.Children[0].Name", "Alice"))
td.Cmp(t, body2, td.JSONPointer("/service/owner/children/0/name", "Alice"))
td.Cmp(t, headers, td.Keys(td.SuperSetOf("X-Ovh", "X-Remote-IP")))
td.Cmp(t, err, td.Contains("integrity constraint"))
td.Cmp(t, bytes, td.Lax("pipo bingo!"))
27
28. Custom operators — for beginners
Two operators Code and Smuggle allow to achieve what others cannot for your very
special use cases
Below, only the year of time.Time is important
With Code, you do the test �
�
td.Cmp(t, gotTime,
td.Code(func(date time.Time) bool {
return date.Year() == 2018
}))
With Smuggle, you transform the value and delegate the test �
�
td.Cmp(t, gotTime,
td.Smuggle(func(date time.Time) int { return date.Year() },
td.Between(2010, 2020)),
)
Discover more features in each operator description 28
29. Custom operators — master class 1/2
Sometimes you need to test something over and over, let's do your own operator!
func CheckDateGte(t time.Time, catch *time.Time) td.TestDeep {
op := td.Gte(t.Truncate(time.Millisecond))
if catch != nil {
op = td.Catch(catch, op)
}
return td.All(
td.HasSuffix("Z"),
td.Smuggle(func(s string) (time.Time, error) {
t, err := time.Parse(time.RFC3339Nano, s)
if err == nil && t.IsZero() {
err = errors.New("zero time")
}
return t, err
}, op))
}
Ensures that a RFC3339-stringified date has "Z" suffix and is well RFC3339-formatted. Then
check it is greater or equal than t truncated to milliseconds
Additionally, if catch is non-nil, stores the resulting time.Time in *catch 29
30. Custom operators — master class 2/2
This new operator is useful when used with JSON operator �
�
func TestCreateArticle(t *testing.T) {
type Article struct {
ID int64 `json:"id"`
Code string `json:"code"`
CreatedAt time.Time `json:"created_at"`
}
var createdAt time.Time
beforeCreation := time.Now()
td.Cmp(t, CreateArticle("Car"),
td.JSON(`{"id": NotZero(), "code": "Car", "created_at": $1}`,
CheckDateGte(beforeCreation, &createdAt)))
// If the test succeeds, then "created_at" value is well a RFC3339
// datetime in UTC timezone and its value is directly exploitable as
// time.Time thanks to createdAt variable
t.Logf("Article created at %s", createdAt)
}
30
31. The tdhttp helper or how to easily test a http.Handler
And now you want to test your API, aka a http.Handler
Thanks to the tdhttp helper and all these *#@!# operators, nothing is easier! �
�
import (
"testing"
"github.com/maxatome/go-testdeep/helpers/tdhttp"
"github.com/maxatome/go-testdeep/td"
)
func TestMyApi(t *testing.T) {
ta := tdhttp.NewTestAPI(t, myAPI)
var id int64
ta.Name("Retrieve a person").
Get("/person/Bob", "Accept", "application/json").
CmpStatus(http.StatusOK).
CmpHeader(td.ContainsKey("X-Custom-Header")).
CmpJSONBody(td.JSON(`{"id": $1, "name": "Bob", "age": 26}`, td.Catch(&id, td.NotZero())))
t.Logf("Did the test succeeded? %t, ID of Bob is %d", !ta.Failed(), id)
}
31
32. tdhttp for any framework
myAPI is here a http.Handler ↴
ta := tdhttp.NewTestAPI(t, myAPI)
That's pretty cool as:
• gin-gonic *gin.Engine
• net/http *http.ServeMux
• go-swagger restapi.configureAPI() or restapi.HandlerAPI() return value
• Beego, echo, gorilla/mux, HttpRouter, pat
• any other?
implement all http.Handler! Check the HTTP frameworks section of the FAQ
You can now change your web framework and keep your test framework :) 32
33. tdhttp for any content
Ready to use GET, POST, PATCH, PUT, DELETE, HEAD requests, but can be fed by any
already created http.Request
Supports out of the box application/x-www-form-urlencoded, application/json,
application/xml and multipart/form-data encoding & cookies
Supports string and []byte bodies so you can handle the encoding by yourself
Operator anchoring works out of the box too �
�
func TestMyApiAnchor(t *testing.T) {
ta := tdhttp.NewTestAPI(t, myAPI)
var id int64
ta.Get("/person/Bob", "Accept", "application/json").
CmpStatus(http.StatusOK).
CmpJSONBody(Person{
ID: ta.A(td.Catch(&id, td.NotZero())).(int64),
Name: "Bob",
Age: ta.A(td.Between(25, 30)).(int),
})
}
33
34. tdhttp with easy debug
Thanks to AutoDumpResponse, Or & OrDumpResponse methods, you can inpect the
HTTP response to see what happened in case of failure �
�
func TestMyApiDumpIfFailure(t *testing.T) {
ta := tdhttp.NewTestAPI(t, myAPI)
var id int64
ta.Name("Person creation").
PostJSON("/person", PersonNew{Name: "Bob"}).
CmpStatus(http.StatusCreated).
CmpJSONBody(td.JSON(`
{
"id": $1,
"name": "Bob",
"created_at": $2
}`,
td.Catch(&id, td.NotZero()), // catch just created ID
td.Gte(ta.SentAt()), // check that created_at is ≥ request sent date
)).
OrDumpResponse() // if some test fails, the response is dumped
}
34
35. The tdsuite helper — Simple example — 1/3
Or tests suites with one hand tied behind your back �
�
import (
"testing"
"github.com/maxatome/go-testdeep/helpers/tdsuite"
"github.com/maxatome/go-testdeep/td"
)
func TestPerson(t *testing.T) {
tdsuite.Run(t, &PersonSuite{ // entrypoint of the suite
db: InitDB(), // a DB handler probably used in each tests
})
}
type PersonSuite struct{ db MyDBHandler }
func (ps *PersonSuite) TestGet(assert *td.T) {
// …
}
func (ps *PersonSuite) TestPost(assert, require *td.T) {
// …
}
35
36. tdsuite — All hooks — 2/3
Each method Test* of the suite is run in a sub-test
assert or assert+require, you choose
Several hooks can be implemented:
Setup(t *td.T) error
Destroy(t *td.T) error
PreTest(t *td.T, testName string) error
PostTest(t *td.T, testName string) error
BetweenTests(t *td.T, previousTestName, nextTestName string) error
Setup and Destroy are respectively called before any test is run and after all tests ran
PreTest and PostTest are respectively called before and after each test
BetweenTests is called between 2 consecutive tests (after the PostTest call of previous test
but before the PreTest call of next test)
If a hook returns a non-nil error, the suite fails immediately 36
37. tdsuite — Composing suites — 3/3
func TestAnother(t *testing.T) { // �
� https://goplay.tools/snippet/5cbM9eHbx33
tdsuite.Run(t, &AnotherSuite{}) // entrypoint of the suite
}
// BaseSuite is the base test suite used by all tests suite using the DB.
type BaseSuite struct{ db MyDBHandler }
func (bs *BaseSuite) Setup(t *td.T) (err error) {
bs.db, err = InitDB()
return
}
func (bs *BaseSuite) Destroy(t *td.T) error {
return bs.db.Exec(`TRUNCATE x, y, z CASCADE`)
}
// AnotherSuite is the final test suite blah blah blah…
type AnotherSuite struct{ BaseSuite }
func (as *AnotherSuite) TestGet(assert, require *td.T) {
res, err := as.db.Query(`SELECT 42`)
require.CmpNoError(err)
assert.Cmp(res, 42)
}
37
38. End
Some stats:
• +37k lines of go code
• 99.855% code coverage
• go report A+
• 24 releases since 2018
Links:
• Home page
• godoc
• godoc API tester — tdhttp
• godoc testing suite — tdsuite
• godoc helpers utils — tdutil
• Github ← don't forget to � :) 38