mirror of
https://github.com/jimeh/go-mocktesting.git
synced 2026-02-18 19:46:38 +00:00
427 lines
8.9 KiB
Go
427 lines
8.9 KiB
Go
package mocktesting
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestingT is an interface covering *mocktesting.T's internal use of
|
|
// *testing.T. See WithTestingT() for more details.
|
|
type TestingT interface {
|
|
Fatal(args ...interface{})
|
|
}
|
|
|
|
// T is a fake/mock implementation of testing.T. It records all actions
|
|
// performed via all methods on the testing.T interface, so they can be
|
|
// inspected and asserted.
|
|
//
|
|
// It is specifically intended for testing test helpers which accept a
|
|
// *testing.T or *testing.B, so you can verify that the helpers call Fatal(),
|
|
// Error(), etc, as they need.
|
|
type T struct {
|
|
name string
|
|
abort bool
|
|
baseTempdir string
|
|
testingT TestingT
|
|
deadline time.Time
|
|
timeout bool
|
|
|
|
mux sync.RWMutex
|
|
skipped bool
|
|
failed int
|
|
parallel bool
|
|
output []string
|
|
helpers []string
|
|
aborted bool
|
|
cleanups []func()
|
|
env map[string]string
|
|
subtests []*T
|
|
tempdirs []string
|
|
|
|
// subtestNames is used to ensure subtests do not have conflicting names.
|
|
subtestNames map[string]bool
|
|
|
|
// mkdirTempFunc is used by the TempDir function instead of ioutil.TempDir()
|
|
// if it is not nil. This is only used by tests for TempDir itself to ensure
|
|
// it behaves correctly if temp directory creation fails.
|
|
mkdirTempFunc func(string, string) (string, error)
|
|
|
|
// Embed *testing.T to implement the testing.TB interface, which has a
|
|
// private method to prevent it from being implemented. However that means
|
|
// it's very difficult to test testing helpers.
|
|
*testing.T
|
|
}
|
|
|
|
// Ensure T struct implements testing.TB interface.
|
|
var _ testing.TB = (*T)(nil)
|
|
|
|
func NewT(name string, options ...Option) *T {
|
|
t := &T{
|
|
name: strings.ReplaceAll(name, " ", "_"),
|
|
abort: true,
|
|
baseTempdir: os.TempDir(),
|
|
deadline: time.Now().Add(10 * time.Minute),
|
|
timeout: true,
|
|
}
|
|
|
|
for _, opt := range options {
|
|
opt.apply(t)
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
type Option interface {
|
|
apply(*T)
|
|
}
|
|
|
|
type optionFunc func(*T)
|
|
|
|
func (fn optionFunc) apply(g *T) {
|
|
fn(g)
|
|
}
|
|
|
|
// WithTimeout specifies a custom timeout for the mock test. It effectively
|
|
// determines the return values of Deadline().
|
|
//
|
|
// When given a zero-value time.Duration, Deadline() will act as if no timeout
|
|
// has been set.
|
|
//
|
|
// If this option is not used, the default timeout value is set to 10 minutes.
|
|
func WithTimeout(d time.Duration) Option {
|
|
return optionFunc(func(t *T) {
|
|
if d > 0 {
|
|
t.timeout = true
|
|
t.deadline = time.Now().Add(d)
|
|
} else {
|
|
t.timeout = false
|
|
t.deadline = time.Time{}
|
|
}
|
|
})
|
|
}
|
|
|
|
// WithDeadline specifies a custom timeout for the mock test, but setting the
|
|
// deadline to an exact value, rather than setting it based on the offset from
|
|
// now of a time.Duration. It effectively determines the return values of
|
|
// Deadline().
|
|
//
|
|
// When given a empty time.Time{}, Deadline() will act as if no timeout has been
|
|
// set.
|
|
//
|
|
// If this option is not used, the default timeout value is set to 10 minutes.
|
|
func WithDeadline(d time.Time) Option {
|
|
return optionFunc(func(t *T) {
|
|
if d != (time.Time{}) {
|
|
t.timeout = true
|
|
t.deadline = d
|
|
} else {
|
|
t.timeout = false
|
|
t.deadline = time.Time{}
|
|
}
|
|
})
|
|
}
|
|
|
|
// WithNoAbort disables aborting the current goroutine with runtime.Goexit()
|
|
// when SkipNow or FailNow is called. This should be used with care, as it
|
|
// causes behavior to diverge from normal *tesing.T, as code after calling
|
|
// t.Fatal() will be executed.
|
|
func WithNoAbort() Option {
|
|
return optionFunc(func(t *T) {
|
|
t.abort = false
|
|
})
|
|
}
|
|
|
|
// WithBaseTempdir sets the base directory that TempDir() creates temporary
|
|
// directories within.
|
|
//
|
|
// If this option is not used, the default base directory used is os.TempDir().
|
|
func WithBaseTempdir(dir string) Option {
|
|
return optionFunc(func(t *T) {
|
|
if dir != "" {
|
|
t.baseTempdir = dir
|
|
}
|
|
})
|
|
}
|
|
|
|
// WithTestingT accepts a *testing.T instance which is used to report internal
|
|
// errors within *mocktesting.T itself. For example if the TempDir() function
|
|
// fails to create a temporary directory on disk, it will call Fatal() on the
|
|
// *testing.T instance provided here.
|
|
//
|
|
// If this option is not used, internal errors will instead cause a panic.
|
|
func WithTestingT(testingT TestingT) Option {
|
|
return optionFunc(func(t *T) {
|
|
t.testingT = testingT
|
|
})
|
|
}
|
|
|
|
func (t *T) goexit() {
|
|
t.aborted = true
|
|
if t.abort {
|
|
runtime.Goexit()
|
|
}
|
|
}
|
|
|
|
func (t *T) internalError(err error) {
|
|
err = fmt.Errorf("mocktesting: %w", err)
|
|
|
|
if t.testingT != nil {
|
|
t.testingT.Fatal(err)
|
|
} else {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (t *T) Name() string {
|
|
return t.name
|
|
}
|
|
|
|
func (t *T) Deadline() (time.Time, bool) {
|
|
return t.deadline, t.timeout
|
|
}
|
|
|
|
func (t *T) Error(args ...interface{}) {
|
|
t.Log(args...)
|
|
t.Fail()
|
|
}
|
|
|
|
func (t *T) Errorf(format string, args ...interface{}) {
|
|
t.Logf(format, args...)
|
|
t.Fail()
|
|
}
|
|
|
|
func (t *T) Fail() {
|
|
t.failed++
|
|
}
|
|
|
|
func (t *T) FailNow() {
|
|
t.Fail()
|
|
t.goexit()
|
|
}
|
|
|
|
func (t *T) Failed() bool {
|
|
return t.failed > 0
|
|
}
|
|
|
|
func (t *T) Fatal(args ...interface{}) {
|
|
t.Log(args...)
|
|
t.FailNow()
|
|
}
|
|
|
|
func (t *T) Fatalf(format string, args ...interface{}) {
|
|
t.Logf(format, args...)
|
|
t.FailNow()
|
|
}
|
|
|
|
func (t *T) Log(args ...interface{}) {
|
|
t.mux.Lock()
|
|
defer t.mux.Unlock()
|
|
|
|
t.output = append(t.output, fmt.Sprintln(args...))
|
|
}
|
|
|
|
func (t *T) Logf(format string, args ...interface{}) {
|
|
t.mux.Lock()
|
|
defer t.mux.Unlock()
|
|
|
|
if len(format) == 0 || format[len(format)-1] != '\n' {
|
|
format += "\n"
|
|
}
|
|
t.output = append(t.output, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (t *T) Parallel() {
|
|
t.parallel = true
|
|
}
|
|
|
|
func (t *T) Skip(args ...interface{}) {
|
|
t.Log(args...)
|
|
t.SkipNow()
|
|
}
|
|
|
|
func (t *T) Skipf(format string, args ...interface{}) {
|
|
t.Logf(format, args...)
|
|
t.SkipNow()
|
|
}
|
|
|
|
func (t *T) SkipNow() {
|
|
t.skipped = true
|
|
t.goexit()
|
|
}
|
|
|
|
func (t *T) Skipped() bool {
|
|
return t.skipped
|
|
}
|
|
|
|
func (t *T) Helper() {
|
|
pc, _, _, ok := runtime.Caller(1)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
fnName := runtime.FuncForPC(pc).Name()
|
|
|
|
t.mux.Lock()
|
|
defer t.mux.Unlock()
|
|
|
|
t.helpers = append(t.helpers, fnName)
|
|
}
|
|
|
|
func (t *T) Cleanup(f func()) {
|
|
t.mux.Lock()
|
|
defer t.mux.Unlock()
|
|
|
|
t.cleanups = append(t.cleanups, f)
|
|
}
|
|
|
|
func (t *T) TempDir() string {
|
|
// Allow setting MkdirTemp function for testing purposes.
|
|
f := t.mkdirTempFunc
|
|
if f == nil {
|
|
f = ioutil.TempDir
|
|
}
|
|
|
|
dir, err := f(t.baseTempdir, "go-mocktesting*")
|
|
if err != nil {
|
|
err = fmt.Errorf("TempDir() failed to create directory: %w", err)
|
|
t.internalError(err)
|
|
}
|
|
|
|
t.mux.Lock()
|
|
defer t.mux.Unlock()
|
|
t.tempdirs = append(t.tempdirs, dir)
|
|
|
|
return dir
|
|
}
|
|
|
|
func (t *T) Run(name string, f func(testing.TB)) bool {
|
|
name = t.newSubTestName(name)
|
|
fullname := name
|
|
if t.name != "" {
|
|
fullname = t.name + "/" + name
|
|
}
|
|
|
|
subtest := NewT(fullname)
|
|
subtest.abort = t.abort
|
|
subtest.baseTempdir = t.baseTempdir
|
|
subtest.testingT = t.testingT
|
|
subtest.deadline = t.deadline
|
|
subtest.timeout = t.timeout
|
|
|
|
if t.subtestNames == nil {
|
|
t.subtestNames = map[string]bool{}
|
|
}
|
|
|
|
t.mux.Lock()
|
|
t.subtests = append(t.subtests, subtest)
|
|
t.subtestNames[name] = true
|
|
t.mux.Unlock()
|
|
|
|
Go(func() {
|
|
f(subtest)
|
|
})
|
|
|
|
if subtest.Failed() {
|
|
t.Fail()
|
|
}
|
|
|
|
return !subtest.Failed()
|
|
}
|
|
|
|
func (t *T) newSubTestName(name string) string {
|
|
name = strings.ReplaceAll(name, " ", "_")
|
|
|
|
if !t.subtestNames[name] {
|
|
return name
|
|
}
|
|
|
|
i := 1
|
|
for {
|
|
n := name + "#" + fmt.Sprintf("%02d", i)
|
|
if !t.subtestNames[n] {
|
|
return n
|
|
}
|
|
|
|
i++
|
|
}
|
|
}
|
|
|
|
//
|
|
// Inspection Methods which are not part of the testing.TB interface.
|
|
//
|
|
|
|
// Output returns a string slice of all output produced by calls to Log(),
|
|
// Logf(), Error(), Errorf(), Fatal(), Fatalf(), Skip(), and Skipf().
|
|
func (t *T) Output() []string {
|
|
t.mux.RLock()
|
|
defer t.mux.RUnlock()
|
|
|
|
return t.output
|
|
}
|
|
|
|
// CleanupFuncs returns a slice of functions given to Cleanup().
|
|
func (t *T) CleanupFuncs() []func() {
|
|
t.mux.RLock()
|
|
defer t.mux.RUnlock()
|
|
|
|
return t.cleanups
|
|
}
|
|
|
|
// CleanupNames returns a string slice of function names given to Cleanup().
|
|
func (t *T) CleanupNames() []string {
|
|
r := make([]string, 0, len(t.cleanups))
|
|
for _, f := range t.cleanups {
|
|
p := reflect.ValueOf(f).Pointer()
|
|
r = append(r, runtime.FuncForPC(p).Name())
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// FailedCount returns the number of times Error(), Errorf(), Fail(), Failf(),
|
|
// FailNow(), Fatal(), and Fatalf() were called.
|
|
func (t *T) FailedCount() int {
|
|
return t.failed
|
|
}
|
|
|
|
// Aborted returns true if the TB instance aborted the current goroutine via
|
|
// runtime.Goexit(), which is called by FailNow() and SkipNow().
|
|
func (t *T) Aborted() bool {
|
|
return t.aborted
|
|
}
|
|
|
|
// HelperNames returns a list of function names which called Helper().
|
|
func (t *T) HelperNames() []string {
|
|
t.mux.RLock()
|
|
defer t.mux.RUnlock()
|
|
|
|
return t.helpers
|
|
}
|
|
|
|
// Paralleled returns true if Parallel() has been called.
|
|
func (t *T) Paralleled() bool {
|
|
return t.parallel
|
|
}
|
|
|
|
// Subtests returns a list map of *TB instances for any subtests executed via
|
|
// Run().
|
|
func (t *T) Subtests() []*T {
|
|
if t.subtests == nil {
|
|
t.mux.Lock()
|
|
t.subtests = []*T{}
|
|
t.mux.Unlock()
|
|
}
|
|
|
|
t.mux.RLock()
|
|
defer t.mux.RUnlock()
|
|
|
|
return t.subtests
|
|
}
|