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
}
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 value
Morality:
CmpNil
or strongly type nil (e.g. (*int)(nil)
) in expected parameter of Cmp
;CmpNoError
or nil direcly in expected parameter of Cmp
.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 a Person
;Person
are not exactly known in
advance, we use the Struct
operator as
expected parameter. It allows to match exactly some fields, and use
TestDeep operators on others. Here we
know that Name
field should always be “Bob”;StructFields
is a map allowing to use TestDeep operators
for any field;ID
field should be ≠ 0. See NotZero
operator for details;Age
field should be ≥ 40 and ≤ 45. See Between
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 a Person
;Person
. No operator needed here;Name
field should always be “Bob”, no change here;ID
field should be ≠ 0: anchor the NotZero
operator:
A
method. Break this line down:
assert.A( // ← ①
td.NotZero(), // ← ②
int64(0), // ← ③
).(int64) // ← ④
A
method is the key of the anchoring system. It saves
the operator in assert
instance, so it can be
retrieved during the comparison of the next Cmp
call on assert
,A
that the returned
value must be a int64
. Sometimes, this type can be deduced
from the operator, but as NotZero
can
handle any kind of number, it is not the case here. So we have
to pass it,A
method returns an any
, we need to assert the
int64
type to bypass the golang static typing system,A
generic function. Break this line down:
td.A[ // ← ①
int64, // ← ②
](
assert, // ← ③
td.NotZero(), // ← ④
)
Age
field should be ≥ 40 and ≤ 45: anchor the
Between
operator:
A
method. Break this line down:
assert.A( // ← ①
td.Between(uint8(40), uint8(45)), // ← ②
).(uint8) // ← ③
A
method saves the operator in assert
, so it can be
retrieved during the comparison of the next Cmp
call on assert
,Between
knows the type of its operands (here uint8
), there is no need
to tell A
the returned type must be uint8
. It can be deduced
from Between
,A
method returns an any
, we need to assert the
uint8
type to bypass the golang static typing system.A
generic function. Break this line down:
td.A[ // ← ①
uint8, // ← ②
](
assert, // ← ③
td.Between(uint8(40), uint8(45)), // ← ④
)
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:
A
or Anchor
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
Cmp
call done. To
share them between Cmp
calls, use the SetAnchorsPersist
method as in:
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
})
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!")))
}
string
s instead of byte
sNo 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!")))
}
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,
}))
}
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
}`)))
}
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)))
}
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…
}
http.StatusOK
;SStruct
operator;ID
field is NotZero
;CreatedAt
field is greater or equal than y2008
variable
(set just before tdhttp.NewTestAPI
call).If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
net/http
handlersIt 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…
}
http.StatusOK
;SStruct
operator;ID
field is NotZero
;CreatedAt
field is greater or equal than y2008
variable
(set just before tdhttp.NewTestAPI
call).If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
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…
}
http.StatusOK
;SStruct
operator;Catch
the ID
field: put it in id
variable and check it is NotZero
;Catch
the CreatedAt
field: put it in createdAt
variable and check it is greater or equal than y2008
variable
(set just before tdhttp.NewTestAPI
call).If you prefer to do one function call instead of chaining methods as above, you can try CmpJSONResponse.
tdhttp.NewTestAPI()
function needs a http.Handler
instance.
Let’s see for each following framework how to get it:
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)
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.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
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
2 cases here, the default generation and the Stratoscale template:
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)
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.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.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
File an issue or open a PR to fix this!
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…
}
http.StatusCreated
and the line just below, the body should match the
JSON
operator;$id
placeholder, Catch
its
value: put it in id
variable and check it is
NotZero
;$createdAt
placeholder, use the All
operator. It combines several operators like a AND;$createdAt
date ends with “Z” using
HasSuffix
. As we expect a RFC3339
date, we require it in UTC time zone;$createdAt
date into a time.Time
using a custom
function thanks to the Smuggle
operator;Catch
the resulting value: put it in
createdAt
variable and check it is greater or equal than
ta.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.
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!
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.
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 to td
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:
import "github.com/maxatome/go-testdeep/td"
then use td.DefaultContextConfig
instead of
testdeep.DefaultContextConfig
, and continue to use testdeep
package elsewhere.import "github.com/maxatome/go-testdeep"
by
import "github.com/maxatome/go-testdeep/td"
then rename all occurrences of testdeep
package to td
.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
Using some environment variables:
TESTDEEP_COLOR
enable (on
) or disable (off
) the color
output. It defaults to on
;TESTDEEP_COLOR_TEST_NAME
color of the test name. See below
for color format, it defaults to yellow
;TESTDEEP_COLOR_TITLE
color of the test failure title. See below
for color format, it defaults to cyan
;TESTDEEP_COLOR_OK
color of the test expected value. See below
for color format, it defaults to green
;TESTDEEP_COLOR_BAD
color of the test got value. See below
for color format, it defaults to red
;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
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.
X
testing framework allows to test/do Y
while go-testdeep notThe 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",
)
You want to add a new FooBar
operator.
td_foo_bar.go
file and fully
document its usage:
// summary(FooBar): small description
line, before
operator comment,// input(FooBar): …
line, just after summary(FooBar)
line. This one lists all inputs accepted by the operator;td_foo_bar_test.go
file;example_test.go
file, add examples function(s) ExampleFooBar*
in alphabetical order;JSON
,
SubJSONOf
and
SuperJSONOf
operators?
FooBar
to the forbiddenOpsInJSON
map in
td/td_json.go
with a possible alternative text to help the user,FooBar
needs specific handling as N
or
Between
does for example?CmpFooBar
& T.FooBar
(+ examples) code:
./tools/gen_funcs.pl
go test ./...
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%.