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)fails with the error
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)fails with the error
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 valueMorality:
- to compare a pointer against nil, use
CmpNilor strongly type nil (e.g.(*int)(nil)) in expected parameter ofCmp; - to compare an error against nil, use
CmpNoErroror 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:
IDfield should be β 0;Namefield should always be “Bob”;Agefield 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
Personare not exactly known in advance, we use theStructoperator as expected parameter. It allows to match exactly some fields, and use TestDeep operators on others. Here we know thatNamefield should always be “Bob”; StructFieldsis a map allowing to use TestDeep operators for any field;IDfield should be β 0. SeeNotZerooperator for details;Agefield should be β₯ 40 and β€ 45. SeeBetweenoperator 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; Namefield should always be “Bob”, no change here;IDfield should be β 0: anchor theNotZerooperator:- using the
Amethod. Break this line down:assert.A( // β β td.NotZero(), // β β‘ int64(0), // β β’ ).(int64) // β β£- the
Amethod is the key of the anchoring system. It saves the operator inassertinstance, so it can be retrieved during the comparison of the nextCmpcall onassert, - the operator we want to anchor,
- this optional parameter is needed to tell
Athat the returned value must be aint64. Sometimes, this type can be deduced from the operator, but asNotZerocan handle any kind of number, it is not the case here. So we have to pass it, - as
Amethod returns anany, we need to assert theint64type to bypass the golang static typing system,
- the
- using the
Ageneric function. Break this line down:td.A[ // β β int64, // β β‘ ]( assert, // β β’ td.NotZero(), // β β£ )
- using the
Agefield should be β₯ 40 and β€ 45: anchor theBetweenoperator:- using the
Amethod. Break this line down:assert.A( // β β td.Between(uint8(40), uint8(45)), // β β‘ ).(uint8) // β β’- the
Amethod saves the operator inassert, so it can be retrieved during the comparison of the nextCmpcall onassert, - the operator we want to anchor. As
Betweenknows the type of its operands (hereuint8), there is no need to tellAthe returned type must beuint8. It can be deduced fromBetween, - as
Amethod returns anany, we need to assert theuint8type to bypass the golang static typing system.
- the
- using the
Ageneric 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
AorAnchormethods: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
Cmpcall done. To share them betweenCmpcalls, use theSetAnchorsPersistmethod 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 strings instead of bytes
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
SStructoperator; - check the
IDfield isNotZero; - check the
CreatedAtfield is greater or equal thany2008variable (set just beforetdhttp.NewTestAPIcall).
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
SStructoperator; - check the
IDfield isNotZero; - check the
CreatedAtfield is greater or equal thany2008variable (set just beforetdhttp.NewTestAPIcall).
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
SStructoperator; CatchtheIDfield: put it inidvariable and check it isNotZero;CatchtheCreatedAtfield: put it increatedAtvariable and check it is greater or equal thany2008variable (set just beforetdhttp.NewTestAPIcall).
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.HandlerGin
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.Handlergorilla/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.Handlergo-swagger
2 cases here, the default generation and the Stratoscale template:
- default generation requires some tricks to retrieve the
http.Handlerinstance: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.Handlerpat
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.HandlerAnother 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.StatusCreatedand the line just below, the body should match theJSONoperator; - for the
$idplaceholder,Catchits value: put it inidvariable and check it isNotZero; - for the
$createdAtplaceholder, use theAlloperator. It combines several operators like a AND; - check that
$createdAtdate ends with “Z” usingHasSuffix. As we expect a RFC3339 date, we require it in UTC time zone; - convert
$createdAtdate into atime.Timeusing a custom function thanks to theSmuggleoperator; - then
Catchthe resulting value: put it increatedAtvariable 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
testdeeppackage totdas 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.DefaultContextConfiginstead oftestdeep.DefaultContextConfig, and continue to usetestdeeppackage elsewhere. - or replace the import line:
by
import "github.com/maxatome/go-testdeep"then rename all occurrences ofimport "github.com/maxatome/go-testdeep/td"testdeeppackage 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 testSetting it to -1 means no limit:
TESTDEEP_MAX_ERRORS=-1 go testHow do I change these crappy colors?
Using some environment variables:
TESTDEEP_COLORenable (on) or disable (off) the color output. It defaults toon;TESTDEEP_COLOR_TEST_NAMEcolor of the test name. See below for color format, it defaults toyellow;TESTDEEP_COLOR_TITLEcolor of the test failure title. See below for color format, it defaults tocyan;TESTDEEP_COLOR_OKcolor of the test expected value. See below for color format, it defaults togreen;TESTDEEP_COLOR_BADcolor 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 untouchedforeground_color and background_color can be:
blackredgreenyellowbluemagentacyanwhitegray
For example:
TESTDEEP_COLOR_OK=black:green \
TESTDEEP_COLOR_BAD=white:red \
TESTDEEP_COLOR_TITLE=yellow \
go testplay.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.gofile and fully document its usage:- add a
// summary(FooBar): small descriptionline, 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.gofile; - in
example_test.gofile, add examples function(s)ExampleFooBar*in alphabetical order; - should this operator be available in
JSON,SubJSONOfandSuperJSONOfoperators?- If no, add
FooBarto theforbiddenOpsInJSONmap intd/td_json.gowith a possible alternative text to help the user, - If yes, does
FooBarneeds specific handling asNorBetweendoes 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-lintas 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%.