SlideShare a Scribd company logo
1 of 39
Download to read offline
go-testdeep
Maxime Soulé
2022-01-28
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
td.T — Main methods — 2/6
func (t *T) Cmp(got, expected interface{}, args ...interface{}) bool
func (t *T) CmpError(got error, args ...interface{}) bool
func (t *T) CmpLax(got, expected interface{}, args ...interface{}) bool
func (t *T) CmpNoError(got error, args ...interface{}) bool
func (t *T) CmpNotPanic(fn func(), args ...interface{}) bool
func (t *T) CmpPanic(fn func(), expected interface{}, args ...interface{}) bool
func (t *T) False(got interface{}, args ...interface{}) bool
func (t *T) Not(got, notExpected interface{}, args ...interface{}) bool
func (t *T) True(got interface{}, args ...interface{}) bool
func (t *T) Run(name string, f func(t *T)) bool
func (t *T) RunAssertRequire(name string, f func(assert, require *T)) bool
In fact mostly the same as main functions, but:
• CmpFalse() / CmpTrue() → (t *T) False() / (t *T) True()
• CmpNot() → (t *T) Not()
• new (t *T) Run() and (t *T) RunAssertRequire()
See documentation for details 21
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
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
td.T — Simple example — 5/6
�
�
import (
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestExample(t *testing.T) {
assert, require := td.AssertRequire(t)
person, err := GetPerson("Bob")
require.CmpNoError(err) // exits test if it fails
assert.RootName("PERSON").
Cmp(person, &Person{
ID: assert.A(td.NotZero(), int64(0)).(int64),
Name: "Bob",
Age: assert.A(td.Between(40, 45)).(int),
Children: assert.A(td.Len(2), ([]*Person)(nil)).([]*Person),
})
}
24
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Thank you
Maxime Soulé
2022-01-28
https://go-testdeep.zetta.rocks/

More Related Content

What's hot

Naïveté vs. Experience
Naïveté vs. ExperienceNaïveté vs. Experience
Naïveté vs. ExperienceMike Fogus
 
The Macronomicon
The MacronomiconThe Macronomicon
The MacronomiconMike Fogus
 
Functional Programming & Event Sourcing - a pair made in heaven
Functional Programming & Event Sourcing - a pair made in heavenFunctional Programming & Event Sourcing - a pair made in heaven
Functional Programming & Event Sourcing - a pair made in heavenPawel Szulc
 
Unit testing with PHPUnit
Unit testing with PHPUnitUnit testing with PHPUnit
Unit testing with PHPUnitferca_sl
 
Beyond javascript using the features of tomorrow
Beyond javascript   using the features of tomorrowBeyond javascript   using the features of tomorrow
Beyond javascript using the features of tomorrowAlexander Varwijk
 
Ast transformations
Ast transformationsAst transformations
Ast transformationsHamletDRC
 
Advanced Python, Part 2
Advanced Python, Part 2Advanced Python, Part 2
Advanced Python, Part 2Zaar Hai
 
Code as data as code.
Code as data as code.Code as data as code.
Code as data as code.Mike Fogus
 
Apache Commons - Don\'t re-invent the wheel
Apache Commons - Don\'t re-invent the wheelApache Commons - Don\'t re-invent the wheel
Apache Commons - Don\'t re-invent the wheeltcurdt
 
JavaScript Survival Guide
JavaScript Survival GuideJavaScript Survival Guide
JavaScript Survival GuideGiordano Scalzo
 
Game unleashedjavascript
Game unleashedjavascriptGame unleashedjavascript
Game unleashedjavascriptReece Carlson
 
AST Transformations
AST TransformationsAST Transformations
AST TransformationsHamletDRC
 
JavaScript 1 for high school
JavaScript 1 for high schoolJavaScript 1 for high school
JavaScript 1 for high schooljekkilekki
 

What's hot (20)

Strings and Characters
Strings and CharactersStrings and Characters
Strings and Characters
 
Naïveté vs. Experience
Naïveté vs. ExperienceNaïveté vs. Experience
Naïveté vs. Experience
 
Groovy
GroovyGroovy
Groovy
 
The Macronomicon
The MacronomiconThe Macronomicon
The Macronomicon
 
Functional Programming & Event Sourcing - a pair made in heaven
Functional Programming & Event Sourcing - a pair made in heavenFunctional Programming & Event Sourcing - a pair made in heaven
Functional Programming & Event Sourcing - a pair made in heaven
 
Unit testing with PHPUnit
Unit testing with PHPUnitUnit testing with PHPUnit
Unit testing with PHPUnit
 
Hidden Gems of Ruby 1.9
Hidden Gems of Ruby 1.9Hidden Gems of Ruby 1.9
Hidden Gems of Ruby 1.9
 
Clean coding-practices
Clean coding-practicesClean coding-practices
Clean coding-practices
 
Beyond javascript using the features of tomorrow
Beyond javascript   using the features of tomorrowBeyond javascript   using the features of tomorrow
Beyond javascript using the features of tomorrow
 
JavaScript Primer
JavaScript PrimerJavaScript Primer
JavaScript Primer
 
Ast transformations
Ast transformationsAst transformations
Ast transformations
 
Advanced Python, Part 2
Advanced Python, Part 2Advanced Python, Part 2
Advanced Python, Part 2
 
Oscon 2010 Specs talk
Oscon 2010 Specs talkOscon 2010 Specs talk
Oscon 2010 Specs talk
 
Code as data as code.
Code as data as code.Code as data as code.
Code as data as code.
 
Apache Commons - Don\'t re-invent the wheel
Apache Commons - Don\'t re-invent the wheelApache Commons - Don\'t re-invent the wheel
Apache Commons - Don\'t re-invent the wheel
 
JavaScript Survival Guide
JavaScript Survival GuideJavaScript Survival Guide
JavaScript Survival Guide
 
Game unleashedjavascript
Game unleashedjavascriptGame unleashedjavascript
Game unleashedjavascript
 
AST Transformations
AST TransformationsAST Transformations
AST Transformations
 
Groovy closures
Groovy closuresGroovy closures
Groovy closures
 
JavaScript 1 for high school
JavaScript 1 for high schoolJavaScript 1 for high school
JavaScript 1 for high school
 

Similar to Go testdeep

Stored Procedures and MUMPS for DivConq
 Stored Procedures and  MUMPS for DivConq  Stored Procedures and  MUMPS for DivConq
Stored Procedures and MUMPS for DivConq eTimeline, LLC
 
C++11 - A Change in Style - v2.0
C++11 - A Change in Style - v2.0C++11 - A Change in Style - v2.0
C++11 - A Change in Style - v2.0Yaser Zhian
 
Bdd: Tdd and beyond the infinite
Bdd: Tdd and beyond the infiniteBdd: Tdd and beyond the infinite
Bdd: Tdd and beyond the infiniteGiordano Scalzo
 
Lecture 15_Strings and Dynamic Memory Allocation.pptx
Lecture 15_Strings and  Dynamic Memory Allocation.pptxLecture 15_Strings and  Dynamic Memory Allocation.pptx
Lecture 15_Strings and Dynamic Memory Allocation.pptxJawadTanvir
 
SWP - A Generic Language Parser
SWP - A Generic Language ParserSWP - A Generic Language Parser
SWP - A Generic Language Parserkamaelian
 
Real life-coffeescript
Real life-coffeescriptReal life-coffeescript
Real life-coffeescriptDavid Furber
 
Spock Framework - Slidecast
Spock Framework - SlidecastSpock Framework - Slidecast
Spock Framework - SlidecastDaniel Kolman
 
Making JavaScript Libraries More Approachable
Making JavaScript Libraries More ApproachableMaking JavaScript Libraries More Approachable
Making JavaScript Libraries More ApproachablePamela Fox
 
13 Strings and Text Processing
13 Strings and Text Processing13 Strings and Text Processing
13 Strings and Text ProcessingIntro C# Book
 
Programming with Java: the Basics
Programming with Java: the BasicsProgramming with Java: the Basics
Programming with Java: the BasicsJussi Pohjolainen
 

Similar to Go testdeep (20)

Stored Procedures and MUMPS for DivConq
 Stored Procedures and  MUMPS for DivConq  Stored Procedures and  MUMPS for DivConq
Stored Procedures and MUMPS for DivConq
 
Scala introduction
Scala introductionScala introduction
Scala introduction
 
C++11 - A Change in Style - v2.0
C++11 - A Change in Style - v2.0C++11 - A Change in Style - v2.0
C++11 - A Change in Style - v2.0
 
Bdd: Tdd and beyond the infinite
Bdd: Tdd and beyond the infiniteBdd: Tdd and beyond the infinite
Bdd: Tdd and beyond the infinite
 
Summary of C++17 features
Summary of C++17 featuresSummary of C++17 features
Summary of C++17 features
 
C to perl binding
C to perl bindingC to perl binding
C to perl binding
 
Lecture 15_Strings and Dynamic Memory Allocation.pptx
Lecture 15_Strings and  Dynamic Memory Allocation.pptxLecture 15_Strings and  Dynamic Memory Allocation.pptx
Lecture 15_Strings and Dynamic Memory Allocation.pptx
 
SWP - A Generic Language Parser
SWP - A Generic Language ParserSWP - A Generic Language Parser
SWP - A Generic Language Parser
 
Array
ArrayArray
Array
 
Scala in Places API
Scala in Places APIScala in Places API
Scala in Places API
 
Real life-coffeescript
Real life-coffeescriptReal life-coffeescript
Real life-coffeescript
 
Spock Framework - Slidecast
Spock Framework - SlidecastSpock Framework - Slidecast
Spock Framework - Slidecast
 
Spock Framework
Spock FrameworkSpock Framework
Spock Framework
 
The hitchhicker’s guide to unit testing
The hitchhicker’s guide to unit testingThe hitchhicker’s guide to unit testing
The hitchhicker’s guide to unit testing
 
Making JavaScript Libraries More Approachable
Making JavaScript Libraries More ApproachableMaking JavaScript Libraries More Approachable
Making JavaScript Libraries More Approachable
 
Chapter 2
Chapter 2Chapter 2
Chapter 2
 
Scala 2 + 2 > 4
Scala 2 + 2 > 4Scala 2 + 2 > 4
Scala 2 + 2 > 4
 
Spock
SpockSpock
Spock
 
13 Strings and Text Processing
13 Strings and Text Processing13 Strings and Text Processing
13 Strings and Text Processing
 
Programming with Java: the Basics
Programming with Java: the BasicsProgramming with Java: the Basics
Programming with Java: the Basics
 

Recently uploaded

Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx
Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptxDecoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx
Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptxJoão Esperancinha
 
CCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdf
CCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdfCCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdf
CCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdfAsst.prof M.Gokilavani
 
CCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdf
CCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdfCCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdf
CCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdfAsst.prof M.Gokilavani
 
IVE Industry Focused Event - Defence Sector 2024
IVE Industry Focused Event - Defence Sector 2024IVE Industry Focused Event - Defence Sector 2024
IVE Industry Focused Event - Defence Sector 2024Mark Billinghurst
 
Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)
Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)
Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)dollysharma2066
 
main PPT.pptx of girls hostel security using rfid
main PPT.pptx of girls hostel security using rfidmain PPT.pptx of girls hostel security using rfid
main PPT.pptx of girls hostel security using rfidNikhilNagaraju
 
INFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETE
INFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETEINFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETE
INFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETEroselinkalist12
 
Past, Present and Future of Generative AI
Past, Present and Future of Generative AIPast, Present and Future of Generative AI
Past, Present and Future of Generative AIabhishek36461
 
EduAI - E learning Platform integrated with AI
EduAI - E learning Platform integrated with AIEduAI - E learning Platform integrated with AI
EduAI - E learning Platform integrated with AIkoyaldeepu123
 
Study on Air-Water & Water-Water Heat Exchange in a Finned Tube Exchanger
Study on Air-Water & Water-Water Heat Exchange in a Finned Tube ExchangerStudy on Air-Water & Water-Water Heat Exchange in a Finned Tube Exchanger
Study on Air-Water & Water-Water Heat Exchange in a Finned Tube ExchangerAnamika Sarkar
 
Concrete Mix Design - IS 10262-2019 - .pptx
Concrete Mix Design - IS 10262-2019 - .pptxConcrete Mix Design - IS 10262-2019 - .pptx
Concrete Mix Design - IS 10262-2019 - .pptxKartikeyaDwivedi3
 
VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...
VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...
VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...VICTOR MAESTRE RAMIREZ
 
Effects of rheological properties on mixing
Effects of rheological properties on mixingEffects of rheological properties on mixing
Effects of rheological properties on mixingviprabot1
 
Arduino_CSE ece ppt for working and principal of arduino.ppt
Arduino_CSE ece ppt for working and principal of arduino.pptArduino_CSE ece ppt for working and principal of arduino.ppt
Arduino_CSE ece ppt for working and principal of arduino.pptSAURABHKUMAR892774
 
CCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdf
CCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdfCCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdf
CCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdfAsst.prof M.Gokilavani
 
Heart Disease Prediction using machine learning.pptx
Heart Disease Prediction using machine learning.pptxHeart Disease Prediction using machine learning.pptx
Heart Disease Prediction using machine learning.pptxPoojaBan
 

Recently uploaded (20)

Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx
Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptxDecoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx
Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx
 
CCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdf
CCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdfCCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdf
CCS355 Neural Network & Deep Learning Unit II Notes with Question bank .pdf
 
CCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdf
CCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdfCCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdf
CCS355 Neural Networks & Deep Learning Unit 1 PDF notes with Question bank .pdf
 
IVE Industry Focused Event - Defence Sector 2024
IVE Industry Focused Event - Defence Sector 2024IVE Industry Focused Event - Defence Sector 2024
IVE Industry Focused Event - Defence Sector 2024
 
Design and analysis of solar grass cutter.pdf
Design and analysis of solar grass cutter.pdfDesign and analysis of solar grass cutter.pdf
Design and analysis of solar grass cutter.pdf
 
Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)
Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)
Call Us ≽ 8377877756 ≼ Call Girls In Shastri Nagar (Delhi)
 
main PPT.pptx of girls hostel security using rfid
main PPT.pptx of girls hostel security using rfidmain PPT.pptx of girls hostel security using rfid
main PPT.pptx of girls hostel security using rfid
 
INFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETE
INFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETEINFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETE
INFLUENCE OF NANOSILICA ON THE PROPERTIES OF CONCRETE
 
Call Us -/9953056974- Call Girls In Vikaspuri-/- Delhi NCR
Call Us -/9953056974- Call Girls In Vikaspuri-/- Delhi NCRCall Us -/9953056974- Call Girls In Vikaspuri-/- Delhi NCR
Call Us -/9953056974- Call Girls In Vikaspuri-/- Delhi NCR
 
Past, Present and Future of Generative AI
Past, Present and Future of Generative AIPast, Present and Future of Generative AI
Past, Present and Future of Generative AI
 
EduAI - E learning Platform integrated with AI
EduAI - E learning Platform integrated with AIEduAI - E learning Platform integrated with AI
EduAI - E learning Platform integrated with AI
 
Study on Air-Water & Water-Water Heat Exchange in a Finned Tube Exchanger
Study on Air-Water & Water-Water Heat Exchange in a Finned Tube ExchangerStudy on Air-Water & Water-Water Heat Exchange in a Finned Tube Exchanger
Study on Air-Water & Water-Water Heat Exchange in a Finned Tube Exchanger
 
Concrete Mix Design - IS 10262-2019 - .pptx
Concrete Mix Design - IS 10262-2019 - .pptxConcrete Mix Design - IS 10262-2019 - .pptx
Concrete Mix Design - IS 10262-2019 - .pptx
 
VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...
VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...
VICTOR MAESTRE RAMIREZ - Planetary Defender on NASA's Double Asteroid Redirec...
 
Effects of rheological properties on mixing
Effects of rheological properties on mixingEffects of rheological properties on mixing
Effects of rheological properties on mixing
 
Exploring_Network_Security_with_JA3_by_Rakesh Seal.pptx
Exploring_Network_Security_with_JA3_by_Rakesh Seal.pptxExploring_Network_Security_with_JA3_by_Rakesh Seal.pptx
Exploring_Network_Security_with_JA3_by_Rakesh Seal.pptx
 
9953056974 Call Girls In South Ex, Escorts (Delhi) NCR.pdf
9953056974 Call Girls In South Ex, Escorts (Delhi) NCR.pdf9953056974 Call Girls In South Ex, Escorts (Delhi) NCR.pdf
9953056974 Call Girls In South Ex, Escorts (Delhi) NCR.pdf
 
Arduino_CSE ece ppt for working and principal of arduino.ppt
Arduino_CSE ece ppt for working and principal of arduino.pptArduino_CSE ece ppt for working and principal of arduino.ppt
Arduino_CSE ece ppt for working and principal of arduino.ppt
 
CCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdf
CCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdfCCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdf
CCS355 Neural Network & Deep Learning UNIT III notes and Question bank .pdf
 
Heart Disease Prediction using machine learning.pptx
Heart Disease Prediction using machine learning.pptxHeart Disease Prediction using machine learning.pptx
Heart Disease Prediction using machine learning.pptx
 

Go testdeep

  • 2.
  • 3.
  • 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
  • 21. td.T — Main methods — 2/6 func (t *T) Cmp(got, expected interface{}, args ...interface{}) bool func (t *T) CmpError(got error, args ...interface{}) bool func (t *T) CmpLax(got, expected interface{}, args ...interface{}) bool func (t *T) CmpNoError(got error, args ...interface{}) bool func (t *T) CmpNotPanic(fn func(), args ...interface{}) bool func (t *T) CmpPanic(fn func(), expected interface{}, args ...interface{}) bool func (t *T) False(got interface{}, args ...interface{}) bool func (t *T) Not(got, notExpected interface{}, args ...interface{}) bool func (t *T) True(got interface{}, args ...interface{}) bool func (t *T) Run(name string, f func(t *T)) bool func (t *T) RunAssertRequire(name string, f func(assert, require *T)) bool In fact mostly the same as main functions, but: • CmpFalse() / CmpTrue() → (t *T) False() / (t *T) True() • CmpNot() → (t *T) Not() • new (t *T) Run() and (t *T) RunAssertRequire() See documentation for details 21
  • 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
  • 24. td.T — Simple example — 5/6 � � import ( "testing" "github.com/maxatome/go-testdeep/td" ) func TestExample(t *testing.T) { assert, require := td.AssertRequire(t) person, err := GetPerson("Bob") require.CmpNoError(err) // exits test if it fails assert.RootName("PERSON"). Cmp(person, &Person{ ID: assert.A(td.NotZero(), int64(0)).(int64), Name: "Bob", Age: assert.A(td.Between(40, 45)).(int), Children: assert.A(td.Len(2), ([]*Person)(nil)).([]*Person), }) } 24
  • 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