godog/testingt.go
Mark Hughes 7017c73ef8
Provide a useful implementation of something compatible with testing.T (#571)
* Attempting to provide a useful implementation of something compatible with testing.T

* Handle Fail calls on the TestingT in the right place

* Provide as much of testing.T as possible + tidy up

* Add initial tests for testingT support

* Check compatibility with testing.T and friends

Co-authored-by: Piotr Bocheński <bochenski.piotr@gmail.com>

* Update assert-godogs example to show new usage. Rename 'GetTestingT(ctx)' to 'T(ctx)'

* Update changelog and readme with new usage

* Improve test coverage

* Review updates

---------

Co-authored-by: Piotr Bocheński <bochenski.piotr@gmail.com>
2024-04-29 10:26:25 +02:00

206 строки
5,6 КиБ
Go

package godog
import (
"context"
"fmt"
"strings"
"testing"
)
// 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.
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
}
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 := getTestingT(ctx); t != nil {
t.Log(args...)
return
}
fmt.Println(args...)
}
// 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
// test
var errStopNow = fmt.Errorf("FailNow or SkipNow called")
type testingT struct {
name string
t *testing.T
failed bool
skipped bool
failMessages []string
logMessages []string
}
// check interface against our testingT and the upstream testing.B/F/T:
var (
_ TestingT = &testingT{}
_ TestingT = (*testing.T)(nil)
)
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...)
}
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...)
}
func (dt *testingT) Error(args ...interface{}) {
dt.Log(args...)
dt.failMessages = append(dt.failMessages, fmt.Sprintln(args...))
dt.Fail()
}
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 *testingT) Fail() {
dt.failed = true
}
func (dt *testingT) FailNow() {
dt.Fail()
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 *testingT) isFailed() error {
if dt.skipped {
return ErrSkip
}
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* "))
}
}
type testingTCtxVal struct{}
func setContextTestingT(ctx context.Context, dt *testingT) context.Context {
return context.WithValue(ctx, testingTCtxVal{}, dt)
}
func getTestingT(ctx context.Context) *testingT {
dt, ok := ctx.Value(testingTCtxVal{}).(*testingT)
if !ok {
return nil
}
return dt
}