diff --git a/README.md b/README.md index 7344e311..218caa42 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,25 @@ go-testdeep [![Go Report Card](https://goreportcard.com/badge/github.com/maxatome/go-testdeep)](https://goreportcard.com/report/github.com/maxatome/go-testdeep) [![GoDoc](https://godoc.org/github.com/maxatome/go-testdeep?status.svg)](https://godoc.org/github.com/maxatome/go-testdeep) -Golang package `testdeep` allows extremely flexible deep comparison, -built for testing. +Extremely flexible golang deep comparison, extends the go testing package. + +- [Latest new](#latest-news) +- [Synopsis](#synopsis) +- [Installation](#installation) +- [Presentation](#presentation) +- [Available operators](#available-operators) +- [License](#license) ## Latest news +- 2018/06/19: new + [ContextConfig](https://godoc.org/github.com/maxatome/go-testdeep#ContextConfig) + feature `FailureIsFatal` available. See + [DefaultContextConfig](https://godoc.org/github.com/maxatome/go-testdeep#pkg-variables) + for default global value and + [`T.FailureIsFatal`](https://godoc.org/github.com/maxatome/go-testdeep#T.FailureIsFatal) + method; - 2018/06/17: new [`CmpPanic`](https://godoc.org/github.com/maxatome/go-testdeep#CmpPanic) & @@ -27,7 +40,6 @@ built for testing. [`CmpSmuggle`](https://godoc.org/github.com/maxatome/go-testdeep#CmpSmuggle) & [`T.Smuggle`](https://godoc.org/github.com/maxatome/go-testdeep#T.Smuggle)); -- 2018/06/11: `DefaultContextConfig.MaxErrors` defaults to 10 (was 1); - see [commits history](https://github.com/maxatome/go-testdeep/commits/master) for other/older changes. @@ -56,15 +68,16 @@ func TestCreateRecord(t *testing.T) { 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()), - }, + 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") } } @@ -255,7 +268,7 @@ func TestCreateRecord(t *testing.T) { if td.CmpDeeply(t, err, nil) { td.CmpDeeply(t, record, - Struct( + td.Struct( Record{ Name: "Bob", Age: 23, diff --git a/cmp_deeply.go b/cmp_deeply.go index 7b2c195a..cab96de5 100644 --- a/cmp_deeply.go +++ b/cmp_deeply.go @@ -13,7 +13,7 @@ import ( "strings" ) -func formatError(t TestingT, err *Error, args ...interface{}) { +func formatError(t TestingT, isFatal bool, err *Error, args ...interface{}) { t.Helper() const failedTest = "Failed test" @@ -40,7 +40,11 @@ func formatError(t TestingT, err *Error, args ...interface{}) { err.Append(buf, "") - t.Error(buf.String()) + if isFatal { + t.Fatal(buf.String()) + } else { + t.Error(buf.String()) + } } func cmpDeeply(ctx Context, t TestingT, got, expected interface{}, @@ -51,7 +55,7 @@ func cmpDeeply(ctx Context, t TestingT, got, expected interface{}, return true } t.Helper() - formatError(t, err, args...) + formatError(t, ctx.failureIsFatal, err, args...) return false } diff --git a/cmp_deeply_test.go b/cmp_deeply_test.go index 1eb56ecc..8f2199e5 100644 --- a/cmp_deeply_test.go +++ b/cmp_deeply_test.go @@ -8,22 +8,9 @@ package testdeep import ( "bytes" - "fmt" "testing" ) -type TestTestingT struct { - LastMessage string -} - -func (t *TestTestingT) Error(args ...interface{}) { - t.LastMessage = fmt.Sprint(args...) -} - -func (t *TestTestingT) Helper() { - // Do nothing -} - func TestFormatError(t *testing.T) { ttt := &TestTestingT{} @@ -35,41 +22,49 @@ func TestFormatError(t *testing.T) { nonStringName := bytes.NewBufferString("zip!") - // - // Without args - formatError(ttt, err) - equalStr(t, ttt.LastMessage, `Failed test + for _, fatal := range []bool{false, true} { + // + // Without args + formatError(ttt, fatal, err) + equalStr(t, ttt.LastMessage, `Failed test DATA: test error message test error summary`) + equalBool(t, ttt.IsFatal, fatal) - // - // With one arg - formatError(ttt, err, "foo bar!") - equalStr(t, ttt.LastMessage, `Failed test 'foo bar!' + // + // With one arg + formatError(ttt, fatal, err, "foo bar!") + equalStr(t, ttt.LastMessage, `Failed test 'foo bar!' DATA: test error message test error summary`) + equalBool(t, ttt.IsFatal, fatal) - formatError(ttt, err, nonStringName) - equalStr(t, ttt.LastMessage, `Failed test 'zip!' + formatError(ttt, fatal, err, nonStringName) + equalStr(t, ttt.LastMessage, `Failed test 'zip!' DATA: test error message test error summary`) + equalBool(t, ttt.IsFatal, fatal) - // - // With several args & Printf format - formatError(ttt, err, "hello %d!", 123) - equalStr(t, ttt.LastMessage, `Failed test 'hello 123!' + // + // With several args & Printf format + formatError(ttt, fatal, err, "hello %d!", 123) + equalStr(t, ttt.LastMessage, `Failed test 'hello 123!' DATA: test error message test error summary`) + equalBool(t, ttt.IsFatal, fatal) - // - // With several args without Printf format - formatError(ttt, err, "hello ", "world! ", 123) - equalStr(t, ttt.LastMessage, `Failed test 'hello world! 123' + // + // With several args without Printf format + formatError(ttt, fatal, err, "hello ", "world! ", 123) + equalStr(t, ttt.LastMessage, `Failed test 'hello world! 123' DATA: test error message test error summary`) + equalBool(t, ttt.IsFatal, fatal) - formatError(ttt, err, nonStringName, "hello ", "world! ", 123) - equalStr(t, ttt.LastMessage, `Failed test 'zip!hello world! 123' + formatError(ttt, fatal, err, nonStringName, "hello ", "world! ", 123) + equalStr(t, ttt.LastMessage, `Failed test 'zip!hello world! 123' DATA: test error message test error summary`) + equalBool(t, ttt.IsFatal, fatal) + } } diff --git a/cmp_funcs_misc.go b/cmp_funcs_misc.go index de8ffcd8..f13b1288 100644 --- a/cmp_funcs_misc.go +++ b/cmp_funcs_misc.go @@ -45,6 +45,7 @@ func cmpError(ctx Context, t TestingT, got error, args ...interface{}) bool { t.Helper() formatError(t, + ctx.failureIsFatal, &Error{ Context: ctx, Message: "should be an error", @@ -63,6 +64,7 @@ func cmpNoError(ctx Context, t TestingT, got error, args ...interface{}) bool { t.Helper() formatError(t, + ctx.failureIsFatal, &Error{ Context: ctx, Message: "should NOT be an error", @@ -125,6 +127,7 @@ func cmpPanic(ctx Context, t TestingT, fn func(), expected interface{}, args ... if !panicked { formatError(t, + ctx.failureIsFatal, &Error{ Context: ctx, Message: "should have panicked", @@ -174,6 +177,7 @@ func cmpNotPanic(ctx Context, t TestingT, fn func(), args ...interface{}) bool { } formatError(t, + ctx.failureIsFatal, &Error{ Context: ctx, Message: "should NOT have panicked", diff --git a/context.go b/context.go index 41cc2953..22ed6006 100644 --- a/context.go +++ b/context.go @@ -36,6 +36,11 @@ type ContextConfig struct { // Setting it to a negative number means no limit: all errors // will be dumped. MaxErrors int + // FailureIsFatal allows to Fatal() (instead of Error()) when a test + // fails. Using *testing.T instance as + // t.TestingFT value, FailNow() is called behind the scenes when + // Fatal() is called. See testing documentation for details. + FailureIsFatal bool } const ( @@ -59,8 +64,9 @@ func getMaxErrorsFromEnv() int { // tests failures. If overridden, new settings will impact all Cmp* // functions and *T methods (if not specifically configured.) var DefaultContextConfig = ContextConfig{ - RootName: contextDefaultRootName, - MaxErrors: getMaxErrorsFromEnv(), + RootName: contextDefaultRootName, + MaxErrors: getMaxErrorsFromEnv(), + FailureIsFatal: false, } func (c *ContextConfig) sanitize() { @@ -96,6 +102,8 @@ type Context struct { // < 0 do not stop until comparison ends. maxErrors int errors *[]*Error + // See ContextConfig.FailureIsFatal for details + failureIsFatal bool } // NewContext creates a new Context using DefaultContextConfig configuration. @@ -108,9 +116,10 @@ func NewContextWithConfig(config ContextConfig) (ctx Context) { config.sanitize() ctx = Context{ - path: config.RootName, - visited: map[visit]bool{}, - maxErrors: config.MaxErrors, + path: config.RootName, + visited: map[visit]bool{}, + maxErrors: config.MaxErrors, + failureIsFatal: config.FailureIsFatal, } ctx.initErrors() diff --git a/context_test.go b/context_test.go index 52624768..d7e701e2 100644 --- a/context_test.go +++ b/context_test.go @@ -35,6 +35,18 @@ func equalInt(t *testing.T, got, expected int) bool { return false } +func equalBool(t *testing.T, got, expected bool) bool { + if got == expected { + return true + } + + t.Helper() + t.Errorf(`Failed test + got: %t + expected: %t`, got, expected) + return false +} + func TestContext(t *testing.T) { equalStr(t, NewContext().path, "DATA") equalStr(t, NewBooleanContext().path, "") diff --git a/t_struct.go b/t_struct.go index cd84a6b6..b5c83655 100644 --- a/t_struct.go +++ b/t_struct.go @@ -166,6 +166,37 @@ func (t *T) RootName(rootName string) *T { return &new } +// FailureIsFatal allows to choose whether t.TestingFT.Fatal() or +// t.TestingFT.Error() will be used to print the next failure +// reports. When "enable" is true (or missing) testing.Fatal() will be +// called, else testing.Error(). Using *testing.T instance as +// t.TestingFT value, FailNow() is called behind the scenes when +// Fatal() is called. See testing documentation for details. +// +// It returns a new instance of *T so does not alter the original t +// and used as follows: +// +// // Following t.CmpDeeply() will call Fatal() if failure +// t = t.FailureIsFatal() +// t.CmpDeeply(...) +// t.CmpDeeply(...) +// // Following t.CmpDeeply() won't call Fatal() if failure +// t = t.FailureIsFatal(false) +// t.CmpDeeply(...) +// +// or, if only one call is critic: +// +// // This CmpDeeply() call will call Fatal() if failure +// t.FailureIsFatal().CmpDeeply(...) +// // Following t.CmpDeeply() won't call Fatal() if failure +// t.CmpDeeply(...) +// t.CmpDeeply(...) +func (t *T) FailureIsFatal(enable ...bool) *T { + new := *t + new.Config.FailureIsFatal = len(enable) == 0 || enable[0] + return &new +} + // CmpDeeply is mostly a shortcut for: // // CmpDeeply(t.TestingFT, got, expected, args...) diff --git a/t_struct_test.go b/t_struct_test.go index 8d3b8123..ebbb75ed 100644 --- a/t_struct_test.go +++ b/t_struct_test.go @@ -200,3 +200,39 @@ func TestRun(tt *testing.T) { t.True(ok) t.True(runPassed) } + +func TestFailureIsFatal(tt *testing.T) { + ttt := &TestTestingFT{} + + // All t.True(false) tests of course fail + + // Using default config + t := NewT(ttt) + t.True(false) // failure + CmpNotEmpty(tt, ttt.LastMessage) + CmpFalse(tt, ttt.IsFatal, "by default it not fatal") + + // Using specific config + t = NewT(ttt, ContextConfig{FailureIsFatal: true}) + t.True(false) // failure + CmpNotEmpty(tt, ttt.LastMessage) + CmpTrue(tt, ttt.IsFatal, "it must be fatal") + + // Using FailureIsFatal() + t = NewT(ttt).FailureIsFatal() + t.True(false) // failure + CmpNotEmpty(tt, ttt.LastMessage) + CmpTrue(tt, ttt.IsFatal, "it must be fatal") + + // Using FailureIsFatal(true) + t = NewT(ttt).FailureIsFatal(true) + t.True(false) // failure + CmpNotEmpty(tt, ttt.LastMessage) + CmpTrue(tt, ttt.IsFatal, "it must be fatal") + + // Canceling specific config + t = NewT(ttt, ContextConfig{FailureIsFatal: false}).FailureIsFatal(false) + t.True(false) // failure + CmpNotEmpty(tt, ttt.LastMessage) + CmpFalse(tt, ttt.IsFatal, "it must be not fatal") +} diff --git a/types.go b/types.go index fab001cc..5342c38c 100644 --- a/types.go +++ b/types.go @@ -29,6 +29,7 @@ var ( // errors. It is commonly implemented by *testing.T and testing.TB. type TestingT interface { Error(args ...interface{}) + Fatal(args ...interface{}) Helper() } @@ -36,7 +37,21 @@ type TestingT interface { // delegate common *testing.T functions to it. Of course, *testing.T // implements it. type TestingFT interface { - testing.TB + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fail() + FailNow() + Failed() bool + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Name() string + Skip(args ...interface{}) + SkipNow() + Skipf(format string, args ...interface{}) + Skipped() bool + Helper() Run(name string, f func(t *testing.T)) bool } diff --git a/types_test.go b/types_test.go new file mode 100644 index 00000000..63312654 --- /dev/null +++ b/types_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2018, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +package testdeep + +import ( + "fmt" + "testing" +) + +type TestTestingT struct { + LastMessage string + IsFatal bool + HasFailed bool +} + +func (t *TestTestingT) Error(args ...interface{}) { + t.LastMessage = fmt.Sprint(args...) + t.IsFatal = false + t.HasFailed = true +} + +func (t *TestTestingT) Fatal(args ...interface{}) { + t.LastMessage = fmt.Sprint(args...) + t.IsFatal = true + t.HasFailed = true +} + +func (t *TestTestingT) Helper() { + // Do nothing +} + +type TestTestingFT struct { + TestTestingT +} + +func (t *TestTestingFT) Errorf(format string, args ...interface{}) { + t.Error(fmt.Sprintf(format, args...)) +} + +func (t *TestTestingFT) Fail() { + t.HasFailed = true +} + +func (t *TestTestingFT) FailNow() { + t.HasFailed = true + t.IsFatal = true +} + +func (t *TestTestingFT) Failed() bool { + return t.HasFailed +} + +func (t *TestTestingFT) Fatalf(format string, args ...interface{}) { + t.Fatal(fmt.Sprintf(format, args...)) +} + +func (t *TestTestingFT) Log(args ...interface{}) {} +func (t *TestTestingFT) Logf(format string, args ...interface{}) {} + +func (t *TestTestingFT) Name() string { + return "" +} + +func (t *TestTestingFT) Skip(args ...interface{}) {} +func (t *TestTestingFT) SkipNow() {} +func (t *TestTestingFT) Skipf(format string, args ...interface{}) {} +func (t *TestTestingFT) Skipped() bool { + return false +} + +func (t *TestTestingFT) Run(name string, f func(t *testing.T)) bool { + return true +}