From 7eb5eefc7bc9a021cac76eb5c0d4b99063d41696 Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Fri, 18 Aug 2023 09:14:39 +0100 Subject: [PATCH 1/9] Attempting to provide a useful implementation of something compatible with testing.T --- suite.go | 47 +++++++++++++++++++++++++++++++-- testingt.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 testingt.go diff --git a/suite.go b/suite.go index 3de07dec..96c8c46a 100644 --- a/suite.go +++ b/suite.go @@ -85,12 +85,18 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena // user multistep definitions may panic defer func() { if e := recover(); e != nil { - if err != nil { + pe, isErr := e.(error) + switch { + case isErr && errors.Is(pe, errFailNow): + // FailNow called on dogTestingT, recover the error so we set the step as a 'normal' + // fail instead of normal panic handling + err = e.(error) + case err != nil: err = &traceError{ msg: fmt.Sprintf("%s: %v", err.Error(), e), stack: callStack(), } - } else { + default: err = &traceError{ msg: fmt.Sprintf("%v", e), stack: callStack(), @@ -509,17 +515,26 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { s.fmt.Pickle(pickle) + dt := &dogTestingT{} + ctx = setContextDogTester(ctx, dt) // scenario if s.testingT != nil { // Running scenario as a subtest. s.testingT.Run(pickle.Name, func(t *testing.T) { + dt.t = t ctx, err = s.runSteps(ctx, pickle, pickle.Steps) + if err == nil { + err = dt.check() + } if s.shouldFail(err) { t.Errorf("%+v", err) } }) } else { ctx, err = s.runSteps(ctx, pickle, pickle.Steps) + if err == nil { + err = dt.check() + } } // After scenario handlers are called in context of last evaluated step @@ -527,3 +542,31 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { return err } + +type TestingT interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fail() + FailNow() +} + +// Logf will log test output. If called in the context of a test and testing.T has been registered, +// this will log using the step's testing.T, else it will simply log to stdout. +func Logf(ctx context.Context, format string, args ...interface{}) { + if t := getDogTestingT(ctx); t != nil { + t.Logf(format, args...) + return + } + fmt.Printf(format+"\n", args...) +} + +// Log will log test output. If called in the context of a test and testing.T has been registered, +// this will log using the step's testing.T, else it will simply log to stdout. +func Log(ctx context.Context, args ...interface{}) { + if t := getDogTestingT(ctx); t != nil { + t.Log(args...) + return + } + fmt.Println(args...) +} diff --git a/testingt.go b/testingt.go new file mode 100644 index 00000000..85bb0a24 --- /dev/null +++ b/testingt.go @@ -0,0 +1,76 @@ +package godog + +import ( + "context" + "fmt" + "testing" +) + +// errFailNow should be returned inside a panic within the test to immediately halt execution of that +// test +var errFailNow = fmt.Errorf("FailNow called") + +type dogTestingT struct { + t *testing.T + failed bool +} + +// check interface: +var _ TestingT = &dogTestingT{} + +func (dt *dogTestingT) Log(args ...interface{}) { + if dt.t != nil { + dt.t.Log(args...) + return + } + fmt.Println(args...) +} + +func (dt *dogTestingT) Logf(format string, args ...interface{}) { + if dt.t != nil { + dt.t.Logf(format, args...) + return + } + fmt.Printf(format+"\n", args...) +} + +func (dt *dogTestingT) Errorf(format string, args ...interface{}) { + dt.Logf(format, args...) + dt.failed = true +} + +func (dt *dogTestingT) Fail() { + dt.failed = true +} + +func (dt *dogTestingT) FailNow() { + panic(errFailNow) +} + +func (dt *dogTestingT) check() error { + if dt.failed { + return fmt.Errorf("one or more checks failed") + } + + return nil +} + +type testingTCtxVal struct{} + +func setContextDogTester(ctx context.Context, dt *dogTestingT) context.Context { + return context.WithValue(ctx, testingTCtxVal{}, dt) +} +func getDogTestingT(ctx context.Context) *dogTestingT { + dt, ok := ctx.Value(testingTCtxVal{}).(*dogTestingT) + if !ok { + return nil + } + return dt +} + +// GetTestingT returns a TestingT compatible interface from the current test context. It will return +// nil if called outside the context of a test. This can be used with (for example) testify's assert +// and require packages. +func GetTestingT(ctx context.Context) TestingT { + return getDogTestingT(ctx) +} From b0cfbf5f92a4e6080e5c1d9f1a886d27f7404507 Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Fri, 18 Aug 2023 10:48:17 +0100 Subject: [PATCH 2/9] Handle Fail calls on the TestingT in the right place --- suite.go | 19 +++++-------------- testingt.go | 50 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/suite.go b/suite.go index 96c8c46a..87d9619e 100644 --- a/suite.go +++ b/suite.go @@ -106,6 +106,11 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena earlyReturn := scenarioErr != nil || errors.Is(err, ErrUndefined) + // Check for any calls to Fail on dogT + if err == nil { + err = getDogTestingT(ctx).isFailed() + } + switch { case errors.Is(err, ErrPending): status = StepPending @@ -523,18 +528,12 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { s.testingT.Run(pickle.Name, func(t *testing.T) { dt.t = t ctx, err = s.runSteps(ctx, pickle, pickle.Steps) - if err == nil { - err = dt.check() - } if s.shouldFail(err) { t.Errorf("%+v", err) } }) } else { ctx, err = s.runSteps(ctx, pickle, pickle.Steps) - if err == nil { - err = dt.check() - } } // After scenario handlers are called in context of last evaluated step @@ -543,14 +542,6 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { return err } -type TestingT interface { - Log(args ...interface{}) - Logf(format string, args ...interface{}) - Errorf(format string, args ...interface{}) - Fail() - FailNow() -} - // Logf will log test output. If called in the context of a test and testing.T has been registered, // this will log using the step's testing.T, else it will simply log to stdout. func Logf(ctx context.Context, format string, args ...interface{}) { diff --git a/testingt.go b/testingt.go index 85bb0a24..ef95ccb3 100644 --- a/testingt.go +++ b/testingt.go @@ -3,16 +3,33 @@ package godog import ( "context" "fmt" + "strings" "testing" ) +type TestingT interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fail() + FailNow() +} + +// GetTestingT returns a TestingT compatible interface from the current test context. It will return +// nil if called outside the context of a test. This can be used with (for example) testify's assert +// and require packages. +func GetTestingT(ctx context.Context) TestingT { + return getDogTestingT(ctx) +} + // errFailNow should be returned inside a panic within the test to immediately halt execution of that // test var errFailNow = fmt.Errorf("FailNow called") type dogTestingT struct { - t *testing.T - failed bool + t *testing.T + failed bool + failMessages []string } // check interface: @@ -36,7 +53,8 @@ func (dt *dogTestingT) Logf(format string, args ...interface{}) { func (dt *dogTestingT) Errorf(format string, args ...interface{}) { dt.Logf(format, args...) - dt.failed = true + dt.failMessages = append(dt.failMessages, fmt.Sprintf(format, args...)) + dt.Fail() } func (dt *dogTestingT) Fail() { @@ -44,15 +62,23 @@ func (dt *dogTestingT) Fail() { } func (dt *dogTestingT) FailNow() { + dt.Fail() panic(errFailNow) } -func (dt *dogTestingT) check() error { - if dt.failed { - return fmt.Errorf("one or more checks failed") +// isFailed will return an error representing the calls to Fail made during this test +func (dt *dogTestingT) isFailed() error { + if !dt.failed { + return nil + } + switch len(dt.failMessages) { + case 0: + return fmt.Errorf("fail called on TestingT") + case 1: + return fmt.Errorf(dt.failMessages[0]) + default: + return fmt.Errorf("checks failed:\n* %s", strings.Join(dt.failMessages, "\n* ")) } - - return nil } type testingTCtxVal struct{} @@ -60,6 +86,7 @@ type testingTCtxVal struct{} func setContextDogTester(ctx context.Context, dt *dogTestingT) context.Context { return context.WithValue(ctx, testingTCtxVal{}, dt) } + func getDogTestingT(ctx context.Context) *dogTestingT { dt, ok := ctx.Value(testingTCtxVal{}).(*dogTestingT) if !ok { @@ -67,10 +94,3 @@ func getDogTestingT(ctx context.Context) *dogTestingT { } return dt } - -// GetTestingT returns a TestingT compatible interface from the current test context. It will return -// nil if called outside the context of a test. This can be used with (for example) testify's assert -// and require packages. -func GetTestingT(ctx context.Context) TestingT { - return getDogTestingT(ctx) -} From 7a8acddc47b4551d6b2e481e8e78363a092bb1fc Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Mon, 22 Apr 2024 19:37:51 +0100 Subject: [PATCH 3/9] Provide as much of testing.T as possible + tidy up --- suite.go | 36 ++++---------- testingt.go | 140 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/suite.go b/suite.go index 87d9619e..fdfcb197 100644 --- a/suite.go +++ b/suite.go @@ -87,10 +87,10 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena if e := recover(); e != nil { pe, isErr := e.(error) switch { - case isErr && errors.Is(pe, errFailNow): - // FailNow called on dogTestingT, recover the error so we set the step as a 'normal' - // fail instead of normal panic handling - err = e.(error) + case isErr && errors.Is(pe, errStopNow): + // FailNow or SkipNow called on dogTestingT, so clear the error to let the normal + // below getTestingT(ctx).isFailed() call handle the reasons. + err = nil case err != nil: err = &traceError{ msg: fmt.Sprintf("%s: %v", err.Error(), e), @@ -108,7 +108,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena // Check for any calls to Fail on dogT if err == nil { - err = getDogTestingT(ctx).isFailed() + err = getTestingT(ctx).isFailed() } switch { @@ -520,8 +520,10 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { s.fmt.Pickle(pickle) - dt := &dogTestingT{} - ctx = setContextDogTester(ctx, dt) + dt := &testingT{ + name: pickle.Name, + } + ctx = setContextTestingT(ctx, dt) // scenario if s.testingT != nil { // Running scenario as a subtest. @@ -541,23 +543,3 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { return err } - -// Logf will log test output. If called in the context of a test and testing.T has been registered, -// this will log using the step's testing.T, else it will simply log to stdout. -func Logf(ctx context.Context, format string, args ...interface{}) { - if t := getDogTestingT(ctx); t != nil { - t.Logf(format, args...) - return - } - fmt.Printf(format+"\n", args...) -} - -// Log will log test output. If called in the context of a test and testing.T has been registered, -// this will log using the step's testing.T, else it will simply log to stdout. -func Log(ctx context.Context, args ...interface{}) { - if t := getDogTestingT(ctx); t != nil { - t.Log(args...) - return - } - fmt.Println(args...) -} diff --git a/testingt.go b/testingt.go index ef95ccb3..7da2d36e 100644 --- a/testingt.go +++ b/testingt.go @@ -7,67 +7,165 @@ import ( "testing" ) +// TestingT is a subset of the public methods implemented by go's testing.T. It allows assertion +// libraries to be used with godog, provided they depend only on this subset of methods. type TestingT interface { + // Name returns the name of the current pickle under test + Name() string + // Log will log to the current testing.T log if set, otherwise it will log to stdout Log(args ...interface{}) + // Logf will log a formatted string to the current testing.T log if set, otherwise it will log + // to stdout Logf(format string, args ...interface{}) + // Error fails the current test and logs the provided arguments. Equivalent to calling Log then + // Fail. + Error(args ...interface{}) + // Errorf fails the current test and logs the formatted message. Equivalent to calling Logf then + // Fail. Errorf(format string, args ...interface{}) + // Fail marks the current test as failed, but does not halt execution of the step. Fail() + // FailNow marks the current test as failed and halts execution of the step. FailNow() + // Fatal logs the provided arguments, marks the test as failed and halts execution of the step. + Fatal(args ...interface{}) + // Fatal logs the formatted message, marks the test as failed and halts execution of the step. + Fatalf(format string, args ...interface{}) + // Skip logs the provided arguments and marks the test as skipped but does not halt execution + // of the step. + Skip(args ...interface{}) + // Skipf logs the formatted message and marks the test as skipped but does not halt execution + // of the step. + Skipf(format string, args ...interface{}) + // SkipNow marks the current test as skipped and halts execution of the step. + SkipNow() + // Skipped returns true if the test has been marked as skipped. + Skipped() bool +} + +// Logf will log test output. If called in the context of a test and testing.T has been registered, +// this will log using the step's testing.T, else it will simply log to stdout. +func Logf(ctx context.Context, format string, args ...interface{}) { + if t := getTestingT(ctx); t != nil { + t.Logf(format, args...) + return + } + fallbackLogf(format, args...) +} + +// Log will log test output. If called in the context of a test and testing.T has been registered, +// this will log using the step's testing.T, else it will simply log to stdout. +func Log(ctx context.Context, args ...interface{}) { + if t := getTestingT(ctx); t != nil { + t.Log(args...) + return + } + fallbackLog(args...) } // GetTestingT returns a TestingT compatible interface from the current test context. It will return // nil if called outside the context of a test. This can be used with (for example) testify's assert // and require packages. func GetTestingT(ctx context.Context) TestingT { - return getDogTestingT(ctx) + return getTestingT(ctx) } -// errFailNow should be returned inside a panic within the test to immediately halt execution of that +// errStopNow should be returned inside a panic within the test to immediately halt execution of that // test -var errFailNow = fmt.Errorf("FailNow called") +var errStopNow = fmt.Errorf("FailNow or SkipNow called") -type dogTestingT struct { +type testingT struct { + name string t *testing.T failed bool + skipped bool failMessages []string + logMessages []string } // check interface: -var _ TestingT = &dogTestingT{} +var _ TestingT = &testingT{} -func (dt *dogTestingT) Log(args ...interface{}) { +func (dt *testingT) Name() string { + if dt.t != nil { + return dt.t.Name() + } + return dt.name +} + +func (dt *testingT) Log(args ...interface{}) { + dt.logMessages = append(dt.logMessages, fmt.Sprint(args...)) if dt.t != nil { dt.t.Log(args...) return } - fmt.Println(args...) + fallbackLog(args...) } -func (dt *dogTestingT) Logf(format string, args ...interface{}) { +func (dt *testingT) Logf(format string, args ...interface{}) { + dt.logMessages = append(dt.logMessages, fmt.Sprintf(format, args...)) if dt.t != nil { dt.t.Logf(format, args...) return } - fmt.Printf(format+"\n", args...) + fallbackLogf(format, args...) +} + +func (dt *testingT) Error(args ...interface{}) { + dt.Log(args...) + dt.failMessages = append(dt.failMessages, fmt.Sprintln(args...)) + dt.Fail() } -func (dt *dogTestingT) Errorf(format string, args ...interface{}) { +func (dt *testingT) Errorf(format string, args ...interface{}) { dt.Logf(format, args...) dt.failMessages = append(dt.failMessages, fmt.Sprintf(format, args...)) dt.Fail() } -func (dt *dogTestingT) Fail() { +func (dt *testingT) Fail() { dt.failed = true } -func (dt *dogTestingT) FailNow() { +func (dt *testingT) FailNow() { dt.Fail() - panic(errFailNow) + panic(errStopNow) +} + +func (dt *testingT) Fatal(args ...interface{}) { + dt.Log(args...) + dt.FailNow() +} + +func (dt *testingT) Fatalf(format string, args ...interface{}) { + dt.Logf(format, args...) + dt.FailNow() +} + +func (dt *testingT) Skip(args ...interface{}) { + dt.Log(args...) + dt.skipped = true +} + +func (dt *testingT) Skipf(format string, args ...interface{}) { + dt.Logf(format, args...) + dt.skipped = true +} + +func (dt *testingT) SkipNow() { + dt.skipped = true + panic(errStopNow) +} + +func (dt *testingT) Skipped() bool { + return dt.skipped } // isFailed will return an error representing the calls to Fail made during this test -func (dt *dogTestingT) isFailed() error { +func (dt *testingT) isFailed() error { + if dt.skipped { + return ErrSkip + } if !dt.failed { return nil } @@ -83,14 +181,22 @@ func (dt *dogTestingT) isFailed() error { type testingTCtxVal struct{} -func setContextDogTester(ctx context.Context, dt *dogTestingT) context.Context { +func setContextTestingT(ctx context.Context, dt *testingT) context.Context { return context.WithValue(ctx, testingTCtxVal{}, dt) } -func getDogTestingT(ctx context.Context) *dogTestingT { - dt, ok := ctx.Value(testingTCtxVal{}).(*dogTestingT) +func getTestingT(ctx context.Context) *testingT { + dt, ok := ctx.Value(testingTCtxVal{}).(*testingT) if !ok { return nil } return dt } + +// fallbackLog is used to log when no testing.T is available +var fallbackLog = fmt.Println + +// fallbackLogf is used to log a formatted string when no testing.T is available +var fallbackLogf = func(message string, args ...interface{}) { + fmt.Printf(message+"\n", args...) +} From c51d503ee2eb5c0d6c316f5b2597273d3b2b53bf Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Mon, 22 Apr 2024 22:46:29 +0100 Subject: [PATCH 4/9] Add initial tests for testingT support --- features/formatter/events.feature | 4 +- features/formatter/pretty.feature | 7 +- features/lang.feature | 3 +- features/load.feature | 3 +- features/testingt.feature | 144 ++++++++++++++++++++++++++++++ run_test.go | 12 +-- suite_context_test.go | 98 ++++++++++++++++++++ testingt.go | 6 +- 8 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 features/testingt.feature diff --git a/features/formatter/events.feature b/features/formatter/events.feature index b33554cc..6ccc257a 100644 --- a/features/formatter/events.feature +++ b/features/formatter/events.feature @@ -13,7 +13,7 @@ Feature: event stream formatter """ Scenario: should process simple scenario - Given a feature path "features/load.feature:26" + Given a feature path "features/load.feature:27" When I run feature suite with formatter "events" Then the following events should be fired: """ @@ -34,7 +34,7 @@ Feature: event stream formatter """ Scenario: should process outline scenario - Given a feature path "features/load.feature:34" + Given a feature path "features/load.feature:35" When I run feature suite with formatter "events" Then the following events should be fired: """ diff --git a/features/formatter/pretty.feature b/features/formatter/pretty.feature index 197c1f50..81cf7b1d 100644 --- a/features/formatter/pretty.feature +++ b/features/formatter/pretty.feature @@ -350,7 +350,7 @@ Feature: pretty formatter Scenario: Should scenarios identified with path:line and preserve the order. Given a feature path "features/load.feature:6" And a feature path "features/multistep.feature:6" - And a feature path "features/load.feature:26" + And a feature path "features/load.feature:27" And a feature path "features/multistep.feature:23" When I run feature suite with formatter "pretty" Then the rendered output will be as follows: @@ -363,7 +363,7 @@ Feature: pretty formatter Scenario: load features within path # features/load.feature:6 Given a feature path "features" # suite_context_test.go:0 -> *godogFeaturesScenario When I parse features # suite_context_test.go:0 -> *godogFeaturesScenario - Then I should have 13 feature files: # suite_context_test.go:0 -> *godogFeaturesScenario + Then I should have 14 feature files: # suite_context_test.go:0 -> *godogFeaturesScenario \"\"\" features/background.feature features/events.feature @@ -378,6 +378,7 @@ Feature: pretty formatter features/run.feature features/snippets.feature features/tags.feature + features/testingt.feature \"\"\" Feature: run features with nested steps @@ -407,7 +408,7 @@ Feature: pretty formatter As a test suite I need to be able to load features - Scenario: load a specific feature file # features/load.feature:26 + Scenario: load a specific feature file # features/load.feature:27 Given a feature path "features/load.feature" # suite_context_test.go:0 -> *godogFeaturesScenario When I parse features # suite_context_test.go:0 -> *godogFeaturesScenario Then I should have 1 feature file: # suite_context_test.go:0 -> *godogFeaturesScenario diff --git a/features/lang.feature b/features/lang.feature index 0b41014c..c5f87058 100644 --- a/features/lang.feature +++ b/features/lang.feature @@ -8,7 +8,7 @@ Savybė: užkrauti savybes Scenarijus: savybių užkrovimas iš aplanko Duota savybių aplankas "features" Kai aš išskaitau savybes - Tada aš turėčiau turėti 13 savybių failus: + Tada aš turėčiau turėti 14 savybių failus: """ features/background.feature features/events.feature @@ -23,4 +23,5 @@ Savybė: užkrauti savybes features/run.feature features/snippets.feature features/tags.feature + features/testingt.feature """ diff --git a/features/load.feature b/features/load.feature index 3bc2865f..0b9689f9 100644 --- a/features/load.feature +++ b/features/load.feature @@ -6,7 +6,7 @@ Feature: load features Scenario: load features within path Given a feature path "features" When I parse features - Then I should have 13 feature files: + Then I should have 14 feature files: """ features/background.feature features/events.feature @@ -21,6 +21,7 @@ Feature: load features features/run.feature features/snippets.feature features/tags.feature + features/testingt.feature """ Scenario: load a specific feature file diff --git a/features/testingt.feature b/features/testingt.feature new file mode 100644 index 00000000..ef845f6c --- /dev/null +++ b/features/testingt.feature @@ -0,0 +1,144 @@ +Feature: providing testingT compatibility + In order to test application behavior using standard go assertion techniques + As a test suite + I need to be able to provide a testing.T compatible interface + + Scenario: should fail test if FailNow called on testing T + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When I fail the test by calling FailNow on testing T + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following step should be failed: + """ + I fail the test by calling FailNow on testing T + """ + + Scenario: should pass test when testify assertions pass + Given a feature "testify.feature" file: + """ + Feature: passed feature + + Scenario: pass a scenario + Given passing step + When I call testify's assert.Equal with expected "exp" and actual "exp" + When I call testify's require.Equal with expected "exp" and actual "exp" + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + I call testify's assert.Equal with expected "exp" and actual "exp" + I call testify's require.Equal with expected "exp" and actual "exp" + """ + + Scenario: should fail test when testify assertions do not pass + Given a feature "testify.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When I call testify's assert.Equal with expected "exp" and actual "not" + And I call testify's assert.Equal with expected "exp2" and actual "not" + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following steps should be failed: + """ + I call testify's assert.Equal with expected "exp" and actual "not" + """ + And the following steps should be skipped: + """ + I call testify's assert.Equal with expected "exp2" and actual "not" + """ + + Scenario: should fail test when multiple testify assertions are used in a step + Given a feature "testify.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When I call testify's assert.Equal 3 times + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following steps should be failed: + """ + I call testify's assert.Equal 3 times + """ + + Scenario: should pass test when multiple testify assertions are used successfully in a step + Given a feature "testify.feature" file: + """ + Feature: passed feature + + Scenario: pass a scenario + Given passing step + When I call testify's assert.Equal 3 times with match + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + I call testify's assert.Equal 3 times with match + """ + + Scenario: should skip test when skip is called on the testing.T + Given a feature "testify.feature" file: + """ + Feature: skipped feature + + Scenario: skip a scenario + Given passing step + When I skip the test by calling Skip on testing T + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + """ + And the following steps should be skipped: + """ + I skip the test by calling Skip on testing T + """ + + Scenario: should log to testing.T + Given a feature "logging.feature" file: + """ + Feature: logged feature + + Scenario: logged scenario + Given passing step + When I call Logf on testing T with message "format this %s" and argument "formatparam1" + And I call Log on testing T with message "log this message" + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + I call Logf on testing T with message "format this %s" and argument "formatparam1" + I call Log on testing T with message "log this message" + """ diff --git a/run_test.go b/run_test.go index 94f71744..5d7a11b3 100644 --- a/run_test.go +++ b/run_test.go @@ -525,11 +525,11 @@ func Test_AllFeaturesRun(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -...... 356 +....................................... 389 -94 scenarios (94 passed) -356 steps (356 passed) +101 scenarios (101 passed) +389 steps (389 passed) 0s ` @@ -553,11 +553,11 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -...... 356 +....................................... 389 -94 scenarios (94 passed) -356 steps (356 passed) +101 scenarios (101 passed) +389 steps (389 passed) 0s ` diff --git a/suite_context_test.go b/suite_context_test.go index 6df223b7..b625de65 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -161,6 +161,20 @@ func InitializeScenario(ctx *ScenarioContext) { } }) + // introduced to test testingT + ctx.Step(`^I fail the test by calling FailNow on testing T$`, tc.iCallTFailNow) + ctx.Step(`^I fail the test by calling Fail on testing T$`, tc.iCallTFail) + ctx.Step(`^I fail the test by calling Error on testing T with message "([^"]*)"$`, tc.iCallTError) + ctx.Step(`^I fail the test by calling Errorf on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.iCallTErrorf) + ctx.Step(`^I skip the test by calling SkipNow on testing T$`, tc.iCallTSkipNow) + ctx.Step(`^I skip the test by calling Skip on testing T$`, tc.iCallTSkip) + ctx.Step(`^I call Logf on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.iCallTLogf) + ctx.Step(`^I call Log on testing T with message "([^"]*)"$`, tc.iCallTLog) + ctx.Step(`^I call testify's assert.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.iCallTestifyAssertEqual) + ctx.Step(`^I call testify's require.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.iCallTestifyRequireEqual) + ctx.Step(`^I call testify's assert.Equal ([0-9]+) times$`, tc.iCallTestifyAssertEqualMultipleTimes) + ctx.Step(`^I call testify's assert.Equal ([0-9]+) times with match$`, tc.iCallTestifyAssertEqualMultipleTimesWithMatch) + ctx.StepContext().Before(tc.inject) } @@ -385,6 +399,90 @@ func (tc *godogFeaturesScenario) iShouldSeeTheContextInTheNextStep(ctx context.C return nil } +func (tc *godogFeaturesScenario) iCallTFailNow(ctx context.Context) error { + t := GetTestingT(ctx) + t.FailNow() + return nil +} + +func (tc *godogFeaturesScenario) iCallTFail(ctx context.Context) error { + t := GetTestingT(ctx) + t.Fail() + return nil +} + +func (tc *godogFeaturesScenario) iCallTSkipNow(ctx context.Context) error { + t := GetTestingT(ctx) + t.SkipNow() + return nil +} + +func (tc *godogFeaturesScenario) iCallTSkip(ctx context.Context) error { + t := GetTestingT(ctx) + t.Skip() + return nil +} + +func (tc *godogFeaturesScenario) iCallTError(ctx context.Context, message string) error { + t := GetTestingT(ctx) + t.Error(message) + return nil +} + +func (tc *godogFeaturesScenario) iCallTErrorf(ctx context.Context, message string, arg string) error { + t := GetTestingT(ctx) + t.Errorf(message, arg) + return nil +} + +func (tc *godogFeaturesScenario) iCallTestifyAssertEqual(ctx context.Context, a string, b string) error { + t := GetTestingT(ctx) + assert.Equal(t, a, b) + return nil +} + +func (tc *godogFeaturesScenario) iCallTestifyAssertEqualMultipleTimes(ctx context.Context, times string) error { + t := GetTestingT(ctx) + timesInt, err := strconv.Atoi(times) + if err != nil { + return fmt.Errorf("test step has invalid times value %s: %w", times, err) + } + for i := 0; i < timesInt; i++ { + assert.Equal(t, "exp", fmt.Sprintf("notexp%v", i)) + } + return nil +} + +func (tc *godogFeaturesScenario) iCallTestifyAssertEqualMultipleTimesWithMatch(ctx context.Context, times string) error { + t := GetTestingT(ctx) + timesInt, err := strconv.Atoi(times) + if err != nil { + return fmt.Errorf("test step has invalid times value %s: %w", times, err) + } + for i := 0; i < timesInt; i++ { + assert.Equal(t, fmt.Sprintf("exp%v", i), fmt.Sprintf("exp%v", i)) + } + return nil +} + +func (tc *godogFeaturesScenario) iCallTestifyRequireEqual(ctx context.Context, a string, b string) error { + t := GetTestingT(ctx) + require.Equal(t, a, b) + return nil +} + +func (tc *godogFeaturesScenario) iCallTLog(ctx context.Context, message string) error { + t := GetTestingT(ctx) + t.Log(message) + return nil +} + +func (tc *godogFeaturesScenario) iCallTLogf(ctx context.Context, message string, arg string) error { + t := GetTestingT(ctx) + t.Logf(message, arg) + return nil +} + func (tc *godogFeaturesScenario) followingStepsShouldHave(status string, steps *DocString) error { var expected = strings.Split(steps.Content, "\n") var actual, unmatched, matched []string diff --git a/testingt.go b/testingt.go index 7da2d36e..09f107c7 100644 --- a/testingt.go +++ b/testingt.go @@ -193,10 +193,12 @@ func getTestingT(ctx context.Context) *testingT { return dt } -// fallbackLog is used to log when no testing.T is available +// fallbackLog is used to log when no testing.T is available. Set as a variable so this can be +// disabled / re-routed in future if needed. var fallbackLog = fmt.Println -// fallbackLogf is used to log a formatted string when no testing.T is available +// fallbackLogf is used to log a formatted string when no testing.T is available. Set as a variable +// so this can be disabled / re-routed in future if needed. var fallbackLogf = func(message string, args ...interface{}) { fmt.Printf(message+"\n", args...) } From ce4bc3f6727ca5c3720247323c5339c5f941d80c Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Tue, 23 Apr 2024 16:55:19 +0100 Subject: [PATCH 5/9] Check compatibility with testing.T and friends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Piotr Bocheński --- testingt.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testingt.go b/testingt.go index 09f107c7..30f9af94 100644 --- a/testingt.go +++ b/testingt.go @@ -7,6 +7,12 @@ import ( "testing" ) +var ( + _ TestingT = (*testing.B)(nil) + _ TestingT = (*testing.F)(nil) + _ TestingT = (*testing.T)(nil) +) + // TestingT is a subset of the public methods implemented by go's testing.T. It allows assertion // libraries to be used with godog, provided they depend only on this subset of methods. type TestingT interface { From bd4c69dc372d5b4c54d2952eb09f49571350179f Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Tue, 23 Apr 2024 17:41:36 +0100 Subject: [PATCH 6/9] Update assert-godogs example to show new usage. Rename 'GetTestingT(ctx)' to 'T(ctx)' --- _examples/assert-godogs/godogs_test.go | 59 ++++---------------------- testingt.go | 8 ++-- 2 files changed, 13 insertions(+), 54 deletions(-) diff --git a/_examples/assert-godogs/godogs_test.go b/_examples/assert-godogs/godogs_test.go index 180339c8..6aec222a 100644 --- a/_examples/assert-godogs/godogs_test.go +++ b/_examples/assert-godogs/godogs_test.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "os" "testing" @@ -36,31 +35,22 @@ func thereAreGodogs(available int) error { return nil } -func iEat(num int) error { - err := assertExpectedAndActual( - assert.GreaterOrEqual, Godogs, num, - "You cannot eat %d godogs, there are %d available", num, Godogs, - ) - if err != nil { - return err +func iEat(ctx context.Context, num int) error { + if !assert.GreaterOrEqual(godog.T(ctx), Godogs, num, "You cannot eat %d godogs, there are %d available", num, Godogs) { + return nil } - Godogs -= num return nil } -func thereShouldBeRemaining(remaining int) error { - return assertExpectedAndActual( - assert.Equal, Godogs, remaining, - "Expected %d godogs to be remaining, but there is %d", remaining, Godogs, - ) +func thereShouldBeRemaining(ctx context.Context, remaining int) error { + assert.Equal(godog.T(ctx), Godogs, remaining, "Expected %d godogs to be remaining, but there is %d", remaining, Godogs) + return nil } -func thereShouldBeNoneRemaining() error { - return assertActual( - assert.Empty, Godogs, - "Expected none godogs to be remaining, but there is %d", Godogs, - ) +func thereShouldBeNoneRemaining(ctx context.Context) error { + assert.Empty(godog.T(ctx), Godogs, "Expected none godogs to be remaining, but there is %d", Godogs) + return nil } func InitializeScenario(ctx *godog.ScenarioContext) { @@ -74,34 +64,3 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) ctx.Step(`^there should be none remaining$`, thereShouldBeNoneRemaining) } - -// assertExpectedAndActual is a helper function to allow the step function to call -// assertion functions where you want to compare an expected and an actual value. -func assertExpectedAndActual(a expectedAndActualAssertion, expected, actual interface{}, msgAndArgs ...interface{}) error { - var t asserter - a(&t, expected, actual, msgAndArgs...) - return t.err -} - -type expectedAndActualAssertion func(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool - -// assertActual is a helper function to allow the step function to call -// assertion functions where you want to compare an actual value to a -// predined state like nil, empty or true/false. -func assertActual(a actualAssertion, actual interface{}, msgAndArgs ...interface{}) error { - var t asserter - a(&t, actual, msgAndArgs...) - return t.err -} - -type actualAssertion func(t assert.TestingT, actual interface{}, msgAndArgs ...interface{}) bool - -// asserter is used to be able to retrieve the error reported by the called assertion -type asserter struct { - err error -} - -// Errorf is used by the called assertion to report an error -func (a *asserter) Errorf(format string, args ...interface{}) { - a.err = fmt.Errorf(format, args...) -} diff --git a/testingt.go b/testingt.go index 30f9af94..33d0475d 100644 --- a/testingt.go +++ b/testingt.go @@ -69,10 +69,10 @@ func Log(ctx context.Context, args ...interface{}) { fallbackLog(args...) } -// GetTestingT returns a TestingT compatible interface from the current test context. It will return -// nil if called outside the context of a test. This can be used with (for example) testify's assert -// and require packages. -func GetTestingT(ctx context.Context) TestingT { +// T returns a TestingT compatible interface from the current test context. It will return nil if +// called outside the context of a test. This can be used with (for example) testify's assert and +// require packages. +func T(ctx context.Context) TestingT { return getTestingT(ctx) } From 145f942b3699b8ca289fcb8ba46c15271b9b5053 Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Wed, 24 Apr 2024 11:32:37 +0100 Subject: [PATCH 7/9] Update changelog and readme with new usage --- CHANGELOG.md | 3 +++ README.md | 31 ++++++------------------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d340d02c..e819dd11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt ## Unreleased +### Added +- Provide testing.T-compatible interface on test context, allowing usage of assertion libraries such as testify's assert/require - ([571](https://github.com/cucumber/godog/pull/571) - [mrsheepuk](https://github.com/mrsheepuk)) + ## [v0.14.0] ### Added - Improve ErrSkip handling, add test for Summary and operations order ([584](https://github.com/cucumber/godog/pull/584) - [vearutop](https://github.com/vearutop)) diff --git a/README.md b/README.md index ba78d34e..1bbf2087 100644 --- a/README.md +++ b/README.md @@ -495,31 +495,12 @@ If you want to filter scenarios by tags, you can use the `-t=` or `- A more extensive example can be [found here](/_examples/assert-godogs). ```go -func thereShouldBeRemaining(remaining int) error { - return assertExpectedAndActual( - assert.Equal, Godogs, remaining, - "Expected %d godogs to be remaining, but there is %d", remaining, Godogs, - ) -} - -// assertExpectedAndActual is a helper function to allow the step function to call -// assertion functions where you want to compare an expected and an actual value. -func assertExpectedAndActual(a expectedAndActualAssertion, expected, actual interface{}, msgAndArgs ...interface{}) error { - var t asserter - a(&t, expected, actual, msgAndArgs...) - return t.err -} - -type expectedAndActualAssertion func(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool - -// asserter is used to be able to retrieve the error reported by the called assertion -type asserter struct { - err error -} - -// Errorf is used by the called assertion to report an error -func (a *asserter) Errorf(format string, args ...interface{}) { - a.err = fmt.Errorf(format, args...) +func thereShouldBeRemaining(ctx context.Context, remaining int) error { + assert.Equal( + godog.T(ctx), Godogs, remaining, + "Expected %d godogs to be remaining, but there is %d", remaining, Godogs, + ) + return nil } ``` From dbb8426ed99ed058a4b53f39024ab554b0b823e0 Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Wed, 24 Apr 2024 11:32:50 +0100 Subject: [PATCH 8/9] Improve test coverage --- features/testingt.feature | 120 +++++++++++++++++++++++---------- run_test.go | 14 ++-- suite_context_test.go | 136 ++++++++++++++++++++------------------ testingt.go | 32 +++++---- 4 files changed, 186 insertions(+), 116 deletions(-) diff --git a/features/testingt.feature b/features/testingt.feature index ef845f6c..1125d819 100644 --- a/features/testingt.feature +++ b/features/testingt.feature @@ -3,14 +3,14 @@ Feature: providing testingT compatibility As a test suite I need to be able to provide a testing.T compatible interface - Scenario: should fail test if FailNow called on testing T + Scenario Outline: should fail test with no message if called on testing T Given a feature "failed.feature" file: """ Feature: failed feature Scenario: fail a scenario Given passing step - When I fail the test by calling FailNow on testing T + When my step fails the test by calling on testing T """ When I run feature suite Then the suite should have failed @@ -20,8 +20,61 @@ Feature: providing testingT compatibility """ And the following step should be failed: """ - I fail the test by calling FailNow on testing T + my step fails the test by calling on testing T """ + Examples: + | op | + | Fail | + | FailNow | + + Scenario Outline: should fail test with message if called on T + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step fails the test by calling on testing T with message "an unformatted message" + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following step should be failed: + """ + my step fails the test by calling on testing T with message "an unformatted message" + """ + Examples: + | op | + | Error | + | Fatal | + + + Scenario Outline: should fail test with formatted message if called on T + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: fail a scenario + Given passing step + When my step fails the test by calling on testing T with message "a formatted message %s" and argument "arg1" + """ + When I run feature suite + Then the suite should have failed + And the following steps should be passed: + """ + passing step + """ + And the following step should be failed: + """ + my step fails the test by calling on testing T with message "a formatted message %s" and argument "arg1" + """ + Examples: + | op | + | Errorf | + | Fatalf | Scenario: should pass test when testify assertions pass Given a feature "testify.feature" file: @@ -30,16 +83,16 @@ Feature: providing testingT compatibility Scenario: pass a scenario Given passing step - When I call testify's assert.Equal with expected "exp" and actual "exp" - When I call testify's require.Equal with expected "exp" and actual "exp" + When my step calls testify's assert.Equal with expected "exp" and actual "exp" + When my step calls testify's require.Equal with expected "exp" and actual "exp" """ When I run feature suite Then the suite should have passed And the following steps should be passed: """ passing step - I call testify's assert.Equal with expected "exp" and actual "exp" - I call testify's require.Equal with expected "exp" and actual "exp" + my step calls testify's assert.Equal with expected "exp" and actual "exp" + my step calls testify's require.Equal with expected "exp" and actual "exp" """ Scenario: should fail test when testify assertions do not pass @@ -49,8 +102,8 @@ Feature: providing testingT compatibility Scenario: fail a scenario Given passing step - When I call testify's assert.Equal with expected "exp" and actual "not" - And I call testify's assert.Equal with expected "exp2" and actual "not" + When my step calls testify's assert.Equal with expected "exp" and actual "not" + And my step calls testify's assert.Equal with expected "exp2" and actual "not" """ When I run feature suite Then the suite should have failed @@ -60,11 +113,11 @@ Feature: providing testingT compatibility """ And the following steps should be failed: """ - I call testify's assert.Equal with expected "exp" and actual "not" + my step calls testify's assert.Equal with expected "exp" and actual "not" """ And the following steps should be skipped: """ - I call testify's assert.Equal with expected "exp2" and actual "not" + my step calls testify's assert.Equal with expected "exp2" and actual "not" """ Scenario: should fail test when multiple testify assertions are used in a step @@ -74,7 +127,7 @@ Feature: providing testingT compatibility Scenario: fail a scenario Given passing step - When I call testify's assert.Equal 3 times + When my step calls testify's assert.Equal 3 times """ When I run feature suite Then the suite should have failed @@ -84,7 +137,7 @@ Feature: providing testingT compatibility """ And the following steps should be failed: """ - I call testify's assert.Equal 3 times + my step calls testify's assert.Equal 3 times """ Scenario: should pass test when multiple testify assertions are used successfully in a step @@ -94,24 +147,24 @@ Feature: providing testingT compatibility Scenario: pass a scenario Given passing step - When I call testify's assert.Equal 3 times with match + When my step calls testify's assert.Equal 3 times with match """ When I run feature suite Then the suite should have passed And the following steps should be passed: """ passing step - I call testify's assert.Equal 3 times with match + my step calls testify's assert.Equal 3 times with match """ - Scenario: should skip test when skip is called on the testing.T + Scenario Outline: should skip test when is called on the testing.T Given a feature "testify.feature" file: """ Feature: skipped feature Scenario: skip a scenario Given passing step - When I skip the test by calling Skip on testing T + When my step skips the test by calling on testing T """ When I run feature suite Then the suite should have passed @@ -121,24 +174,21 @@ Feature: providing testingT compatibility """ And the following steps should be skipped: """ - I skip the test by calling Skip on testing T + my step skips the test by calling on testing T """ + Examples: + | op | + | Skip | + | SkipNow | - Scenario: should log to testing.T - Given a feature "logging.feature" file: - """ - Feature: logged feature + Scenario: should log when Logf/Log called on testing.T + When my step calls Logf on testing T with message "format this %s" and argument "formatparam1" + And my step calls Log on testing T with message "log this message" + Then the logged messages should include "format this formatparam1" + And the logged messages should include "log this message" - Scenario: logged scenario - Given passing step - When I call Logf on testing T with message "format this %s" and argument "formatparam1" - And I call Log on testing T with message "log this message" - """ - When I run feature suite - Then the suite should have passed - And the following steps should be passed: - """ - passing step - I call Logf on testing T with message "format this %s" and argument "formatparam1" - I call Log on testing T with message "log this message" - """ + Scenario: should log when godog.Logf/Log called + When my step calls godog.Logf with message "format this %s" and argument "formatparam1" + And my step calls godog.Log with message "log this message" + Then the logged messages should include "format this formatparam1" + And the logged messages should include "log this message" diff --git a/run_test.go b/run_test.go index 5d7a11b3..6a070d68 100644 --- a/run_test.go +++ b/run_test.go @@ -525,11 +525,12 @@ func Test_AllFeaturesRun(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -....................................... 389 +...................................................................... 420 +... 423 -101 scenarios (101 passed) -389 steps (389 passed) +108 scenarios (108 passed) +423 steps (423 passed) 0s ` @@ -553,11 +554,12 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) { ...................................................................... 210 ...................................................................... 280 ...................................................................... 350 -....................................... 389 +...................................................................... 420 +... 423 -101 scenarios (101 passed) -389 steps (389 passed) +108 scenarios (108 passed) +423 steps (423 passed) 0s ` diff --git a/suite_context_test.go b/suite_context_test.go index b625de65..5217f674 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -162,18 +162,17 @@ func InitializeScenario(ctx *ScenarioContext) { }) // introduced to test testingT - ctx.Step(`^I fail the test by calling FailNow on testing T$`, tc.iCallTFailNow) - ctx.Step(`^I fail the test by calling Fail on testing T$`, tc.iCallTFail) - ctx.Step(`^I fail the test by calling Error on testing T with message "([^"]*)"$`, tc.iCallTError) - ctx.Step(`^I fail the test by calling Errorf on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.iCallTErrorf) - ctx.Step(`^I skip the test by calling SkipNow on testing T$`, tc.iCallTSkipNow) - ctx.Step(`^I skip the test by calling Skip on testing T$`, tc.iCallTSkip) - ctx.Step(`^I call Logf on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.iCallTLogf) - ctx.Step(`^I call Log on testing T with message "([^"]*)"$`, tc.iCallTLog) - ctx.Step(`^I call testify's assert.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.iCallTestifyAssertEqual) - ctx.Step(`^I call testify's require.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.iCallTestifyRequireEqual) - ctx.Step(`^I call testify's assert.Equal ([0-9]+) times$`, tc.iCallTestifyAssertEqualMultipleTimes) - ctx.Step(`^I call testify's assert.Equal ([0-9]+) times with match$`, tc.iCallTestifyAssertEqualMultipleTimesWithMatch) + ctx.Step(`^my step (?:fails|skips) the test by calling (FailNow|Fail|SkipNow|Skip) on testing T$`, tc.myStepCallsTFailErrorSkip) + ctx.Step(`^my step fails the test by calling (Fatal|Error) on testing T with message "([^"]*)"$`, tc.myStepCallsTErrorFatal) + ctx.Step(`^my step fails the test by calling (Fatalf|Errorf) on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.myStepCallsTErrorfFatalf) + ctx.Step(`^my step calls Log on testing T with message "([^"]*)"$`, tc.myStepCallsTLog) + ctx.Step(`^my step calls Logf on testing T with message "([^"]*)" and argument "([^"]*)"$`, tc.myStepCallsTLogf) + ctx.Step(`^my step calls testify's assert.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.myStepCallsTestifyAssertEqual) + ctx.Step(`^my step calls testify's require.Equal with expected "([^"]*)" and actual "([^"]*)"$`, tc.myStepCallsTestifyRequireEqual) + ctx.Step(`^my step calls testify's assert.Equal ([0-9]+) times(| with match)$`, tc.myStepCallsTestifyAssertEqualMultipleTimes) + ctx.Step(`^my step calls godog.Log with message "([^"]*)"$`, tc.myStepCallsDogLog) + ctx.Step(`^my step calls godog.Logf with message "([^"]*)" and argument "([^"]*)"$`, tc.myStepCallsDogLogf) + ctx.Step(`^the logged messages should include "([^"]*)"$`, tc.theLoggedMessagesShouldInclude) ctx.StepContext().Before(tc.inject) } @@ -399,90 +398,101 @@ func (tc *godogFeaturesScenario) iShouldSeeTheContextInTheNextStep(ctx context.C return nil } -func (tc *godogFeaturesScenario) iCallTFailNow(ctx context.Context) error { - t := GetTestingT(ctx) - t.FailNow() - return nil -} - -func (tc *godogFeaturesScenario) iCallTFail(ctx context.Context) error { - t := GetTestingT(ctx) - t.Fail() - return nil -} - -func (tc *godogFeaturesScenario) iCallTSkipNow(ctx context.Context) error { - t := GetTestingT(ctx) - t.SkipNow() - return nil -} - -func (tc *godogFeaturesScenario) iCallTSkip(ctx context.Context) error { - t := GetTestingT(ctx) - t.Skip() +func (tc *godogFeaturesScenario) myStepCallsTFailErrorSkip(ctx context.Context, op string) error { + switch op { + case "FailNow": + T(ctx).FailNow() + case "Fail": + T(ctx).Fail() + case "SkipNow": + T(ctx).SkipNow() + case "Skip": + T(ctx).Skip() + default: + return fmt.Errorf("operation %s not supported by iCallTFailErrorSkip", op) + } return nil } -func (tc *godogFeaturesScenario) iCallTError(ctx context.Context, message string) error { - t := GetTestingT(ctx) - t.Error(message) +func (tc *godogFeaturesScenario) myStepCallsTErrorFatal(ctx context.Context, op string, message string) error { + switch op { + case "Error": + T(ctx).Error(message) + case "Fatal": + T(ctx).Fatal(message) + default: + return fmt.Errorf("operation %s not supported by iCallTErrorFatal", op) + } return nil } -func (tc *godogFeaturesScenario) iCallTErrorf(ctx context.Context, message string, arg string) error { - t := GetTestingT(ctx) - t.Errorf(message, arg) +func (tc *godogFeaturesScenario) myStepCallsTErrorfFatalf(ctx context.Context, op string, message string, arg string) error { + switch op { + case "Errorf": + T(ctx).Errorf(message, arg) + case "Fatalf": + T(ctx).Fatalf(message, arg) + default: + return fmt.Errorf("operation %s not supported by iCallTErrorfFatalf", op) + } return nil } -func (tc *godogFeaturesScenario) iCallTestifyAssertEqual(ctx context.Context, a string, b string) error { - t := GetTestingT(ctx) - assert.Equal(t, a, b) +func (tc *godogFeaturesScenario) myStepCallsTestifyAssertEqual(ctx context.Context, a string, b string) error { + assert.Equal(T(ctx), a, b) return nil } -func (tc *godogFeaturesScenario) iCallTestifyAssertEqualMultipleTimes(ctx context.Context, times string) error { - t := GetTestingT(ctx) +func (tc *godogFeaturesScenario) myStepCallsTestifyAssertEqualMultipleTimes(ctx context.Context, times string, withMatch string) error { timesInt, err := strconv.Atoi(times) if err != nil { return fmt.Errorf("test step has invalid times value %s: %w", times, err) } for i := 0; i < timesInt; i++ { - assert.Equal(t, "exp", fmt.Sprintf("notexp%v", i)) + if withMatch == " with match" { + assert.Equal(T(ctx), fmt.Sprintf("exp%v", i), fmt.Sprintf("exp%v", i)) + } else { + assert.Equal(T(ctx), "exp", fmt.Sprintf("notexp%v", i)) + } } return nil } -func (tc *godogFeaturesScenario) iCallTestifyAssertEqualMultipleTimesWithMatch(ctx context.Context, times string) error { - t := GetTestingT(ctx) - timesInt, err := strconv.Atoi(times) - if err != nil { - return fmt.Errorf("test step has invalid times value %s: %w", times, err) - } - for i := 0; i < timesInt; i++ { - assert.Equal(t, fmt.Sprintf("exp%v", i), fmt.Sprintf("exp%v", i)) - } +func (tc *godogFeaturesScenario) myStepCallsTestifyRequireEqual(ctx context.Context, a string, b string) error { + require.Equal(T(ctx), a, b) return nil } -func (tc *godogFeaturesScenario) iCallTestifyRequireEqual(ctx context.Context, a string, b string) error { - t := GetTestingT(ctx) - require.Equal(t, a, b) +func (tc *godogFeaturesScenario) myStepCallsTLog(ctx context.Context, message string) error { + T(ctx).Log(message) return nil } -func (tc *godogFeaturesScenario) iCallTLog(ctx context.Context, message string) error { - t := GetTestingT(ctx) - t.Log(message) +func (tc *godogFeaturesScenario) myStepCallsTLogf(ctx context.Context, message string, arg string) error { + T(ctx).Logf(message, arg) return nil } -func (tc *godogFeaturesScenario) iCallTLogf(ctx context.Context, message string, arg string) error { - t := GetTestingT(ctx) - t.Logf(message, arg) +func (tc *godogFeaturesScenario) myStepCallsDogLog(ctx context.Context, message string) error { + Log(ctx, message) return nil } +func (tc *godogFeaturesScenario) myStepCallsDogLogf(ctx context.Context, message string, arg string) error { + Logf(ctx, message, arg) + return nil +} + +func (tc *godogFeaturesScenario) theLoggedMessagesShouldInclude(ctx context.Context, message string) error { + messages := LoggedMessages(ctx) + for _, m := range messages { + if strings.Contains(m, message) { + return nil + } + } + return fmt.Errorf("the message %q was not logged (logged messages: %v)", message, messages) +} + func (tc *godogFeaturesScenario) followingStepsShouldHave(status string, steps *DocString) error { var expected = strings.Split(steps.Content, "\n") var actual, unmatched, matched []string diff --git a/testingt.go b/testingt.go index 33d0475d..03b8f727 100644 --- a/testingt.go +++ b/testingt.go @@ -7,11 +7,12 @@ import ( "testing" ) -var ( - _ TestingT = (*testing.B)(nil) - _ TestingT = (*testing.F)(nil) - _ TestingT = (*testing.T)(nil) -) +// T returns a TestingT compatible interface from the current test context. It will return nil if +// called outside the context of a test. This can be used with (for example) testify's assert and +// require packages. +func T(ctx context.Context) TestingT { + return getTestingT(ctx) +} // TestingT is a subset of the public methods implemented by go's testing.T. It allows assertion // libraries to be used with godog, provided they depend only on this subset of methods. @@ -69,11 +70,13 @@ func Log(ctx context.Context, args ...interface{}) { fallbackLog(args...) } -// T returns a TestingT compatible interface from the current test context. It will return nil if -// called outside the context of a test. This can be used with (for example) testify's assert and -// require packages. -func T(ctx context.Context) TestingT { - return getTestingT(ctx) +// LoggedMessages returns an array of any logged messages that have been recorded during the test +// through calls to godog.Log / godog.Logf or via operations against godog.T(ctx) +func LoggedMessages(ctx context.Context) []string { + if t := getTestingT(ctx); t != nil { + return t.logMessages + } + return nil } // errStopNow should be returned inside a panic within the test to immediately halt execution of that @@ -89,8 +92,13 @@ type testingT struct { logMessages []string } -// check interface: -var _ TestingT = &testingT{} +// check interface against our testingT and the upstream testing.B/F/T: +var ( + _ TestingT = &testingT{} + _ TestingT = (*testing.B)(nil) + _ TestingT = (*testing.F)(nil) + _ TestingT = (*testing.T)(nil) +) func (dt *testingT) Name() string { if dt.t != nil { From 939bd89862d58021b743bcac59ee5a5e04eb6465 Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Mon, 29 Apr 2024 09:18:38 +0100 Subject: [PATCH 9/9] Review updates --- testingt.go | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/testingt.go b/testingt.go index 03b8f727..25981b89 100644 --- a/testingt.go +++ b/testingt.go @@ -57,7 +57,7 @@ func Logf(ctx context.Context, format string, args ...interface{}) { t.Logf(format, args...) return } - fallbackLogf(format, args...) + fmt.Printf(format+"\n", args...) } // Log will log test output. If called in the context of a test and testing.T has been registered, @@ -67,7 +67,7 @@ func Log(ctx context.Context, args ...interface{}) { t.Log(args...) return } - fallbackLog(args...) + fmt.Println(args...) } // LoggedMessages returns an array of any logged messages that have been recorded during the test @@ -95,8 +95,6 @@ type testingT struct { // check interface against our testingT and the upstream testing.B/F/T: var ( _ TestingT = &testingT{} - _ TestingT = (*testing.B)(nil) - _ TestingT = (*testing.F)(nil) _ TestingT = (*testing.T)(nil) ) @@ -113,7 +111,7 @@ func (dt *testingT) Log(args ...interface{}) { dt.t.Log(args...) return } - fallbackLog(args...) + fmt.Println(args...) } func (dt *testingT) Logf(format string, args ...interface{}) { @@ -122,7 +120,7 @@ func (dt *testingT) Logf(format string, args ...interface{}) { dt.t.Logf(format, args...) return } - fallbackLogf(format, args...) + fmt.Printf(format+"\n", args...) } func (dt *testingT) Error(args ...interface{}) { @@ -206,13 +204,3 @@ func getTestingT(ctx context.Context) *testingT { } return dt } - -// fallbackLog is used to log when no testing.T is available. Set as a variable so this can be -// disabled / re-routed in future if needed. -var fallbackLog = fmt.Println - -// fallbackLogf is used to log a formatted string when no testing.T is available. Set as a variable -// so this can be disabled / re-routed in future if needed. -var fallbackLogf = func(message string, args ...interface{}) { - fmt.Printf(message+"\n", args...) -}