FAQ
How to mix strict requirements and simple assertions?
import (
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestAssertionsAndRequirements(t *testing.T) {
assert, require := td.AssertRequire(t)
got := SomeFunction()
require.Cmp(got, expected) // if it fails: report error + abort
assert.Cmp(got, expected) // if it fails: report error + continue
}
Why nil
is handled so specifically?
var pn *int
td.Cmp(t, pn, nil)
And, yes, it is normal. (TL;DR use
CmpNil
instead, safer, or use
CmpLax
,
but be careful of edge cases.)
To understand why, look at the following examples:
var err error
td.Cmp(t, err, nil)
works (and you want it works), but
var err error = (*MyError)(nil)
td.Cmp(t, err, nil)
and in most cases you want it fails, because err
is not nil! The
pointer stored in the interface is nil, but not the interface itself.
As Cmp
got
parameter type is any
, when you pass an
interface variable in it (whatever the interface is), Cmp
always
receives an any
. So here, Cmp
receives (*MyError)(nil)
in the got
interface, and not error((*MyError)(nil))
β the error
interface information is lost at the compilation time.
In other words, Cmp
has no abilities to tell the difference
between error((*MyError)(nil))
and (*MyError)(nil)
when passed in
got
parameter.
That is why Cmp
is strict by default, and requires that nil be
strongly typed, to be able to detect when a non-nil interface contains
a nil pointer.
So to recap:
var pn *int
td.Cmp(t, pn, nil) // fails as nil is not strongly typed
td.Cmp(t, pn, (*int)(nil)) // succeeds
td.Cmp(t, pn, td.Nil()) // succeeds
td.CmpNil(t, pn) // succeeds
td.Cmp(t, pn, td.Lax(nil)) // succeeds
td.CmpLax(t, pn, nil) // succeeds
var err error
td.Cmp(t, err, nil) // succeeds
td.Cmp(t, err, (*MyError)(nil)) // fails as err does not contain any value
td.Cmp(t, err, td.Nil()) // succeeds
td.CmpNil(t, err) // succeeds
td.Cmp(t, err, td.Lax(nil)) // succeeds
td.CmpLax(t, err, nil) // succeeds
td.CmpError(t, err) // fails as err is nil
td.CmpNoError(t, err) // succeeds
err = (*MyError)(nil)
td.Cmp(t, err, nil) // fails as err contains a value
td.Cmp(t, err, (*MyError)(nil)) // succeeds
td.Cmp(t, err, td.Nil()) // succeeds
td.CmpNil(t, err) // succeeds
td.Cmp(t, err, td.Lax(nil)) // succeeds *** /!\ be careful here! ***
td.CmpLax(t, err, nil) // succeeds *** /!\ be careful here! ***
td.CmpError(t, err) // succeeds
td.CmpNoError(t, err) // fails as err contains a value
Morality:
- to compare a pointer against nil, use
CmpNil
or strongly type nil (e.g.(*int)(nil)
) in expected parameter ofCmp
; - to compare an error against nil, use
CmpNoError
or nil direcly in expected parameter ofCmp
.
How does operator anchoring work?
Take this struct, returned by a GetPerson()
function:
type Person struct {
ID int64
Name string
Age uint8
}
For the Person
returned by GetPerson()
, we expect that:
ID
field should be β 0;Name
field should always be βBobβ;Age
field should be β₯ 40 and β€ 45.
Without operator anchoring:
func TestPerson(t *testing.T) {
assert := td.Assert(t)
assert.Cmp(GetPerson(), // β β
td.Struct(Person{Name: "Bob"}, // β β‘
td.StructFields{ // β β’
"ID": td.NotZero(), // β β£
"Age": td.Between(uint8(40), uint8(45)), // β β€
}))
}
GetPerson()
returns aPerson
;- as some fields of the returned
Person
are not exactly known in advance, we use theStruct
operator as expected parameter. It allows to match exactly some fields, and use TestDeep operators on others. Here we know thatName
field should always be βBobβ; StructFields
is a map allowing to use TestDeep operators for any field;ID
field should be β 0. SeeNotZero
operator for details;Age
field should be β₯ 40 and β€ 45. SeeBetween
operator for details.
With operator anchoring, the use of Struct
operator is no longer needed:
func TestPerson(t *testing.T) {
assert := td.Assert(t)
assert.Cmp(GetPerson(), // β β
Person{ // β β‘
Name: "Bob", // β β’
ID: assert.A(td.NotZero(), int64(0)).(int64), // β β£
Age: assert.A(td.Between(uint8(40), uint8(45))).(uint8), // β β€
})
// Or using generics from go1.18
assert.Cmp(GetPerson(), // β β
Person{ // β β‘
Name: "Bob", // β β’
ID: td.A[int64](assert, td.NotZero()), // β β£
Age: td.A[uint8](assert, td.Between(uint8(40), uint8(45))), // β β€
})
}
GetPerson()
still returns aPerson
;- expected parameter is directly a
Person
. No operator needed here; Name
field should always be βBobβ, no change here;ID
field should be β 0: anchor theNotZero
operator:- using the
A
method. Break this line down:assert.A( // β β td.NotZero(), // β β‘ int64(0), // β β’ ).(int64) // β β£
- the
A
method is the key of the anchoring system. It saves the operator inassert
instance, so it can be retrieved during the comparison of the nextCmp
call onassert
, - the operator we want to anchor,
- this optional parameter is needed to tell
A
that the returned value must be aint64
. Sometimes, this type can be deduced from the operator, but asNotZero
can handle any kind of number, it is not the case here. So we have to pass it, - as
A
method returns anany
, we need to assert theint64
type to bypass the golang static typing system,
- the
- using the
A
generic function. Break this line down:td.A[ // β β int64, // β β‘ ]( assert, // β β’ td.NotZero(), // β β£ )
- using the
Age
field should be β₯ 40 and β€ 45: anchor theBetween
operator:- using the
A
method. Break this line down:assert.A( // β β td.Between(uint8(40), uint8(45)), // β β‘ ).(uint8) // β β’
- the
A
method saves the operator inassert
, so it can be retrieved during the comparison of the nextCmp
call onassert
, - the operator we want to anchor. As
Between
knows the type of its operands (hereuint8
), there is no need to tellA
the returned type must beuint8
. It can be deduced fromBetween
, - as
A
method returns anany
, we need to assert theuint8
type to bypass the golang static typing system.
- the
- using the
A
generic function. Break this line down:td.A[ // β β uint8, // β β‘ ]( assert, // β β’ td.Between(uint8(40), uint8(45)), // β β£ )
- using the
Note the A
method is a shortcut of Anchor
method, as well as
A
function is a shortcut of
Anchor
function.
Some rules have to be kept in mind:
- never cast a value returned by
A
orAnchor
methods:assert := td.Assert(t) // t is a *testing.T assert.A(td.NotZero(), uint(8)).(uint8) // OK uint16(assert.A(td.NotZero(), uint(8)).(uint8)) // Not OK! assert.A(td.NotZero(), uint16(0)).(uint16) // OK
- anchored operators disappear once the next
Cmp
call done. To share them betweenCmp
calls, use theSetAnchorsPersist
method as in:Try it in playground πassert := td.Assert(t) // t is a *testing.T age := assert.A(td.Between(uint8(40), uint8(45))).(uint8) assert.SetAnchorsPersist(true) // β Don't reset anchors after next Cmp() call assert.Cmp(GetPerson(1), Person{ Name: "Bob", Age: age, }) assert.Cmp(GetPerson(2), Person{ Name: "Bob", Age: age, // β OK })
How to test io.Reader
contents, like net/http.Response.Body
for example?
The Smuggle
operator is done for that,
here with the help of ReadAll
.
import (
"io"
"net/http"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestResponseBody(t *testing.T) {
// Expect this response sends "Expected Response!"
var resp *http.Response = GetResponse()
td.Cmp(t, resp.Body,
td.Smuggle(io.ReadAll, []byte("Expected Response!")))
}
OK, but I prefer comparing string
s instead of byte
s
No problem, ReadAll
the body (still
using Smuggle
operator), then ask
go-testdeep to compare it against a string using
String
operator:
import (
"io"
"net/http"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestResponseBody(t *testing.T) {
// Expect this response sends "Expected Response!"
var resp *http.Response = GetResponse()
td.Cmp(t, resp.Body,
td.Smuggle(io.ReadAll, td.String("Expected Response!")))
}
OK, but my response is in fact a JSON marshaled struct of my own
No problem, JSON decode while reading the body:
import (
"encoding/json"
"io"
"net/http"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestResponseBody(t *testing.T) {
// Expect this response sends `{"ID":42,"Name":"Bob","Age":28}`
var resp *http.Response = GetResponse()
type Person struct {
ID uint64
Name string
Age int
}
td.Cmp(t, resp.Body, td.Smuggle( // β transform a io.Reader in *Person
func(body io.Reader) (*Person, error) {
var p Person
return &p, json.NewDecoder(body).Decode(&p)
},
&Person{ // β check Person content
ID: 42,
Name: "Bob",
Age: 28,
}))
}
So I always need to manually unmarshal in a struct?
It is up to you! Using JSON
operator for
example, you can test any JSON content. The first step is to read all
the body (which is an io.Reader
) into
a json.RawMessage
thanks to the Smuggle
operator special cast
feature, then ask JSON
operator to do the
comparison:
import (
"encoding/json"
"net/http"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestResponseBody(t *testing.T) {
// Expect this response sends `{"ID":42,"Name":"Bob","Age":28}`
var resp *http.Response = GetResponse()
td.Cmp(t, resp.Body, td.Smuggle(json.RawMessage{}, td.JSON(`
{
"ID": 42,
"Name": "Bob",
"Age": 28
}`)))
}
OK, but you are funny, this response sends a new created object, so I donβt know the ID in advance!
No problem, use Struct
operator to test
that ID
field is non-zero (as a bonus, add a CreatedAt
field):
import (
"encoding/json"
"io"
"net/http"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestResponseBody(t *testing.T) {
// Expect this response sends:
// `{"ID":42,"Name":"Bob","Age":28,"CreatedAt":"2019-01-02T11:22:33Z"}`
var resp *http.Response = GetResponse()
type Person struct {
ID uint64
Name string
Age int
CreatedAt time.Time
}
y2019, _ := time.Parse(time.RFC3339, "2019-01-01T00:00:00Z")
// Using Struct operator
td.Cmp(t, resp.Body, td.Smuggle( // β transform a io.Reader in *Person
func(body io.Reader) (*Person, error) {
var s Person
return &s, json.NewDecoder(body).Decode(&s)
},
td.Struct(&Person{ // β check Person content
Name: "Bob",
Age: 28,
}, td.StructFields{
"ID": td.NotZero(), // check ID β 0
"CreatedAt": td.Gte(y2019), // check CreatedAt β₯ 2019/01/01
})))
// Using anchoring feature
resp = GetResponse()
assert := td.Assert(t)
assert.Cmp(resp.Body, td.Smuggle( // β transform a io.Reader in *Person
func(body io.Reader) (*Person, error) {
var s Person
return &s, json.NewDecoder(body).Decode(&s)
},
&Person{ // β check Person content
Name: "Bob",
Age: 28,
ID: assert.A(td.NotZero(), uint64(0)).(uint64), // check ID β 0
CreatedAt: assert.A(td.Gte(y2019)).(time.Time), // check CreatedAt β₯ 2019/01/01
}))
// Using anchoring feature with generics
resp = GetResponse()
assert.Cmp(resp.Body, td.Smuggle( // β transform a io.Reader in *Person
func(body io.Reader) (*Person, error) {
var s Person
return &s, json.NewDecoder(body).Decode(&s)
},
&Person{ // β check Person content
Name: "Bob",
Age: 28,
ID: td.A[uint64](assert, td.NotZero()), // check ID β 0
CreatedAt: td.A[time.Time](assert, td.Gte(y2019)), // check CreatedAt β₯ 2019/01/01
}))
// Using JSON operator
resp = GetResponse()
td.Cmp(t, resp.Body, td.Smuggle(json.RawMessage{}, td.JSON(`
{
"ID": NotZero,
"Name": "Bob",
"Age": 28,
"CreatedAt": Gte($1)
}`, y2019)))
}
What about testing the response using my API?
tdhttp
helper
is done for that!
import (
"encoding/json"
"net/http"
"testing"
"time"
"github.com/maxatome/go-testdeep/helpers/tdhttp"
"github.com/maxatome/go-testdeep/td"
)
type Person struct {
ID uint64
Name string
Age int
CreatedAt time.Time
}
// MyApi defines our API.
func MyAPI() *http.ServeMux {
mux := http.NewServeMux()
// GET /json
mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
http.NotFound(w, req)
return
}
b, err := json.Marshal(Person{
ID: 42,
Name: "Bob",
Age: 28,
CreatedAt: time.Now().UTC(),
})
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(b)
})
return mux
}
func TestMyApi(t *testing.T) {
myAPI := MyAPI()
y2008, _ := time.Parse(time.RFC3339, "2008-01-01T00:00:00Z")
ta := tdhttp.NewTestAPI(t, myAPI) // β β
ta.Get("/json"). // β β‘
Name("Testing GET /json").
CmpStatus(http.StatusOK). // β β’
CmpJSONBody(td.SStruct(&Person{ // β β£
Name: "Bob",
Age: 28,
}, td.StructFields{
"ID": td.NotZero(), // β β€
"CreatedAt": td.Gte(y2008), // β β₯
}))
// ta can be used to test another routeβ¦
}
- the API handler ready to be tested;
- the GET request;
- the expected HTTP status should be
http.StatusOK
; - the expected body should match the
SStruct
operator; - check the
ID
field isNotZero
; - check the
CreatedAt
field is greater or equal thany2008
variable (set just beforetdhttp.NewTestAPI
call).
If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
Arf, I use Gin Gonic, and so no net/http
handlers
It is exactly the same as for net/http
handlers as *gin.Engine
implements http.Handler
interface!
So keep using
tdhttp
helper:
import (
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/maxatome/go-testdeep/helpers/tdhttp"
"github.com/maxatome/go-testdeep/td"
)
type Person struct {
ID uint64
Name string
Age int
CreatedAt time.Time
}
// MyGinGonicApi defines our API.
func MyGinGonicAPI() *gin.Engine {
router := gin.Default() // or gin.New() or receive the router by param it doesn't matter
router.GET("/json", func(c *gin.Context) {
c.JSON(http.StatusOK, Person{
ID: 42,
Name: "Bob",
Age: 28,
CreatedAt: time.Now().UTC(),
})
})
return router
}
func TestMyGinGonicApi(t *testing.T) {
myAPI := MyGinGonicAPI()
y2008, _ := time.Parse(time.RFC3339, "2008-01-01T00:00:00Z")
ta := tdhttp.NewTestAPI(t, myAPI) // β β
ta.Get("/json"). // β β‘
Name("Testing GET /json").
CmpStatus(http.StatusOK). // β β’
CmpJSONBody(td.SStruct(&Person{ // β β£
Name: "Bob",
Age: 28,
}, td.StructFields{
"ID": td.NotZero(), // β β€
"CreatedAt": td.Gte(y2008), // β β₯
}))
// ta can be used to test another routeβ¦
}
- the API handler ready to be tested;
- the GET request;
- the expected HTTP status should be
http.StatusOK
; - the expected body should match the
SStruct
operator; - check the
ID
field isNotZero
; - check the
CreatedAt
field is greater or equal thany2008
variable (set just beforetdhttp.NewTestAPI
call).
If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
Fine, the request succeeds and the ID is not 0, but what is the ID real value?
Stay with tdhttp
helper!
In fact you can Catch
the ID
before comparing
it to 0 (as well as CreatedAt
in fact). Try:
func TestMyGinGonicApi(t *testing.T) {
myAPI := MyGinGonicAPI()
var id uint64
var createdAt time.Time
y2008, _ := time.Parse(time.RFC3339, "2008-01-01T00:00:00Z")
ta := tdhttp.NewTestAPI(t, myAPI) // β β
ta.Get("/json"). // β β‘
Name("Testing GET /json").
CmpStatus(http.StatusOK). // β β’
CmpJSONBody(td.SStruct(&Person{ // β β£
Name: "Bob",
Age: 28,
}, td.StructFields{
"ID": td.Catch(&id, td.NotZero()), // β β€
"CreatedAt": td.Catch(&createdAt, td.Gte(y2008)), // β β₯
}))
if !ta.Failed() {
t.Logf("The ID is %d and was created at %s", id, createdAt)
}
// ta can be used to test another routeβ¦
}
- the API handler ready to be tested;
- the GET request;
- the expected HTTP status should be
http.StatusOK
; - the expected body should match the
SStruct
operator; Catch
theID
field: put it inid
variable and check it isNotZero
;Catch
theCreatedAt
field: put it increatedAt
variable and check it is greater or equal thany2008
variable (set just beforetdhttp.NewTestAPI
call).
If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
And what about other HTTP frameworks?
tdhttp.NewTestAPI()
function needs a http.Handler
instance.
Letβs see for each following framework how to get it:
Beego
In single instance mode,
web.BeeApp
variable is a
*web.HttpServer
instance containing a Handlers
field whose
*ControllerRegister
type implements
http.Handler
:
ta := tdhttp.NewTestAPI(t, web.BeeApp.Handlers)
In multi instances mode, each instance is a
*web.HttpServer
so the same rule applies for each instance to test:
ta := tdhttp.NewTestAPI(t, instance.Handlers)
echo
Starting v3.0.0,
echo.New()
returns a *echo.Echo
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI
:
e := echo.New()
// Add routes to e
β¦
ta := tdhttp.NewTestAPI(t, e) // e implements http.Handler
Gin
gin.Default()
and gin.New()
return both a
*gin.Engine
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI
:
engine := gin.Default()
// Add routes to engine
β¦
ta := tdhttp.NewTestAPI(t, engine) // engine implements http.Handler
gorilla/mux
mux.NewRouter()
returns a *mux.Router
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI
:
r := mux.NewRouter()
// Add routes to r
β¦
ta := tdhttp.NewTestAPI(t, r) // r implements http.Handler
go-swagger
2 cases here, the default generation and the Stratoscale template:
- default generation requires some tricks to retrieve the
http.Handler
instance:swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON) td.Require(t).CmpNoError(err, "Swagger spec is loaded") api := operations.NewXxxAPI(swaggerSpec) // Xxx is the name of your API server := restapi.NewServer(api) server.ConfigureAPI() hdl := server.GetHandler() // returns an http.Handler instance ta := tdhttp.NewTestAPI(t, hdl)
- with Stratoscale template, it is simpler:
hdl, _, err := restapi.HandlerAPI(restapi.Config{ Tag1API: Tag1{}, Tag2API: Tag2{}, }) td.Require(t).CmpNoError(err, "API correctly set up") // hdl is an http.Handler instance ta := tdhttp.NewTestAPI(t, hdl)
HttpRouter
httprouter.New()
returns a
*httprouter.Router
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI
:
r := httprouter.New()
// Add routes to r
β¦
ta := tdhttp.NewTestAPI(t, r) // r implements http.Handler
pat
pat.New()
returns a *pat.Router
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI
:
router := pat.New()
// Add routes to router
β¦
ta := tdhttp.NewTestAPI(t, router) // router implements http.Handler
Another web framework not listed here?
File an issue or open a PR to fix this!
OK, but how to be sure the response content is well JSONified?
Again, tdhttp
helper
is your friend!
With the help of JSON
operator of course! See
it below, used with Catch
(note it can be used
without), for a POST
example:
type Person struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
}
func TestMyGinGonicApi(t *testing.T) {
myAPI := MyGinGonicAPI()
var id uint64
var createdAt time.Time
ta := tdhttp.NewTestAPI(t, myAPI) // β β
ta.PostJSON("/person", Person{Name: "Bob", Age: 42}), // β β‘
Name("Create a new Person").
CmpStatus(http.StatusCreated). // β β’
CmpJSONBody(td.JSON(`
{
"id": $id,
"name": "Bob",
"age": 42,
"created_at": "$createdAt",
}`,
td.Tag("id", td.Catch(&id, td.NotZero())), // β β£
td.Tag("createdAt", td.All( // β β€
td.HasSuffix("Z"), // β β₯
td.Smuggle(func(s string) (time.Time, error) { // β β¦
return time.Parse(time.RFC3339Nano, s)
}, td.Catch(&createdAt, td.Gte(ta.SentAt()))), // β β§
)),
))
if !ta.Failed() {
t.Logf("The new Person ID is %d and was created at %s", id, createdAt)
}
// ta can be used to test another routeβ¦
}
- the API handler ready to be tested;
- the POST request with automatic JSON marshalling;
- the expected HTTP status should be
http.StatusCreated
and the line just below, the body should match theJSON
operator; - for the
$id
placeholder,Catch
its value: put it inid
variable and check it isNotZero
; - for the
$createdAt
placeholder, use theAll
operator. It combines several operators like a AND; - check that
$createdAt
date ends with βZβ usingHasSuffix
. As we expect a RFC3339 date, we require it in UTC time zone; - convert
$createdAt
date into atime.Time
using a custom function thanks to theSmuggle
operator; - then
Catch
the resulting value: put it increatedAt
variable and check it is greater or equal thanta.SentAt()
(the time just before the request is handled).
If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
My API uses XML not JSON!
tdhttp
helper
provides the same functions and methods for XML it does for JSON.
RTFM :)
Note that the JSON
operator have not its XML
counterpart yet.
But PRs are welcome!
How to assert for an UUIDv7?
Combining Smuggle
and Code
,
you can easily write a custom operator:
// Uses uuid from github.com/gofrs/uuid/v5
func isUUIDv7() td.TestDeep {
return td.Smuggle(uuid.FromString, td.Code(func(u uuid.UUID) error {
if u.Version() != uuid.V7 {
return fmt.Errorf("UUID v%x instead of v7", u.Version())
}
return nil
}))
}
that you can then use, for example in a JSON
match:
td.Cmp(t, jsonData, td.JSON(`{"id": $1}`, isUUIDv7()))
Should I import github.com/maxatome/go-testdeep
or github.com/maxatome/go-testdeep/td
?
Historically the main package of go-testdeep was testdeep
as in:
import (
"testing"
"github.com/maxatome/go-testdeep"
)
func TestMyFunc(t *testing.T) {
testdeep.Cmp(t, GetPerson(), Person{Name: "Bob", Age: 42})
}
As testdeep
was boring to type, renaming it to td
became a habit as in:
import (
"testing"
td "github.com/maxatome/go-testdeep"
)
func TestMyFunc(t *testing.T) {
td.Cmp(t, GetPerson(), Person{Name: "Bob", Age: 42})
}
Forcing the developer to systematically rename testdeep
package to
td
in all its tests is not very friendly. That is why a decision was
taken to create a new package github.com/maxatome/go-testdeep/td
while keeping github.com/maxatome/go-testdeep
working thanks to go
type aliases.
So the previous examples (that are still working) can now be written as:
import (
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestMyFunc(t *testing.T) {
td.Cmp(t, GetPerson(), Person{Name: "Bob", Age: 42})
}
There is no package renaming anymore. Switching to import
github.com/maxatome/go-testdeep/td
is advised for new code.
What does the error undefined: testdeep.DefaultContextConfig
mean?
Since release v1.3.0
, this variable moved to the new
github.com/maxatome/go-testdeep/td
package.
-
If you rename the
testdeep
package totd
as in:import td "github.com/maxatome/go-testdeep" β¦ td.DefaultContextConfig = td.ContextConfig{β¦}
then just change the import line to:
import "github.com/maxatome/go-testdeep/td"
-
Otherwise, you have two choices:
- either add a new import line:
then use
import "github.com/maxatome/go-testdeep/td"
td.DefaultContextConfig
instead oftestdeep.DefaultContextConfig
, and continue to usetestdeep
package elsewhere. - or replace the import line:
by
import "github.com/maxatome/go-testdeep"
then rename all occurrences ofimport "github.com/maxatome/go-testdeep/td"
testdeep
package totd
.
- either add a new import line:
go-testdeep dumps only 10 errors, how to have more (or less)?
Using the environment variable TESTDEEP_MAX_ERRORS
.
TESTDEEP_MAX_ERRORS
contains the maximum number of errors to report
before stopping during one comparison (one Cmp
execution for
example). It defaults to 10
.
Example:
TESTDEEP_MAX_ERRORS=30 go test
Setting it to -1
means no limit:
TESTDEEP_MAX_ERRORS=-1 go test
How do I change these crappy colors?
Using some environment variables:
TESTDEEP_COLOR
enable (on
) or disable (off
) the color output. It defaults toon
;TESTDEEP_COLOR_TEST_NAME
color of the test name. See below for color format, it defaults toyellow
;TESTDEEP_COLOR_TITLE
color of the test failure title. See below for color format, it defaults tocyan
;TESTDEEP_COLOR_OK
color of the test expected value. See below for color format, it defaults togreen
;TESTDEEP_COLOR_BAD
color of the test got value. See below for color format, it defaults tored
;
Color format
A color in TESTDEEP_COLOR_*
environment variables has the following
format:
foreground_color # set foreground color, background one untouched
foreground_color:background_color # set foreground AND background color
:background_color # set background color, foreground one untouched
foreground_color
and background_color
can be:
black
red
green
yellow
blue
magenta
cyan
white
gray
For example:
TESTDEEP_COLOR_OK=black:green \
TESTDEEP_COLOR_BAD=white:red \
TESTDEEP_COLOR_TITLE=yellow \
go test
play.golang.org does not handle colors, error output is nasty
Just add this single line in playground:
import _ "github.com/maxatome/go-testdeep/helpers/nocolor"
(since go-testdeep v1.10.0) or:
func init() { os.Setenv("TESTDEEP_COLOR", "off") }
until playground supports ANSI color escape sequences.
The X
testing framework allows to test/do Y
while go-testdeep not
The Code
and Smuggle
operators should allow to cover all cases not handled by other
operators.
If you think this missing feature deserves a specific operator, because it is frequently or widely used, file an issue and letβs discuss about it.
We plan to add a new github.com/maxatome/go-testdeep/helpers/tdcombo
helper package, bringing together all what we can call
combo-operators. Combo-operators are operators using any number of
already existing operators.
As an example of such combo-operators, the following one. It allows to
check that a string contains a RFC3339 formatted time, in UTC time
zone (βZβ suffix) and then to compare it as a time.Time
against
expectedValue
(which can be another operator
or, of course, a time.Time
value).
func RFC3339ZToTime(expectedValue any) td.TestDeep {
return td.All(
td.HasSuffix("Z"),
td.Smuggle(func(s string) (time.Time, error) {
return time.Parse(time.RFC3339Nano, s)
}, expectedValue),
)
}
It could be used as:
before := time.Now()
record := NewRecord()
td.Cmp(t, record,
td.SuperJSONOf(`{"created_at": $1}`,
tdcombo.RFC3339ZToTime(td.Between(before, time.Now()),
)),
"The JSONified record.created_at is UTC-RFC3339",
)
How to add a new operator?
You want to add a new FooBar
operator.
- check that another operator does not exist with the same meaning;
- add the operator definition in
td_foo_bar.go
file and fully document its usage:- add a
// summary(FooBar): small description
line, before operator comment, - add a
// input(FooBar): β¦
line, just aftersummary(FooBar)
line. This one lists all inputs accepted by the operator;
- add a
- add operator tests in
td_foo_bar_test.go
file; - in
example_test.go
file, add examples function(s)ExampleFooBar*
in alphabetical order; - should this operator be available in
JSON
,SubJSONOf
andSuperJSONOf
operators?- If no, add
FooBar
to theforbiddenOpsInJSON
map intd/td_json.go
with a possible alternative text to help the user, - If yes, does
FooBar
needs specific handling asN
orBetween
does for example?
- If no, add
- automatically generate
CmpFooBar
&T.FooBar
(+ examples) code:./tools/gen_funcs.pl
- do not forget to run tests:
go test ./...
- run
golangci-lint
as in.github/workflows/ci.yml
;
Each time you change example_test.go
, re-run ./tools/gen_funcs.pl
to update corresponding CmpFooBar
& T.FooBar
examples.
Test coverage must be 100%.