FAQ
How to mix strict requirements and simple assertions?
Why nil is handled so specifically?
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:
works (and you want it works), but
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:
Morality:
- 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:
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:
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:
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:Agefield should be β₯ 40 and β€ 45: anchor theBetweenoperator:
Note the AT method is a shortcut of AnchorT 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
ATorAnchorTmethods: - anchored operators disappear once the next
Cmpcall done. To share them betweenCmpcalls, use theSetAnchorsPersistmethod as in: Try it in playground π - some types cannot be anchored:
bool,struct(unlessAddAnchorableStructTypeis explicitely used), functions, unsafe pointers and arrays. Note that pointers onbool,structor arrays are anchorable.
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.
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:
OK, but my response is in fact a JSON marshaled struct of my own
No problem, JSON decode while reading the body:
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:
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):
What about testing the response using my API?
tdhttp helper
is done for that!
- 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:
- 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:
- 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:
In multi instances mode, each instance is a
*web.HttpServer
so the same rule applies for each instance to test:
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:
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:
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:
go-swagger
2 cases here, the default generation and the Stratoscale template:
- default generation requires some tricks to retrieve the
http.Handlerinstance: - with Stratoscale template, it is simpler:
HttpRouter
httprouter.New()
returns a
*httprouter.Router
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI:
pat
pat.New()
returns a *pat.Router
instance that implements
http.Handler
interface, so this instance can be fed as is to
tdhttp.NewTestAPI:
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:
- 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:
that you can then use, for example in a JSON match:
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:
As testdeep was boring to type, renaming it to td became a habit as in:
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:
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:then just change the import line to:
-
Otherwise, you have two choices:
- either add a new import line:
then use
td.DefaultContextConfiginstead oftestdeep.DefaultContextConfig, and continue to usetestdeeppackage elsewhere. - or replace the import line:
by
then rename all occurrences of
testdeeppackage totd.
- either add a new import line:
then use
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:
Setting it to -1 means no limit:
How 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 and background_color can be:
blackredgreenyellowbluemagentacyanwhitegray
For example:
play.golang.org does not handle colors, error output is nasty
Just add this single line in playground:
(since go-testdeep v1.10.0) or:
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).
It could be used as:
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%.