Extremely flexible golang deep comparison, extends the go testing package
Clone or download
maxatome Merge pull request #47 from maxatome/tdhttp
tdhttp helper + colored output
Latest commit 4ea8fb8 Jan 15, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
doc Add tdhttp (first) helper Jan 14, 2019
helpers Add tdhttp (first) helper Jan 14, 2019
internal Add ANSI colored output Jan 14, 2019
tools Add operators godoc links for each derivative function/method Jan 6, 2019
.travis.yml Add ANSI colored output Jan 14, 2019
LICENSE Allows github to correctly detect the BSD 2-Clause License Jun 12, 2018
README.md Announce new version Jan 14, 2019
check_test.go Add tdhttp (first) helper Jan 14, 2019
cmp_deeply.go Add tdhttp (first) helper Jan 14, 2019
cmp_deeply_test.go Add unit tests to cover all t.Helper() usages Aug 19, 2018
cmp_funcs.go Add operators godoc links for each derivative function/method Jan 6, 2019
cmp_funcs_misc.go Work around golang/go#26995 issue Aug 18, 2018
cmp_funcs_misc_test.go Add unit tests to cover all t.Helper() usages Aug 19, 2018
cmp_funcs_test.go Between, Get, Gte, Lt & Lte now handles strings Dec 16, 2018
config.go Make NewContext* functions private Aug 18, 2018
config_test.go Make NewContext* functions private Aug 18, 2018
equal.go Dereference interfaces before calling Testdeep operators Dec 10, 2018
equal_examples_test.go Add verifiable output to encountered error example Dec 23, 2018
equal_test.go Make last version of gometalinter happy Jan 6, 2019
example_test.go Between, Get, Gte, Lt & Lte now handles strings Dec 16, 2018
go.mod Update go.mod + go.sum Dec 10, 2018
go.sum Update go.mod + go.sum Dec 10, 2018
private_test.go Add README + copyright stuff May 26, 2018
t.go Add operators godoc links for each derivative function/method Jan 6, 2019
t_struct.go No "(int) " prefix for ints in error reports Dec 16, 2018
t_struct_examples_test.go Definitively avoid importing `. "github.com/maxatome/go-testdeep"` Aug 18, 2018
t_struct_test.go Add tdhttp (first) helper Jan 14, 2019
t_test.go Between, Get, Gte, Lt & Lte now handles strings Dec 16, 2018
td_all.go Implement Any() TypeBehind() method (shared with All()) Jan 8, 2019
td_all_test.go Implement All() TypeBehind() method Jan 7, 2019
td_any.go Implement Any() TypeBehind() method (shared with All()) Jan 8, 2019
td_any_test.go Implement Any() TypeBehind() method (shared with All()) Jan 8, 2019
td_array.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_array_each.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_array_each_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_array_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_bag.go README update + fix typos May 27, 2018
td_bag_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_between.go Between, Get, Gte, Lt & Lte now handles strings Dec 16, 2018
td_between_test.go Between, Get, Gte, Lt & Lte now handles strings Dec 16, 2018
td_code.go Code func can now return an error Dec 10, 2018
td_code_test.go Make last version of gometalinter happy Jan 6, 2019
td_contains.go Typo Oct 31, 2018
td_contains_key.go ContainsKey error reworked Nov 2, 2018
td_contains_key_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_contains_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_empty.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_empty_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_expected_type.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_ignore.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_ignore_test.go Definitively avoid importing `. "github.com/maxatome/go-testdeep"` Aug 18, 2018
td_isa.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_isa_test.go Implement All() TypeBehind() method Jan 7, 2019
td_len_cap.go Doc typo Nov 5, 2018
td_len_cap_test.go Revert "checkPanic() comes back into testdeep_test package" Aug 18, 2018
td_list.go Implement Any() TypeBehind() method (shared with All()) Jan 8, 2019
td_map.go Do not break godoc with //go:noinline pragmas Dec 16, 2018
td_map_each.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_map_each_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_map_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_nan.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_nan_test.go reformat: expectedError{} always on its own line Aug 18, 2018
td_nil.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_nil_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_none.go Do not break godoc with //go:noinline pragmas Dec 16, 2018
td_none_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_ptr.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_ptr_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_re.go Do not break godoc with //go:noinline pragmas Dec 16, 2018
td_re_test.go Revert "checkPanic() comes back into testdeep_test package" Aug 18, 2018
td_set.go NoneOf becomes NotAny Jun 14, 2018
td_set_base.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_set_result.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_set_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_shallow.go Shallow now handles strings as well Dec 6, 2018
td_shallow_test.go Dereference interfaces before calling Testdeep operators Dec 10, 2018
td_smuggle.go Correct Smuggle doc Dec 10, 2018
td_smuggle_test.go Make last version of gometalinter happy Jan 6, 2019
td_smuggler_base.go Add Smuggle operator Jun 15, 2018
td_string.go internal/str → internal/util + utils.go split into . and internal/util Aug 18, 2018
td_string_test.go reformat: expectedError{} always on its own line Aug 18, 2018
td_struct.go Tests pass when "unsafe" package not available Aug 18, 2018
td_struct_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
td_trunc_time.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_trunc_time_test.go Dereference interfaces before calling Testdeep operators Dec 10, 2018
td_zero.go Context & Error move to internal/ctxerr package Aug 18, 2018
td_zero_test.go No "(int) " prefix for ints in error reports Dec 16, 2018
testdeep.go Correct doc typos Jul 6, 2018
types.go Make last version of gometalinter happy Jan 6, 2019
utils.go Correct getTime returned Error Aug 18, 2018
utils_test.go Add unit tests Aug 18, 2018

README.md

go-testdeep

Build Status Coverage Status Go Report Card GoDoc Version Awesome

testdeep

Extremely flexible golang deep comparison, extends the go testing package.

Latest news

Synopsis

import (
  "testing"
  "time"

  td "github.com/maxatome/go-testdeep"
)

type Record struct {
  Id        uint64
  Name      string
  Age       int
  CreatedAt time.Time
}

func CreateRecord(name string, age int) (*Record, error) {
  ...
}

func TestCreateRecord(t *testing.T) {
  before := time.Now()
  record, err := CreateRecord("Bob", 23)

  if td.CmpNoError(t, err) {
    // If you know the exact value of all fields of newly created record
    td.CmpDeeply(t, record,
      &Record{
        Id:        245,
        Name:      "Bob",
        Age:       23,
        CreatedAt: time.Date(2018, time.May, 1, 11, 12, 13, 0, time.UTC),
      },
      "Fixed value for each field of newly created record")

    // But as often you cannot guess the values of DB generated fields,
    // you can choose to ignore them and only test the non-zero ones
    td.CmpDeeply(t, record,
      td.Struct(
        &Record{
          Name: "Bob",
          Age:  23,
        },
        nil),
      "Name & Age fields of newly created record")

    // Anyway, it is better to be able to test all fields!
    td.CmpDeeply(t, record,
      td.Struct(
        &Record{
          Name: "Bob",
          Age:  23,
        },
        td.StructFields{
          "Id":        td.NotZero(),
          "CreatedAt": td.Between(before, time.Now()),
        }),
      "Newly created record")
  }
}

Imagine CreateRecord does not set correctly CreatedAt field, then:

go test -run=TestCreateRecord

outputs for last td.CmpDeeply call:

--- FAIL: TestCreateRecord (0.00s)
  test_test.go:46: Failed test 'Newly created record'
    DATA.CreatedAt: values differ
           got: 2018-05-27 10:55:50.788166932 +0200 CEST m=-2.998149554
      expected: 2018-05-27 10:55:53.788163509 +0200 CEST m=+0.001848002 ≤ got ≤ 2018-05-27 10:55:53.788464176 +0200 CEST m=+0.002148179
    [under TestDeep operator Between at test_test.go:54]
FAIL
exit status 1
FAIL  github.com/maxatome/go-testdeep  0.006s

If CreateRecord had not set correctly Id field, output would have been:

--- FAIL: TestCreateRecord (0.00s)
  test_test.go:46: Failed test 'Newly created record'
    DATA.Id: zero value
           got: (uint64) 0
      expected: NotZero()
    [under TestDeep operator Not at test_test.go:53]
FAIL
exit status 1
FAIL  github.com/maxatome/go-testdeep  0.006s

If CreateRecord had not set Name field to "Alice" value instead of expected "Bob", output would have been:

--- FAIL: TestCreateRecord (0.00s)
  test_test.go:46: Failed test 'Newly created record'
    DATA.Name: values differ
           got: "Alice"
      expected: "Bob"
FAIL
exit status 1
FAIL  github.com/maxatome/go-testdeep  0.006s

Using testdeep.T type, TestCreateRecord can also be written as:

import (
  "testing"
  "time"

  td "github.com/maxatome/go-testdeep"
)

type Record struct {
  Id        uint64
  Name      string
  Age       int
  CreatedAt time.Time
}

func CreateRecord(name string, age int) (*Record, error) {
  ...
}

func TestCreateRecord(tt *testing.T) {
  t := td.NewT(tt)

  before := time.Now()
  record, err := CreateRecord("Bob", 23)

  if t.CmpNoError(err) {
    t := t.RootName("RECORD") // Use RECORD instead of DATA in failure reports

    // If you know the exact value of all fields of newly created record
    t.CmpDeeply(record,
      &Record{
        Id:        245,
        Name:      "Bob",
        Age:       23,
        CreatedAt: time.Date(2018, time.May, 1, 11, 12, 13, 0, time.UTC),
      },
      "Fixed value for each field of newly created record")

    // Anyway, it is better to be able to test all fields in a generic way!
    // Using Struct method
    t.Struct(record,
      Record{
        Name: "Bob",
        Age:  23,
      },
      td.StructFields{
        "Id":        td.NotZero(),
        "CreatedAt": td.Between(before, time.Now()),
      },
      "Newly created record")

    // Or using CmpDeeply method, it's a matter of taste
    t.CmpDeeply(record,
      td.Struct(
        Record{
          Name: "Bob",
          Age:  23,
        },
        td.StructFields{
          "Id":        td.NotZero(),
          "CreatedAt": td.Between(before, time.Now()),
        }),
      "Newly created record")
  }
}

Installation

$ go get -u github.com/maxatome/go-testdeep

Presentation

Package testdeep allows extremely flexible deep comparison, built for testing.

It is a go rewrite and adaptation of wonderful Test::Deep perl module.

In golang, comparing data structure is usually done using reflect.DeepEqual or using a package that uses this function behind the scene.

This function works very well, but it is not flexible. Both compared structures must match exactly.

The purpose of testdeep package is to do its best to introduce this missing flexibility using operators when the expected value (or one of its component) cannot be matched exactly.

Imagine a function returning a struct containing a newly created database record. The Id and the CreatedAt fields are set by the database layer. In this case we have to do something like that to check the record contents:

import (
  "testing"
)

...

func TestCreateRecord(t *testing.T) {
  before := time.Now()
  record, err := CreateRecord("Bob", 23)

  if err != nil {
    t.Errorf("An error occurred: %s", err)
  } else {
    expected := Record{Name: "Bob", Age: 23}

    if record.Id == 0 {
      t.Error("Id probably not initialized")
    }
    if before.After(record.CreatedAt) ||
      time.Now().Before(record.CreatedAt) {
      t.Errorf("CreatedAt field not expected: %s", record.CreatedAt)
    }
    if record.Name != expected.Name {
      t.Errorf("Name field differ, got=%s, expected=%s",
        record.Name, expected.Name)
    }
    if record.Age != expected.Age {
      t.Errorf("Age field differ, got=%s, expected=%s",
        record.Age, expected.Age)
    }
  }
}

With testdeep, it is a way simple, thanks to CmpDeeply function:

import (
  "testing"
  td "github.com/maxatome/go-testdeep"
)

...

func TestCreateRecord(t *testing.T) {
  before := time.Now()
  record, err := CreateRecord("Bob", 23)

  if td.CmpDeeply(t, err, nil) {
    td.CmpDeeply(t, record,
      td.Struct(
        Record{
          Name: "Bob",
          Age:  23,
        },
        td.StructFields{
          "Id":        td.NotZero(),
          "CreatedAt": td.Between(before, time.Now()),
        }),
      "Newly created record")
  }
}

Of course not only structs can be compared. A lot of operators can be found below to cover most (all?) needed tests.

The CmpDeeply function is the keystone of this package, but to make the writing of tests even easier, the family of Cmp* functions are provided and act as shortcuts. Using CmpNoError and CmpStruct function, the previous example can be written as:

import (
  "testing"
  td "github.com/maxatome/go-testdeep"
)

...

func TestCreateRecord(t *testing.T) {
  before := time.Now()
  record, err := CreateRecord("Bob", 23)

  if td.CmpNoError(t, err) {
    td.CmpStruct(t, record,
      Record{
        Name: "Bob",
        Age:  23,
      },
      td.StructFields{
        "Id":        td.NotZero(),
        "CreatedAt": td.Between(before, time.Now()),
      },
      "Newly created record")
  }
}

Last, testing.T can be encapsulated in T type, simplifying again the test:

import (
  "testing"
  td "github.com/maxatome/go-testdeep"
)

...

func TestCreateRecord(tt *testing.T) {
  t := td.NewT(tt)

  before := time.Now()
  record, err := CreateRecord()

  if t.CmpNoError(err) {
    t.Struct(record,
      Record{
        Name: "Bob",
        Age:  23,
      },
      td.StructFields{
        "Id":        td.NotZero(),
        "CreatedAt": td.Between(before, time.Now()),
      },
      "Newly created record")
  }
}

Available operators

See functions returning TestDeep interface:

  • All all expected values have to match;
  • Any at least one expected value have to match;
  • Array compares the contents of an array or a pointer on an array;
  • ArrayEach compares each array or slice item;
  • Bag compares the contents of an array or a slice without taking care of the order of items;
  • Between checks that a number, string or time.Time is between two bounds;
  • Cap checks an array, slice or channel capacity;
  • Code allows to use a custom function;
  • Contains checks that a string, error or fmt.Stringer interfaces contain a sub-string; or an array, slice or map contain a value;
  • ContainsKey checks that a map contains a key;
  • Empty checks that an array, a channel, a map, a slice or a string is empty;
  • Gt checks that a number, string or time.Time is greater than a value;
  • Gte checks that a number, string or time.Time is greater or equal than a value;
  • HasPrefix checks the prefix of a string, error or fmt.Stringer interfaces;
  • HasSuffix checks the suffix of a string, error or fmt.Stringer interfaces;
  • Ignore allows to ignore a comparison;
  • Isa checks the data type or whether data implements an interface or not;
  • Len checks an array, slice, map, string or channel length;
  • Lt checks that a number, string or time.Time is lesser than a value;
  • Lte checks that a number, string or time.Time is lesser or equal than a value;
  • Map compares the contents of a map;
  • MapEach compares each map entry;
  • N compares a number with a tolerance value;
  • NaN checks a floating number is math.NaN;
  • Nil compares to nil;
  • None no values have to match;
  • NotAny compares the contents of an array or a slice, no values have to match;
  • Not value must not match;
  • NotEmpty checks that an array, a channel, a map, a slice or a string is not empty;
  • NotNaN checks a floating number is not math.NaN;
  • NotNil checks that data is not nil;
  • NotZero checks that data is not zero regarding its type;
  • PPtr allows to easily test a pointer of pointer value,
  • Ptr allows to easily test a pointer value,
  • Re allows to apply a regexp on a string (or convertible), []byte, error or fmt.Stringer interfaces, and even test the captured groups;
  • ReAll allows to successively apply a regexp on a string (or convertible), []byte, error or fmt.Stringer interfaces, and even test the captured groups;
  • Set compares the contents of an array or a slice ignoring duplicates and without taking care of the order of items;
  • Shallow compares pointers only, not their contents;
  • Slice compares the contents of a slice or a pointer on a slice;
  • Smuggle changes data contents or mutates it into another type via a custom function or a struct fields-path before stepping down in favor of generic comparison process;
  • String checks a string, error or fmt.Stringer interfaces string contents;
  • Struct compares the contents of a struct or a pointer on a struct;
  • SubBagOf compares the contents of an array or a slice without taking care of the order of items but with potentially some exclusions;
  • SubMapOf compares the contents of a map but with potentially some exclusions;
  • SubSetOf compares the contents of an array or a slice ignoring duplicates and without taking care of the order of items but with potentially some exclusions;
  • SuperBagOf compares the contents of an array or a slice without taking care of the order of items but with potentially some extra items;
  • SuperMapOf compares the contents of a map but with potentially some extra entries;
  • SuperSetOf compares the contents of an array or a slice ignoring duplicates and without taking care of the order of items but with potentially some extra items;
  • TruncTime compares time.Time (or assignable) values after truncating them;
  • Zero checks data against its zero'ed conterpart.

Helpers

The goal of helpers is to make use of go-testdeep even more powerful by providing common features using TestDeep operators behind the scene.

tdhttp or HTTP API testing helper

The package github.com/maxatome/go-testdeep/helpers/tdhttp provides some functions to easily test HTTP handlers.

See tdhttp documentation for details or FAQ for an example of use.

Environment variables

  • TESTDEEP_MAX_ERRORS maximum number of errors to report before stopping during one comparison (one CmpDeeply execution for example). It defaults to 10;
  • TESTDEEP_COLOR enable (on) or disable (off) the color output. It defaults to on;
  • 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;

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

Operators vs go types

Operator vs go type nil bool string {u,}int* float* complex* array slice map struct pointer interface¹ chan func operator
All All
Any Any
Array ptr on array Array
ArrayEach ptr on array/slice ArrayEach
Bag ptr on array/slice Bag
Between todo time.Time Between
Cap Cap
Code Code
Contains Contains
ContainsKey ContainsKey
Operator vs go type nil bool string {u,}int* float* complex* array slice map struct pointer interface¹ chan func operator
Empty ptr or/array/slice/map/string Empty
Gt todo time.Time Gt
Gte todo time.Time Gte
HasPrefix ✓ + fmt.Stringer, error HasPrefix
HasSuffix ✓ + fmt.Stringer, error HasSuffix
Ignore Ignore
Isa Isa
Len Len
Lt todo time.Time Lt
Lte todo time.Time Lte
Operator vs go type nil bool string {u,}int* float* complex* array slice map struct pointer interface¹ chan func operator
Map ptr on map Map
MapEach ptr on map MapEach
N todo N
NaN NaN
Nil Nil
None None
NotAny ptr on array/slice NotAny
Not Not
NotEmpty ptr on array/slice/map/string NotEmpty
NotNaN NotNaN
Operator vs go type nil bool string {u,}int* float* complex* array slice map struct pointer interface¹ chan func operator
NotNil NotNil
NotZero NotZero
PPtr PPtr
Ptr Ptr
Re []byte ✓ + fmt.Stringer, error Re
ReAll []byte ✓ + fmt.Stringer, error ReAll
Set ptr on array/slice Set
Shallow Shallow
Slice ptr on slice Slice
Smuggle Smuggle
Operator vs go type nil bool string {u,}int* float* complex* array slice map struct pointer interface¹ chan func operator
String ✓ + fmt.Stringer, error String
Struct ptr on struct Struct
SubBagOf ptr on array/slice SubBagOf
SubMapOf ptr on map SubMapOf
SubSetOf ptr on array/slice SubSetOf
SuperBagOf ptr on array/slice SuperBagOf
SuperMapOf ptr on map SuperMapOf
SuperSetOf ptr on array/slice SuperSetOf
TruncTime time.Time todo TruncTime
Zero Zero

Legend:

  • ✗ means using this operator with this go type will always fail
  • ✓ means using this operator with this go type can succeed
  • []byte, time.Time, ptr on X, fmt.Stringer, error means using this operator with this go type can succeed
  • todo means should be implemented in future (PRs welcome :) )
  • ¹ + ✓ means using this operator with the data behind the interface can succeed

See also

  • testify: a toolkit with common assertions and mocks that plays nicely with the standard library
  • go-cmp: package for comparing Go values in tests

License

go-testdeep is released under the BSD-style license found in the LICENSE file in the root directory of this source tree.

Internal function deepValueEqual is based on deepValueEqual from reflect golang package licensed under the BSD-style license found in the LICENSE file in the golang repository.

Uses two files (bypass.go & bypasssafe.go) from Go-spew which is licensed under the copyfree ISC License.

Public Domain Gopher provided by Egon Elbre. The Go gopher was designed by Renee French.

FAQ

See FAQ.