diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..50b2422 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +--- +name: CI +on: [push] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.43 + env: + VERBOSE: "true" + + tidy: + name: Tidy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Check if mods are tidy + run: make check-tidy + + cov: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.16 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Publish coverage + uses: paambaati/codeclimate-action@v2.7.4 + env: + VERBOSE: "true" + GOMAXPROCS: 4 + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: make cov + prefix: github.com/${{ github.repository }} + coverageLocations: | + ${{ github.workspace }}/coverage.out:gocov + + test: + name: Test + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + go_version: + - "1.15" + - "1.16" + - "1.17" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go_version }} + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run tests + run: go test -v -count=1 -race ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1bda66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/* +coverage.out diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..bee01fe --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,96 @@ +linters-settings: + funlen: + lines: 100 + statements: 150 + gocyclo: + min-complexity: 20 + golint: + min-confidence: 0 + govet: + check-shadowing: true + enable-all: true + disable: + - fieldalignment + lll: + line-length: 80 + tab-width: 4 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - deadcode + - depguard + - durationcheck + - errcheck + - errorlint + - exhaustive + - exportloopref + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - godot + - gofumpt + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - importas + - ineffassign + - lll + - misspell + - nakedret + - nilerr + - nlreturn + - noctx + - nolintlint + - prealloc + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - structcheck + - tparallel + - typecheck + - unconvert + - unparam + - unused + - varcheck + - wastedassign + - whitespace + +issues: + exclude: + - Using the variable on range scope `tt` in function literal + - Using the variable on range scope `tc` in function literal + exclude-rules: + - path: "_test\\.go" + linters: + - funlen + - dupl + - source: "^//go:generate " + linters: + - lll + - source: "`json:" + linters: + - lll + - source: "`xml:" + linters: + - lll + - source: "`yaml:" + linters: + - lll + +run: + timeout: 2m + allow-parallel-runners: true + modules-download-mode: readonly diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9dcb8ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 Jim Myhrberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e6248a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,205 @@ +GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //') +SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \ + -or -name "Makefile") + +# Verbose output +ifdef VERBOSE +V = -v +endif + +# +# Environment +# + +BINDIR := bin +TOOLDIR := $(BINDIR)/tools + +# Global environment variables for all targets +SHELL ?= /bin/bash +SHELL := env \ + GO111MODULE=on \ + GOBIN=$(CURDIR)/$(TOOLDIR) \ + CGO_ENABLED=1 \ + PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \ + $(SHELL) + +# +# Defaults +# + +# Default target +.DEFAULT_GOAL := test + +# +# Tools +# + +TOOLS += $(TOOLDIR)/gobin +$(TOOLDIR)/gobin: + GO111MODULE=off go get -u github.com/myitcv/gobin + +# external tool +define tool # 1: binary-name, 2: go-import-path +TOOLS += $(TOOLDIR)/$(1) + +$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile + gobin $(V) "$(2)" +endef + +$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc)) +$(eval $(call tool,gofumpt,mvdan.cc/gofumpt)) +$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports)) +$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43)) +$(eval $(call tool,gomod,github.com/Helcaraxan/gomod)) + +.PHONY: tools +tools: $(TOOLS) + +# +# Development +# + +BENCH ?= . +TESTARGS ?= + +.PHONY: clean +clean: + rm -f $(TOOLS) + rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check + +.PHONY: test +test: + go test $(V) -count=1 -race $(TESTARGS) ./... + +.PHONY: test-deps +test-deps: + go test all + +.PHONY: lint +lint: $(TOOLDIR)/golangci-lint + golangci-lint $(V) run + +.PHONY: format +format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt + goimports -w . && gofumpt -w . + +.SILENT: bench +.PHONY: bench +bench: + go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./... + +.PHONY: golden-update +golden-update: + GOLDEN_UPDATE=1 $(MAKE) test + +.PHONY: golden-clean +golden-clean: + find . -type f -name '*.golden' -path '*/testdata/*' -delete + find . -type d -empty -path '*/testdata/*' -delete + +.PHONY: golden-regen +golden-regen: golden-clean golden-update + +# +# Code Generation +# + +.PHONY: generate +generate: + go generate ./... + +.PHONY: check-generate +check-generate: + $(eval CHKDIR := $(shell mktemp -d)) + cp -av . "$(CHKDIR)" + make -C "$(CHKDIR)/" generate + ( diff -rN . "$(CHKDIR)" && rm -rf "$(CHKDIR)" ) || \ + ( rm -rf "$(CHKDIR)" && exit 1 ) + +# +# Coverage +# + +.PHONY: cov +cov: coverage.out + +.PHONY: cov-html +cov-html: coverage.out + go tool cover -html=./coverage.out + +.PHONY: cov-func +cov-func: coverage.out + go tool cover -func=./coverage.out + +coverage.out: $(SOURCES) + go test $(V) -covermode=count -coverprofile=./coverage.out ./... + +# +# Dependencies +# + +.PHONY: deps +deps: + go mod download + +.PHONY: deps-update +deps-update: + go get -u -t ./... + +.PHONY: deps-analyze +deps-analyze: $(TOOLDIR)/gomod + gomod analyze + +.PHONY: tidy +tidy: + go mod tidy $(V) + +.PHONY: verify +verify: + go mod verify + +.SILENT: check-tidy +.PHONY: check-tidy +check-tidy: + cp go.mod go.mod.tidy-check + cp go.sum go.sum.tidy-check + go mod tidy + ( \ + diff go.mod go.mod.tidy-check && \ + diff go.sum go.sum.tidy-check && \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum \ + ) || ( \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum; \ + exit 1 \ + ) + +# +# Documentation +# + +# Serve docs +.PHONY: docs +docs: $(TOOLDIR)/godoc + $(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/) + @godoc -http=127.0.0.1:6060 + +# +# Release +# + +.PHONY: new-version +new-version: check-npx + npx standard-version + +.PHONY: next-version +next-version: check-npx + npx standard-version --dry-run + +.PHONY: check-npx +check-npx: + $(if $(shell which npx),,\ + $(error No npx found in PATH, please install NodeJS)) diff --git a/README.md b/README.md index f368abe..e554e23 100644 --- a/README.md +++ b/README.md @@ -1 +1,145 @@ -# mocktesting +

+ go-mocktesting +

+ +

+ + Mock *testing.T for the purpose of testing test helpers. + +

+ +

+ Go Reference + Actions Status + Coverage + GitHub issues + GitHub pull requests + License Status +

+ +## Import + +```go +import "github.com/jimeh/go-mocktesting" +``` + +## Usage + +```go +func Example_basic() { + assertTrue := func(t testing.TB, v bool) { + if v != true { + t.Error("expected false to be true") + } + } + + mt := mocktesting.NewT("TestMyBoolean1") + assertTrue(mt, true) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + + mt = mocktesting.NewT("TestMyBoolean2") + assertTrue(mt, false) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + fmt.Printf("Output: %s\n", strings.Join(mt.Output(), "")) + + // Output: + // Name: TestMyBoolean1 + // Failed: false + // Aborted: false + // Name: TestMyBoolean2 + // Failed: true + // Aborted: false + // Output: expected false to be true +} +``` + +```go +func Example_fatal() { + requireTrue := func(t testing.TB, v bool) { + if v != true { + t.Fatal("expected false to be true") + } + } + + mt := mocktesting.NewT("TestMyBoolean1") + requireTrue(mt, true) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + + mt = mocktesting.NewT("TestMyBoolean2") + mocktesting.Go(func() { + requireTrue(mt, false) + fmt.Println("This is never executed.") + }) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + fmt.Printf("Output: %s\n", strings.Join(mt.Output(), "")) + + // Output: + // Name: TestMyBoolean1 + // Failed: false + // Aborted: false + // Name: TestMyBoolean2 + // Failed: true + // Aborted: true + // Output: expected false to be true +} +``` + +```go +func Example_subtests() { + requireTrue := func(t testing.TB, v bool) { + if v != true { + t.Fatal("expected false to be true") + } + } + + mt := mocktesting.NewT("TestMyBoolean") + mt.Run("true", func(t testing.TB) { + requireTrue(t, true) + }) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Sub1-Name: %s\n", mt.Subtests()[0].Name()) + fmt.Printf("Sub1-Failed: %+v\n", mt.Subtests()[0].Failed()) + fmt.Printf("Sub1-Aborted: %+v\n", mt.Subtests()[0].Aborted()) + + mt.Run("false", func(t testing.TB) { + requireTrue(t, false) + fmt.Println("This is never executed.") + }) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Sub2-Name: %s\n", mt.Subtests()[1].Name()) + fmt.Printf("Sub2-Failed: %+v\n", mt.Subtests()[1].Failed()) + fmt.Printf("Sub2-Aborted: %+v\n", mt.Subtests()[1].Aborted()) + fmt.Printf("Sub2-Output: %s\n", strings.Join(mt.Subtests()[1].Output(), "")) + + // Output: + // Name: TestMyBoolean + // Failed: false + // Sub1-Name: TestMyBoolean/true + // Sub1-Failed: false + // Sub1-Aborted: false + // Failed: true + // Sub2-Name: TestMyBoolean/false + // Sub2-Failed: true + // Sub2-Aborted: true + // Sub2-Output: expected false to be true +} +``` + +## Documentation + +Please see the +[Go Reference](https://pkg.go.dev/github.com/jimeh/go-mocktesting#section-documentation) +for documentation and examples. + +## License + +[MIT](https://github.com/jimeh/go-mocktesting/blob/main/LICENSE) diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..58942e1 --- /dev/null +++ b/example_test.go @@ -0,0 +1,189 @@ +package mocktesting_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/jimeh/go-mocktesting" +) + +func Example_basic() { + assertTrue := func(t testing.TB, v bool) { + if v != true { + t.Error("expected false to be true") + } + } + + mt := mocktesting.NewT("TestMyBoolean1") + assertTrue(mt, true) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + + mt = mocktesting.NewT("TestMyBoolean2") + assertTrue(mt, false) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + fmt.Printf("Output: %s\n", strings.Join(mt.Output(), "")) + + // Output: + // Name: TestMyBoolean1 + // Failed: false + // Aborted: false + // Name: TestMyBoolean2 + // Failed: true + // Aborted: false + // Output: expected false to be true +} + +func Example_fatal() { + requireTrue := func(t testing.TB, v bool) { + if v != true { + t.Fatal("expected false to be true") + } + } + + mt := mocktesting.NewT("TestMyBoolean1") + requireTrue(mt, true) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + + mt = mocktesting.NewT("TestMyBoolean2") + mocktesting.Go(func() { + requireTrue(mt, false) + fmt.Println("This is never executed.") + }) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Aborted: %+v\n", mt.Aborted()) + fmt.Printf("Output: %s\n", strings.Join(mt.Output(), "")) + + // Output: + // Name: TestMyBoolean1 + // Failed: false + // Aborted: false + // Name: TestMyBoolean2 + // Failed: true + // Aborted: true + // Output: expected false to be true +} + +func Example_subtests() { + requireTrue := func(t testing.TB, v bool) { + if v != true { + t.Fatal("expected false to be true") + } + } + + mt := mocktesting.NewT("TestMyBoolean") + mt.Run("true", func(t testing.TB) { + requireTrue(t, true) + }) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Sub1-Name: %s\n", mt.Subtests()[0].Name()) + fmt.Printf("Sub1-Failed: %+v\n", mt.Subtests()[0].Failed()) + fmt.Printf("Sub1-Aborted: %+v\n", mt.Subtests()[0].Aborted()) + + mt.Run("false", func(t testing.TB) { + requireTrue(t, false) + fmt.Println("This is never executed.") + }) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Sub2-Name: %s\n", mt.Subtests()[1].Name()) + fmt.Printf("Sub2-Failed: %+v\n", mt.Subtests()[1].Failed()) + fmt.Printf("Sub2-Aborted: %+v\n", mt.Subtests()[1].Aborted()) + fmt.Printf("Sub2-Output: %s\n", strings.Join(mt.Subtests()[1].Output(), "")) + + // Output: + // Name: TestMyBoolean + // Failed: false + // Sub1-Name: TestMyBoolean/true + // Sub1-Failed: false + // Sub1-Aborted: false + // Failed: true + // Sub2-Name: TestMyBoolean/false + // Sub2-Failed: true + // Sub2-Aborted: true + // Sub2-Output: expected false to be true +} + +func Example_subtests_in_subtests() { + assertGreaterThan := func(t testing.TB, got int, min int) { + if got <= min { + t.Errorf("expected %d to be greater than %d", got, min) + } + } + + mt := mocktesting.NewT("TestMyBoolean") + mt.Run("positive", func(t testing.TB) { + subMT, _ := t.(*mocktesting.T) + + subMT.Run("greater than", func(t testing.TB) { + assertGreaterThan(t, 5, 4) + }) + subMT.Run("equal", func(t testing.TB) { + assertGreaterThan(t, 5, 5) + }) + subMT.Run("less than", func(t testing.TB) { + assertGreaterThan(t, 4, 5) + }) + }) + fmt.Printf("Name: %s\n", mt.Name()) + fmt.Printf("Failed: %+v\n", mt.Failed()) + fmt.Printf("Sub1-Name: %s\n", mt.Subtests()[0].Name()) + fmt.Printf("Sub1-Failed: %+v\n", mt.Subtests()[0].Failed()) + fmt.Printf("Sub1-Aborted: %+v\n", mt.Subtests()[0].Aborted()) + fmt.Printf("Sub1-Sub1-Name: %s\n", mt.Subtests()[0].Subtests()[0].Name()) + fmt.Printf( + "Sub1-Sub1-Failed: %+v\n", mt.Subtests()[0].Subtests()[0].Failed(), + ) + fmt.Printf( + "Sub1-Sub1-Aborted: %+v\n", mt.Subtests()[0].Subtests()[0].Aborted(), + ) + fmt.Printf("Sub1-Sub1-Name: %s\n", mt.Subtests()[0].Subtests()[1].Name()) + fmt.Printf( + "Sub1-Sub2-Failed: %+v\n", mt.Subtests()[0].Subtests()[1].Failed(), + ) + fmt.Printf( + "Sub1-Sub2-Aborted: %+v\n", mt.Subtests()[0].Subtests()[1].Aborted(), + ) + fmt.Printf( + "Sub1-Sub3-Output: %s\n", strings.TrimSpace( + strings.Join(mt.Subtests()[0].Subtests()[1].Output(), ""), + ), + ) + fmt.Printf("Sub1-Sub1-Name: %s\n", mt.Subtests()[0].Subtests()[2].Name()) + fmt.Printf( + "Sub1-Sub3-Failed: %+v\n", mt.Subtests()[0].Subtests()[2].Failed(), + ) + fmt.Printf( + "Sub1-Sub3-Aborted: %+v\n", mt.Subtests()[0].Subtests()[2].Aborted(), + ) + fmt.Printf( + "Sub1-Sub3-Output: %s\n", strings.TrimSpace( + strings.Join(mt.Subtests()[0].Subtests()[2].Output(), ""), + ), + ) + + // Output: + // Name: TestMyBoolean + // Failed: true + // Sub1-Name: TestMyBoolean/positive + // Sub1-Failed: true + // Sub1-Aborted: false + // Sub1-Sub1-Name: TestMyBoolean/positive/greater_than + // Sub1-Sub1-Failed: false + // Sub1-Sub1-Aborted: false + // Sub1-Sub1-Name: TestMyBoolean/positive/equal + // Sub1-Sub2-Failed: true + // Sub1-Sub2-Aborted: false + // Sub1-Sub3-Output: expected 5 to be greater than 5 + // Sub1-Sub1-Name: TestMyBoolean/positive/less_than + // Sub1-Sub3-Failed: true + // Sub1-Sub3-Aborted: false + // Sub1-Sub3-Output: expected 4 to be greater than 5 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ba1d7f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/jimeh/go-mocktesting + +go 1.15 + +require github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb88a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mocktesting.go b/mocktesting.go new file mode 100644 index 0000000..c545246 --- /dev/null +++ b/mocktesting.go @@ -0,0 +1,23 @@ +// Package mocktesting provides a mock of *testing.T for the purpose of testing +// test helpers. +package mocktesting + +import ( + "sync" +) + +// Go runs the provided function in a new goroutine, and blocks until the +// goroutine has exited. +// +// This is essentially a helper function to avoid aborting the current goroutine +// when a *T instance aborts the goroutine that any of FailNow(), Fatal(), +// Fatalf(), SkipNow(), Skip(), or Skipf() are called from. +func Go(f func()) { + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + f() + }() + wg.Wait() +} diff --git a/mocktesting_test.go b/mocktesting_test.go new file mode 100644 index 0000000..72d035b --- /dev/null +++ b/mocktesting_test.go @@ -0,0 +1,30 @@ +package mocktesting + +import ( + "sync" +) + +func runInGoroutine(f func()) { + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + f() + }() + wg.Wait() +} + +func stringsUniq(strs []string) []string { + m := map[string]bool{} + + for _, s := range strs { + m[s] = true + } + + r := make([]string, 0, len(m)) + for s := range m { + r = append(r, s) + } + + return r +} diff --git a/t.go b/t.go new file mode 100644 index 0000000..640d269 --- /dev/null +++ b/t.go @@ -0,0 +1,426 @@ +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 +} diff --git a/t_go116.go b/t_go116.go new file mode 100644 index 0000000..2595579 --- /dev/null +++ b/t_go116.go @@ -0,0 +1,31 @@ +//go:build go1.16 +// +build go1.16 + +package mocktesting + +func (t *T) Setenv(key string, value string) { + t.mux.Lock() + defer t.mux.Unlock() + + if t.env == nil { + t.env = map[string]string{} + } + + if key != "" { + t.env[key] = value + } +} + +// Getenv returns a map[string]string of keys/values given to Setenv(). +func (t *T) Getenv() map[string]string { + if t.env == nil { + t.mux.Lock() + t.env = map[string]string{} + t.mux.Unlock() + } + + t.mux.RLock() + defer t.mux.RUnlock() + + return t.env +} diff --git a/t_go116_test.go b/t_go116_test.go new file mode 100644 index 0000000..13a0f4c --- /dev/null +++ b/t_go116_test.go @@ -0,0 +1,182 @@ +//go:build go1.16 +// +build go1.16 + +package mocktesting + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestT_Setenv(t *testing.T) { + type fields struct { + env map[string]string + } + type args struct { + key string + value string + } + tests := []struct { + name string + fields fields + args args + want map[string]string + }{ + { + name: "empty key and value", + args: args{}, + want: map[string]string{}, + }, + { + name: "empty key", + args: args{value: "bar"}, + want: map[string]string{}, + }, + { + name: "empty value", + args: args{key: "foo"}, + want: map[string]string{"foo": ""}, + }, + { + name: "key and value", + args: args{key: "foo", value: "bar"}, + want: map[string]string{"foo": "bar"}, + }, + { + name: "add to existing", + fields: fields{env: map[string]string{"hello": "world"}}, + args: args{key: "foo", value: "bar"}, + want: map[string]string{"hello": "world", "foo": "bar"}, + }, + { + name: "overwrite existing", + fields: fields{env: map[string]string{"foo": "world"}}, + args: args{key: "foo", value: "bar"}, + want: map[string]string{"foo": "bar"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{env: tt.fields.env} + + mt.Setenv(tt.args.key, tt.args.value) + + assert.Equal(t, tt.want, mt.env) + }) + } +} + +func TestT_Getenv(t *testing.T) { + type fields struct { + env map[string]string + } + tests := []struct { + name string + fields fields + want map[string]string + }{ + { + name: "nil env", + want: map[string]string{}, + }, + { + name: "empty env", + fields: fields{env: map[string]string{}}, + want: map[string]string{}, + }, + { + name: "env", + fields: fields{ + env: map[string]string{ + "hello": "world", + "foo": "bar", + }, + }, + want: map[string]string{ + "hello": "world", + "foo": "bar", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{env: tt.fields.env} + + got := mt.Getenv() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Run_Go116(t *testing.T) { + type fields struct { + name string + abort bool + baseTempdir string + testingT testing.TB + deadline time.Time + timeout bool + } + type args struct { + f func(testing.TB) + } + tests := []struct { + name string + fields fields + args args + want *T + }{ + { + name: "set environment variables", + fields: fields{ + name: "set_environment_variables", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + // Type assert to *TB for compatibility with Go 1.16 and + // earlier. + mt := t.(*T) + mt.Setenv("GO_ENV", "test") + mt.Setenv("FOO", "bar") + }, + }, + want: &T{ + name: "set_environment_variables", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + env: map[string]string{ + "GO_ENV": "test", + "FOO": "bar", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{ + name: tt.fields.name, + abort: tt.fields.abort, + baseTempdir: tt.fields.baseTempdir, + testingT: tt.fields.testingT, + deadline: tt.fields.deadline, + timeout: tt.fields.timeout, + } + + runInGoroutine(func() { + tt.args.f(mt) + }) + + assertEqualMocktestingT(t, tt.want, mt) + }) + } +} diff --git a/t_test.go b/t_test.go new file mode 100644 index 0000000..56f5519 --- /dev/null +++ b/t_test.go @@ -0,0 +1,2460 @@ +package mocktesting + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func assertEqualMocktestingT(t *testing.T, want *T, got *T) { + assert.Equalf(t, want.name, got.name, "name field in %s", got.name) + assert.Equalf(t, want.abort, got.abort, "abort field in %s", got.name) + assert.Equalf(t, + want.baseTempdir, got.baseTempdir, + "baseTempdir field in %s", got.name, + ) + assert.Equalf(t, + want.testingT, got.testingT, + "testingT field in %s", got.name, + ) + assert.WithinDurationf(t, + want.deadline, got.deadline, 1*time.Second, + "deadline field in %s", got.name, + ) + assert.Equalf(t, want.timeout, got.timeout, "timeout field in %s", got.name) + assert.Equalf(t, want.skipped, got.skipped, "skipped field in %s", got.name) + assert.Equalf(t, want.failed, got.failed, "failed field in %s", got.name) + assert.Equalf(t, + want.parallel, got.parallel, + "parallel field in %s", got.name, + ) + assert.Equalf(t, want.output, got.output, "output field in %s", got.name) + assert.Equalf(t, want.helpers, got.helpers, "helpers field in %s", got.name) + assert.Equalf(t, want.aborted, got.aborted, "aborted field in %s", got.name) + + wantFuncs := make([]string, 0, len(want.cleanups)) + for _, f := range want.cleanups { + p := reflect.ValueOf(f).Pointer() + wantFuncs = append(wantFuncs, runtime.FuncForPC(p).Name()) + } + gotFuncs := make([]string, 0, len(got.cleanups)) + for _, f := range got.cleanups { + p := reflect.ValueOf(f).Pointer() + gotFuncs = append(gotFuncs, runtime.FuncForPC(p).Name()) + } + assert.Equalf(t, wantFuncs, gotFuncs, "cleanups field in %s", got.name) + + assert.Equalf(t, want.env, got.env, "env field in %s", got.name) + + for i, wantSubTest := range want.subtests { + gotSubTest := got.subtests[i] + assertEqualMocktestingT(t, wantSubTest, gotSubTest) + } + + assert.Equalf(t, + want.tempdirs, got.tempdirs, + "tempdirs field in %s", got.name, + ) + assert.Equalf(t, + want.subtestNames, got.subtestNames, + "subtestNames field in %s", got.name, + ) +} + +// TestT_methods is a horrible hack of a test to verify that *T directly +// implements/overloads all exported methods of *testing.T. The goal is for this +// test to fail on future versions of Go which add new methods to *testing.T. +func TestT_methods(t *testing.T) { + // Methods should be defined on a file within the same directory as this + // test file. + pc, _, _, _ := runtime.Caller(0) + testFile, _ := runtime.FuncForPC(pc).FileLine(pc) + wantDir := filepath.Dir(testFile) + + tType := reflect.TypeOf(&T{}) + testingType := reflect.TypeOf(t) + + for i := 0; i < testingType.NumMethod(); i++ { + method := testingType.Method(i) + t.Run(method.Name, func(t *testing.T) { + mth, ok := tType.MethodByName(method.Name) + if !ok { + require.FailNowf(t, "method not implemented", + "*mocktesting.T does not implement method %s from "+ + "*testing.T", + method.Name, + ) + } + + fp := mth.Func.Pointer() + file, line := runtime.FuncForPC(fp).FileLine(fp) + dir := filepath.Dir(file) + + if dir != wantDir || line <= 1 { + require.FailNowf(t, "method not implemented", + "*mocktesting.T does not implement method %s from "+ + "*testing.T", + method.Name, + ) + } + }) + } +} + +func TestNewT(t *testing.T) { + testingT := &T{name: "real"} + + type args struct { + name string + options []Option + } + tests := []struct { + name string + args args + want *T + }{ + { + name: "empty name", + args: args{name: ""}, + want: &T{ + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + }, + { + name: "with a name", + args: args{name: "TestFooBar_Nope"}, + want: &T{ + name: "TestFooBar_Nope", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + }, + { + name: "name with spaces", + args: args{name: "foo bar yep"}, + want: &T{ + name: "foo_bar_yep", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + }, + { + name: "with options", + args: args{ + name: "with options", + options: []Option{ + WithTimeout(6 * time.Minute), + WithNoAbort(), + WithBaseTempdir("/tmp/go-mocktesting"), + WithTestingT(testingT), + }, + }, + want: &T{ + name: "with_options", + abort: false, + baseTempdir: "/tmp/go-mocktesting", + testingT: testingT, + deadline: time.Now().Add(6 * time.Minute), + timeout: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewT(tt.args.name, tt.args.options...) + + // assert.Equal(t, tt.want, got) + assertEqualMocktestingT(t, tt.want, got) + }) + } +} + +func TestWithTimeout(t *testing.T) { + type fields struct { + deadline time.Time + timeout bool + } + type args struct { + d time.Duration + } + tests := []struct { + name string + args args + want fields + }{ + { + name: "zero", + args: args{d: time.Duration(0)}, + want: fields{ + timeout: false, + deadline: time.Time{}, + }, + }, + { + name: "1 minutes", + args: args{d: 1 * time.Minute}, + want: fields{ + timeout: true, + deadline: time.Now().Add(1 * time.Minute), + }, + }, + { + name: "10 minutes", + args: args{d: 10 * time.Minute}, + want: fields{ + timeout: true, + deadline: time.Now().Add(10 * time.Minute), + }, + }, + { + name: "3 hours", + args: args{d: 3 * time.Hour}, + want: fields{ + timeout: true, + deadline: time.Now().Add(3 * time.Hour), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{} + + WithTimeout(tt.args.d).apply(mt) + + assert.Equal(t, tt.want.timeout, mt.timeout) + assert.WithinDuration(t, + tt.want.deadline, mt.deadline, 1*time.Second, + ) + }) + } +} + +func TestWithDeadline(t *testing.T) { + in1Minute := time.Now().Add(1 * time.Minute) + in10Minutes := time.Now().Add(10 * time.Minute) + in3Hours := time.Now().Add(3 * time.Hour) + + type fields struct { + deadline time.Time + timeout bool + } + type args struct { + d time.Time + } + tests := []struct { + name string + args args + want fields + }{ + { + name: "zero", + args: args{d: time.Time{}}, + want: fields{ + timeout: false, + deadline: time.Time{}, + }, + }, + { + name: "1 minutes", + args: args{d: in1Minute}, + want: fields{ + timeout: true, + deadline: in1Minute, + }, + }, + { + name: "10 minutes", + args: args{d: in10Minutes}, + want: fields{ + timeout: true, + deadline: in10Minutes, + }, + }, + { + name: "3 hours", + args: args{d: in3Hours}, + want: fields{ + timeout: true, + deadline: in3Hours, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{} + + WithDeadline(tt.args.d).apply(mt) + + assert.Equal(t, tt.want.timeout, mt.timeout) + assert.Equal(t, tt.want.deadline, mt.deadline) + }) + } +} + +func TestWithNoAbort(t *testing.T) { + mt := &T{abort: true} + + WithNoAbort().apply(mt) + + assert.Equal(t, false, mt.abort) +} + +func TestWithBaseTempdir(t *testing.T) { + type args struct { + dir string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty string", + args: args{dir: ""}, + want: os.TempDir(), + }, + { + name: "non-empty string", + args: args{dir: "/tmp/foo-bar-nope"}, + want: "/tmp/foo-bar-nope", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{baseTempdir: os.TempDir()} + + WithBaseTempdir(tt.args.dir).apply(mt) + + assert.Equal(t, tt.want, mt.baseTempdir) + }) + } +} + +func TestWithTestingT(t *testing.T) { + fakeTestingT := &T{name: "parent"} + + type args struct { + t testing.TB + } + tests := []struct { + name string + args args + want TestingT + }{ + { + name: "nil", + args: args{t: nil}, + want: nil, + }, + { + name: "with *TB instance", + args: args{t: fakeTestingT}, + want: fakeTestingT, + }, + { + name: "with *testing.T instance", + args: args{t: t}, + want: t, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{baseTempdir: os.TempDir()} + + WithTestingT(tt.args.t).apply(mt) + + assert.Equal(t, tt.want, mt.testingT) + }) + } +} + +func TestT_Name(t *testing.T) { + type fields struct { + name string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "empty", + fields: fields{name: ""}, + want: "", + }, + { + name: "foo", + fields: fields{name: "foo"}, + want: "foo", + }, + { + name: "foo/bar", + fields: fields{name: "foo/bar"}, + want: "foo/bar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{name: tt.fields.name} + + got := mt.Name() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Deadline(t *testing.T) { + in1Minute := time.Now().Add(1 * time.Minute) + in10Minutes := time.Now().Add(10 * time.Minute) + in3Hours := time.Now().Add(3 * time.Hour) + + type fields struct { + deadline time.Time + timeout bool + } + tests := []struct { + name string + fields fields + want time.Time + wantOK bool + }{ + { + name: "empty", + fields: fields{}, + want: time.Time{}, + wantOK: false, + }, + { + name: "in 1 minutes", + fields: fields{ + deadline: in1Minute, + timeout: true, + }, + want: in1Minute, + wantOK: true, + }, + { + name: "in 10 minutes", + fields: fields{ + deadline: in10Minutes, + timeout: true, + }, + want: in10Minutes, + wantOK: true, + }, + { + name: "in 3 hours", + fields: fields{ + deadline: in3Hours, + timeout: true, + }, + want: in3Hours, + wantOK: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{ + deadline: tt.fields.deadline, + timeout: tt.fields.timeout, + } + + got, gotOK := mt.Deadline() + + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantOK, gotOK) + }) + } +} + +func TestT_Error(t *testing.T) { + type args struct { + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "one arg", + args: args{args: []interface{}{"hello world"}}, + wantLogs: []string{"hello world\n"}, + }, + { + name: "many args", + args: args{ + args: []interface{}{ + "hello world", + "where's my car?", + 1024, + }, + }, + wantLogs: []string{"hello world where's my car? 1024\n"}, + }, + } + variants := map[int]string{ + 0: "not failed", + 1: "failed once", + 2: "failed twice", + } + for _, tt := range tests { + for failedCount, nameSuffix := range variants { + t.Run(tt.name+", "+nameSuffix, func(t *testing.T) { + mt := &T{failed: failedCount} + + mt.Error(tt.args.args...) + + assert.Equal(t, failedCount+1, mt.failed) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Errorf(t *testing.T) { + type args struct { + format string + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no format or args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "format with no args", + args: args{format: "something went wrong"}, + wantLogs: []string{"something went wrong\n"}, + }, + { + name: "format with args", + args: args{ + format: "something went wrong: %s (%d)", + args: []interface{}{ + "not found", + 404, + }, + }, + wantLogs: []string{"something went wrong: not found (404)\n"}, + }, + } + variants := map[int]string{ + 0: "not failed", + 1: "failed once", + 2: "failed twice", + } + for _, tt := range tests { + for failedCount, nameSuffix := range variants { + t.Run(tt.name+", "+nameSuffix, func(t *testing.T) { + mt := &T{failed: failedCount} + + mt.Errorf(tt.args.format, tt.args.args...) + + assert.Equal(t, failedCount+1, mt.failed) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Fail(t *testing.T) { + type fields struct { + failed int + } + tests := []struct { + name string + fields fields + want int + }{ + { + name: "not failed", + fields: fields{failed: 0}, + want: 1, + }, + { + name: "failed once", + fields: fields{failed: 1}, + want: 2, + }, + { + name: "failed twice", + fields: fields{failed: 2}, + want: 3, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{failed: tt.fields.failed} + + mt.Fail() + + assert.Equal(t, tt.want, mt.failed) + }) + } +} + +func TestT_FailNow(t *testing.T) { + type fields struct { + abort bool + failed int + } + tests := []struct { + name string + fields fields + wantFailedCount int + }{ + { + name: "not failed", + fields: fields{abort: true, failed: 0}, + wantFailedCount: 1, + }, + { + name: "not failed, without abort", + fields: fields{abort: false, failed: 0}, + wantFailedCount: 1, + }, + { + name: "failed once", + fields: fields{abort: true, failed: 1}, + wantFailedCount: 2, + }, + { + name: "failed once, without abort", + fields: fields{abort: false, failed: 1}, + wantFailedCount: 2, + }, + { + name: "failed twice", + fields: fields{abort: true, failed: 2}, + wantFailedCount: 3, + }, + { + name: "failed twice, without abort", + fields: fields{abort: false, failed: 2}, + wantFailedCount: 3, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{ + abort: tt.fields.abort, + failed: tt.fields.failed, + } + + halted := true + runInGoroutine(func() { + mt.FailNow() + halted = false + }) + + assert.Equal(t, tt.wantFailedCount, mt.failed) + assert.Equal(t, true, mt.aborted) + assert.Equal(t, tt.fields.abort, halted) + assert.Empty(t, mt.output) + }) + } +} + +func TestT_Failed(t *testing.T) { + type fields struct { + failed int + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "not failed", + fields: fields{failed: 0}, + want: false, + }, + { + name: "failed once", + fields: fields{failed: 1}, + want: true, + }, + { + name: "failed twice", + fields: fields{failed: 2}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{failed: tt.fields.failed} + + got := mt.Failed() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_FailedCount(t *testing.T) { + type fields struct { + failed int + } + tests := []struct { + name string + fields fields + want int + }{ + { + name: "not failed", + fields: fields{failed: 0}, + want: 0, + }, + { + name: "failed once", + fields: fields{failed: 1}, + want: 1, + }, + { + name: "failed twice", + fields: fields{failed: 2}, + want: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{failed: tt.fields.failed} + + got := mt.FailedCount() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Fatal(t *testing.T) { + type fields struct { + abort bool + failed int + } + type args struct { + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "one arg", + args: args{args: []interface{}{"hello world"}}, + wantLogs: []string{"hello world\n"}, + }, + { + name: "many args", + args: args{ + args: []interface{}{ + "hello world", + "where's my car?", + 1024, + }, + }, + wantLogs: []string{"hello world where's my car? 1024\n"}, + }, + } + variants := map[string]fields{ + ", with abort, not failed": {abort: true, failed: 0}, + ", with abort, failed once": {abort: true, failed: 1}, + ", with abort, failed twice": {abort: true, failed: 2}, + ", without abort, not failed": {abort: false, failed: 0}, + ", without abort, failed once": {abort: false, failed: 1}, + ", without abort, failed twice": {abort: false, failed: 2}, + } + for _, tt := range tests { + for nameSuffix, flds := range variants { + t.Run(tt.name+nameSuffix, func(t *testing.T) { + mt := &T{ + abort: flds.abort, + failed: flds.failed, + } + halted := true + + runInGoroutine(func() { + mt.Fatal(tt.args.args...) + halted = false + }) + + assert.Equal(t, flds.failed+1, mt.failed) + assert.Equal(t, true, mt.aborted) + assert.Equal(t, flds.abort, halted) + + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Fatalf(t *testing.T) { + type fields struct { + abort bool + failed int + } + type args struct { + format string + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no format or args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "format with no args", + args: args{format: "something went wrong"}, + wantLogs: []string{"something went wrong\n"}, + }, + { + name: "format with args", + args: args{ + format: "something went wrong: %s (%d)", + args: []interface{}{ + "not found", + 404, + }, + }, + wantLogs: []string{"something went wrong: not found (404)\n"}, + }, + } + variants := map[string]fields{ + ", with abort, not failed": {abort: true, failed: 0}, + ", with abort, failed once": {abort: true, failed: 1}, + ", with abort, failed twice": {abort: true, failed: 2}, + ", without abort, not failed": {abort: false, failed: 0}, + ", without abort, failed once": {abort: false, failed: 1}, + ", without abort, failed twice": {abort: false, failed: 2}, + } + for _, tt := range tests { + for nameSuffix, flds := range variants { + t.Run(tt.name+nameSuffix, func(t *testing.T) { + mt := &T{ + abort: flds.abort, + failed: flds.failed, + } + halted := true + + runInGoroutine(func() { + mt.Fatalf(tt.args.format, tt.args.args...) + halted = false + }) + + assert.Equal(t, flds.failed+1, mt.failed) + assert.Equal(t, true, mt.aborted) + assert.Equal(t, flds.abort, halted) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Log(t *testing.T) { + type fields struct { + failed int + } + type args struct { + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "one arg", + args: args{args: []interface{}{"hello world"}}, + wantLogs: []string{"hello world\n"}, + }, + { + name: "many args", + args: args{ + args: []interface{}{ + "hello world", + "where's my car?", + 1024, + }, + }, + wantLogs: []string{"hello world where's my car? 1024\n"}, + }, + } + variants := map[string]fields{ + ", not failed": {failed: 0}, + ", failed once": {failed: 1}, + ", failed twice": {failed: 2}, + } + for _, tt := range tests { + for nameSuffix, flds := range variants { + t.Run(tt.name+nameSuffix, func(t *testing.T) { + mt := &T{failed: flds.failed} + + mt.Log(tt.args.args...) + + assert.Equal(t, flds.failed, mt.failed) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Logf(t *testing.T) { + type fields struct { + failed int + } + type args struct { + format string + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no format or args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "format with no args", + args: args{format: "something went wrong"}, + wantLogs: []string{"something went wrong\n"}, + }, + { + name: "format with args", + args: args{ + format: "something went wrong: %s (%d)", + args: []interface{}{ + "not found", + 404, + }, + }, + wantLogs: []string{"something went wrong: not found (404)\n"}, + }, + } + variants := map[string]fields{ + ", not failed": {failed: 0}, + ", failed once": {failed: 1}, + ", failed twice": {failed: 2}, + } + for _, tt := range tests { + for nameSuffix, flds := range variants { + t.Run(tt.name+nameSuffix, func(t *testing.T) { + mt := &T{failed: flds.failed} + + mt.Logf(tt.args.format, tt.args.args...) + + assert.Equal(t, flds.failed, mt.failed) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Parallel(t *testing.T) { + type fields struct { + parallel bool + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "not parallel", + fields: fields{parallel: false}, + want: true, + }, + { + name: "already parallel", + fields: fields{parallel: true}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{parallel: tt.fields.parallel} + + mt.Parallel() + + assert.Equal(t, tt.want, mt.parallel) + }) + } +} + +func TestT_Paralleled(t *testing.T) { + type fields struct { + parallel bool + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "not paralleled", + fields: fields{parallel: false}, + want: false, + }, + { + name: "paralleled", + fields: fields{parallel: true}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{parallel: tt.fields.parallel} + + got := mt.Paralleled() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Skip(t *testing.T) { + type args struct { + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "one arg", + args: args{args: []interface{}{"hello world"}}, + wantLogs: []string{"hello world\n"}, + }, + { + name: "many args", + args: args{ + args: []interface{}{ + "hello world", + "where's my car?", + 1024, + }, + }, + wantLogs: []string{"hello world where's my car? 1024\n"}, + }, + } + variants := map[bool]string{ + true: ", with abort", + false: ", without abort", + } + for _, tt := range tests { + for abort, nameSuffix := range variants { + t.Run(tt.name+nameSuffix, func(t *testing.T) { + mt := &T{abort: abort} + halted := true + + runInGoroutine(func() { + mt.Skip(tt.args.args...) + halted = false + }) + + assert.True(t, mt.skipped) + assert.True(t, mt.aborted) + assert.Equal(t, abort, halted) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_Skipf(t *testing.T) { + type args struct { + format string + args []interface{} + } + tests := []struct { + name string + args args + wantLogs []string + }{ + { + name: "no format or args", + args: args{}, + wantLogs: []string{"\n"}, + }, + { + name: "format with no args", + args: args{format: "something went wrong"}, + wantLogs: []string{"something went wrong\n"}, + }, + { + name: "format with args", + args: args{ + format: "something went wrong: %s (%d)", + args: []interface{}{ + "not found", + 404, + }, + }, + wantLogs: []string{"something went wrong: not found (404)\n"}, + }, + } + variants := map[bool]string{ + true: ", with abort", + false: ", without abort", + } + for _, tt := range tests { + for abort, nameSuffix := range variants { + t.Run(tt.name+nameSuffix, func(t *testing.T) { + mt := &T{abort: abort} + halted := true + + runInGoroutine(func() { + mt.Skipf(tt.args.format, tt.args.args...) + halted = false + }) + + assert.True(t, mt.skipped) + assert.True(t, mt.aborted) + assert.Equal(t, abort, halted) + assert.Equal(t, tt.wantLogs, mt.output) + }) + } + } +} + +func TestT_SkipNow(t *testing.T) { + type fields struct { + abort bool + } + tests := []struct { + name string + fields fields + }{ + { + name: "with abort", + fields: fields{abort: true}, + }, + { + name: "without abort", + fields: fields{abort: false}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{abort: tt.fields.abort} + halted := true + + runInGoroutine(func() { + mt.SkipNow() + halted = false + }) + + assert.True(t, mt.skipped) + assert.True(t, mt.aborted) + assert.Equal(t, tt.fields.abort, halted) + assert.Empty(t, mt.output) + }) + } +} + +func TestT_Skipped(t *testing.T) { + type fields struct { + skipped bool + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "not skipped", + fields: fields{skipped: false}, + want: false, + }, + { + name: "skipped", + fields: fields{skipped: true}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{skipped: tt.fields.skipped} + + got := mt.Skipped() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Helper(t *testing.T) { + helper1 := func(t testing.TB) { + t.Helper() + } + helper2 := func(t testing.TB) { + t.Helper() + } + helper3 := func(t testing.TB) { + t.Helper() + helper1(t) + } + + mt1 := &T{} + helper3(mt1) + + mt2 := &T{} + helper2(mt2) + + assert.Equal(t, + []string{ + "github.com/jimeh/go-mocktesting.TestT_Helper.func3", + "github.com/jimeh/go-mocktesting.TestT_Helper.func1", + }, + mt1.helpers, + ) + assert.Equal(t, + []string{ + "github.com/jimeh/go-mocktesting.TestT_Helper.func2", + }, + mt2.helpers, + ) +} + +func TestT_Cleanup(t *testing.T) { + cleanup1 := func() {} + cleanup2 := func() {} + cleanup3 := func() {} + + mt1 := &T{} + mt1.Cleanup(cleanup3) + mt1.Cleanup(cleanup1) + + mt1CleanupNames := make([]string, 0, len(mt1.cleanups)) + for _, f := range mt1.cleanups { + p := reflect.ValueOf(f).Pointer() + mt1CleanupNames = append(mt1CleanupNames, runtime.FuncForPC(p).Name()) + } + + mt2 := &T{} + mt2.Cleanup(cleanup2) + + mt2CleanupNames := make([]string, 0, len(mt2.cleanups)) + for _, f := range mt2.cleanups { + p := reflect.ValueOf(f).Pointer() + mt2CleanupNames = append(mt2CleanupNames, runtime.FuncForPC(p).Name()) + } + + assert.Equal(t, + []string{ + "github.com/jimeh/go-mocktesting.TestT_Cleanup.func3", + "github.com/jimeh/go-mocktesting.TestT_Cleanup.func1", + }, + mt1CleanupNames, + ) + assert.Equal(t, + []string{ + "github.com/jimeh/go-mocktesting.TestT_Cleanup.func2", + }, + mt2CleanupNames, + ) +} + +func TestT_TempDir(t *testing.T) { + customTempDir := t.TempDir() + assert.DirExists(t, customTempDir) + + type fields struct { + baseTempdir string + testingT testing.TB + mkdirTempFunc func(string, string) (string, error) + } + tests := []struct { + name string + calls int + fields fields + wantPrefix string + wantExists bool + wantPanic interface{} + wantTestingT *T + }{ + { + name: "not called", + calls: 0, + wantExists: false, + }, + { + name: "called once", + calls: 1, + wantPrefix: filepath.Join(os.TempDir(), "go-mocktesting"), + wantExists: true, + }, + { + name: "called twice", + calls: 2, + wantPrefix: filepath.Join(os.TempDir(), "go-mocktesting"), + wantExists: true, + }, + { + name: "called three times", + calls: 3, + wantPrefix: filepath.Join(os.TempDir(), "go-mocktesting"), + wantExists: true, + }, + { + name: "custom base tempdir", + calls: 1, + fields: fields{baseTempdir: customTempDir}, + wantPrefix: filepath.Join(customTempDir, "go-mocktesting"), + wantExists: true, + }, + { + name: "directory creation fails", + calls: 1, + fields: fields{ + mkdirTempFunc: func(_ string, _ string) (string, error) { + return "", errors.New("can't create dir") + }, + }, + wantPanic: fmt.Errorf( + "mocktesting: %w", + fmt.Errorf( + "TempDir() failed to create directory: %w", + errors.New("can't create dir"), + ), + ), + }, + { + name: "directory creation fails with testingT assigned", + calls: 1, + fields: fields{ + testingT: &T{name: "real", abort: true}, + mkdirTempFunc: func(_ string, _ string) (string, error) { + return "", errors.New("can't create dir") + }, + }, + wantTestingT: &T{ + name: "real", + abort: true, + failed: 1, + aborted: true, + output: []string{ + "mocktesting: TempDir() failed to create directory: " + + "can't create dir\n", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{ + baseTempdir: tt.fields.baseTempdir, + testingT: tt.fields.testingT, + mkdirTempFunc: tt.fields.mkdirTempFunc, + } + + var dirs []string + for i := 0; i < tt.calls; i++ { + f := func() (dir string, p interface{}) { + defer func() { p = recover() }() + dir = mt.TempDir() + + return + } + + var dir string + var p interface{} + runInGoroutine(func() { + dir, p = f() + }) + + if dir != "" { + t.Cleanup(func() { os.Remove(dir) }) + dirs = append(dirs, dir) + } + + assert.Equal(t, tt.wantPanic, p) + } + + assert.Equal(t, dirs, mt.tempdirs) + if tt.calls > 1 { + assert.Len(t, stringsUniq(dirs), tt.calls, + "returned temporary directories are not unique", + ) + } + for _, dir := range dirs { + assert.Truef(t, strings.HasPrefix(dir, tt.wantPrefix), + "temporary directory %s does not start with %s", + dir, tt.wantPrefix, + ) + if tt.wantExists { + assert.DirExists(t, dir) + } + } + + if tt.wantTestingT != nil { + assert.Equal(t, tt.wantTestingT, mt.testingT) + } + }) + } +} + +func TestT_Run(t *testing.T) { + cleanup1 := func() {} + cleanup2 := func() {} + cleanup3 := func() {} + + helper1 := func(t testing.TB) { t.Helper() } + helper2 := func(t testing.TB) { t.Helper() } + helper3 := func(t testing.TB) { t.Helper(); helper1(t) } + + customTempDir, err := ioutil.TempDir(os.TempDir(), t.Name()+"*") + require.NoError(t, err) + + type fields struct { + name string + abort bool + baseTempdir string + testingT testing.TB + deadline time.Time + timeout bool + } + type args struct { + f func(testing.TB) + } + tests := []struct { + name string + fields fields + args args + want *T + }{ + { + name: "does nothing", + fields: fields{ + name: "nothing", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + _ = fmt.Sprintf("nothing %s", t.Name()) + }, + }, + want: &T{ + name: "nothing", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + }, + { + name: "fails", + fields: fields{ + name: "fails", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + t.Log("before Fail") + t.Fail() + t.Log("after Fail") + }, + }, + want: &T{ + name: "fails", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 1, + output: []string{"before Fail\n", "after Fail\n"}, + }, + }, + { + name: "fails and halts", + fields: fields{ + name: "fails_and_halts", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + t.Log("before Fail") + t.Fail() + t.Log("after Fail") + t.FailNow() + t.Log("after FailNow") + }, + }, + want: &T{ + name: "fails_and_halts", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + aborted: true, + failed: 2, + output: []string{"before Fail\n", "after Fail\n"}, + }, + }, + { + name: "skips", + fields: fields{ + name: "skips", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + t.Log("before Skip") + t.Skip("skipping because reasons") + t.Log("after Skip") + }, + }, + want: &T{ + name: "skips", + abort: true, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + skipped: true, + aborted: true, + output: []string{ + "before Skip\n", + "skipping because reasons\n", + }, + baseTempdir: os.TempDir(), + }, + }, + { + name: "fails and skips", + fields: fields{ + name: "fails_and_skips", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + t.Log("before Fail") + t.Error("oops") + t.Log("before Skip") + t.Skip("skipping because reasons") + t.Log("after Skip") + }, + }, + want: &T{ + name: "fails_and_skips", + abort: true, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + skipped: true, + failed: 1, + aborted: true, + output: []string{ + "before Fail\n", + "oops\n", + "before Skip\n", + "skipping because reasons\n", + }, + baseTempdir: os.TempDir(), + }, + }, + { + name: "parallel", + fields: fields{ + name: "parallel", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + mt, _ := t.(*T) + mt.Parallel() + }, + }, + want: &T{ + name: "parallel", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + parallel: true, + }, + }, + { + name: "helpers", + fields: fields{ + name: "helpers", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + helper2(t) + helper3(t) + }, + }, + want: &T{ + name: "helpers", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + helpers: []string{ + "github.com/jimeh/go-mocktesting.TestT_Run.func5", + "github.com/jimeh/go-mocktesting.TestT_Run.func6", + "github.com/jimeh/go-mocktesting.TestT_Run.func4", + }, + }, + }, + { + name: "cleanups", + fields: fields{ + name: "cleanups", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + t.Cleanup(cleanup3) + t.Cleanup(cleanup1) + t.Cleanup(cleanup2) + }, + }, + want: &T{ + name: "cleanups", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + cleanups: []func(){cleanup3, cleanup1, cleanup2}, + }, + }, + { + name: "subtests with no failures", + fields: fields{ + name: "subtests_no_failures", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + }) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from second sub-test") + }) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from third sub-test") + }) + + mt.Run("hello, world", func(t testing.TB) { + t.Log("from fourth sub-test") + }) + }, + }, + want: &T{ + name: "subtests_no_failures", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + subtests: []*T{ + { + name: "subtests_no_failures/foo_bar", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from first sub-test\n"}, + }, + { + name: "subtests_no_failures/foo_bar#01", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from second sub-test\n"}, + }, + { + name: "subtests_no_failures/foo_bar#02", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from third sub-test\n"}, + }, + { + name: "subtests_no_failures/hello,_world", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from fourth sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "foo_bar#01": true, + "foo_bar#02": true, + "hello,_world": true, + }, + }, + }, + { + name: "subtests with failures", + fields: fields{ + name: "subtests_fail", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + t.Fail() + t.Log("after failure") + }) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from second sub-test") + }) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from third sub-test") + t.FailNow() + t.Log("after failure") + }) + + mt.Run("hello, world", func(t testing.TB) { + t.Log("from fourth sub-test") + }) + }, + }, + want: &T{ + name: "subtests_fail", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 2, + subtests: []*T{ + { + name: "subtests_fail/foo_bar", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 1, + output: []string{ + "from first sub-test\n", + "after failure\n", + }, + }, + { + name: "subtests_fail/foo_bar#01", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from second sub-test\n"}, + }, + { + name: "subtests_fail/foo_bar#02", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 1, + aborted: true, + output: []string{"from third sub-test\n"}, + }, + { + name: "subtests_fail/hello,_world", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from fourth sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "foo_bar#01": true, + "foo_bar#02": true, + "hello,_world": true, + }, + }, + }, + { + name: "subtests inherit abort value", + fields: fields{ + name: "subtests_inherit", + abort: false, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + baseTempdir: os.TempDir(), + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + }) + + mt.Run("hello world", func(t testing.TB) { + t.Log("from second sub-test") + }) + }, + }, + want: &T{ + name: "subtests_inherit", + abort: false, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + subtests: []*T{ + { + name: "subtests_inherit/foo_bar", + abort: false, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + output: []string{"from first sub-test\n"}, + }, + { + name: "subtests_inherit/hello_world", + abort: false, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from second sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "hello_world": true, + }, + }, + }, + { + name: "subtests inherit baseTempdir value", + fields: fields{ + name: "subtests_inherit", + abort: true, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + baseTempdir: customTempDir, + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + }) + + mt.Run("hello world", func(t testing.TB) { + t.Log("from second sub-test") + }) + }, + }, + want: &T{ + name: "subtests_inherit", + abort: true, + baseTempdir: customTempDir, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + subtests: []*T{ + { + name: "subtests_inherit/foo_bar", + abort: true, + baseTempdir: customTempDir, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + output: []string{"from first sub-test\n"}, + }, + { + name: "subtests_inherit/hello_world", + abort: true, + baseTempdir: customTempDir, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from second sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "hello_world": true, + }, + }, + }, + { + name: "subtests inherit testingT value", + fields: fields{ + name: "subtests_inherit", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + testingT: &T{name: "my custom testingT"}, + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + }) + + mt.Run("hello world", func(t testing.TB) { + t.Log("from second sub-test") + }) + }, + }, + want: &T{ + name: "subtests_inherit", + abort: true, + baseTempdir: os.TempDir(), + testingT: &T{name: "my custom testingT"}, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + subtests: []*T{ + { + name: "subtests_inherit/foo_bar", + abort: true, + baseTempdir: os.TempDir(), + testingT: &T{name: "my custom testingT"}, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + failed: 0, + output: []string{"from first sub-test\n"}, + }, + { + name: "subtests_inherit/hello_world", + abort: true, + baseTempdir: os.TempDir(), + testingT: &T{name: "my custom testingT"}, + deadline: time.Now().Add(10 * time.Minute), + timeout: true, + output: []string{"from second sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "hello_world": true, + }, + }, + }, + { + name: "subtests inherit deadline value", + fields: fields{ + name: "subtests_inherit", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(4 * time.Minute), + timeout: true, + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + }) + + mt.Run("hello world", func(t testing.TB) { + t.Log("from second sub-test") + }) + }, + }, + want: &T{ + name: "subtests_inherit", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(4 * time.Minute), + timeout: true, + failed: 0, + subtests: []*T{ + { + name: "subtests_inherit/foo_bar", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(4 * time.Minute), + timeout: true, + failed: 0, + output: []string{"from first sub-test\n"}, + }, + { + name: "subtests_inherit/hello_world", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(4 * time.Minute), + timeout: true, + output: []string{"from second sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "hello_world": true, + }, + }, + }, + { + name: "subtests inherit timeout value", + fields: fields{ + name: "subtests_inherit", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: false, + }, + args: args{ + f: func(t testing.TB) { + mt := t.(*T) + + mt.Run("foo bar", func(t testing.TB) { + t.Log("from first sub-test") + }) + + mt.Run("hello world", func(t testing.TB) { + t.Log("from second sub-test") + }) + }, + }, + want: &T{ + name: "subtests_inherit", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: false, + failed: 0, + subtests: []*T{ + { + name: "subtests_inherit/foo_bar", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: false, + failed: 0, + output: []string{"from first sub-test\n"}, + }, + { + name: "subtests_inherit/hello_world", + abort: true, + baseTempdir: os.TempDir(), + deadline: time.Now().Add(10 * time.Minute), + timeout: false, + output: []string{"from second sub-test\n"}, + }, + }, + subtestNames: map[string]bool{ + "foo_bar": true, + "hello_world": true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{ + name: tt.fields.name, + abort: tt.fields.abort, + baseTempdir: tt.fields.baseTempdir, + testingT: tt.fields.testingT, + deadline: tt.fields.deadline, + timeout: tt.fields.timeout, + } + + runInGoroutine(func() { + tt.args.f(mt) + }) + + assertEqualMocktestingT(t, tt.want, mt) + }) + } +} + +func TestT_Output(t *testing.T) { + type fields struct { + output []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "nil", + fields: fields{}, + want: nil, + }, + { + name: "empty", + fields: fields{output: []string{}}, + want: []string{}, + }, + { + name: "one item", + fields: fields{output: []string{"oops: not found\n"}}, + want: []string{"oops: not found\n"}, + }, + { + name: "multiple items", + fields: fields{output: []string{"oops: not found\n", "bye\n"}}, + want: []string{"oops: not found\n", "bye\n"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{output: tt.fields.output} + + got := mt.Output() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_CleanupFuncs(t *testing.T) { + cleanup1 := func() {} + cleanup2 := func() {} + cleanup3 := func() {} + + type fields struct { + cleanups []func() + } + + tests := []struct { + name string + fields fields + want []func() + }{ + { + name: "nil", + want: nil, + }, + { + name: "empty", + fields: fields{cleanups: []func(){}}, + want: []func(){}, + }, + { + name: "one func", + fields: fields{cleanups: []func(){cleanup1}}, + want: []func(){cleanup1}, + }, + { + name: "many funcs", + fields: fields{cleanups: []func(){cleanup3, cleanup1, cleanup2}}, + want: []func(){cleanup3, cleanup1, cleanup2}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{cleanups: tt.fields.cleanups} + + got := mt.CleanupFuncs() + + switch { + case tt.want == nil: + assert.Nil(t, got) + case len(tt.want) == 0: + assert.NotNil(t, got) + assert.Len(t, got, 0) + default: + var wantFuncs []string + for _, f := range tt.want { + p := reflect.ValueOf(f).Pointer() + wantFuncs = append(wantFuncs, runtime.FuncForPC(p).Name()) + } + var gotFuncs []string + for _, f := range got { + p := reflect.ValueOf(f).Pointer() + gotFuncs = append(gotFuncs, runtime.FuncForPC(p).Name()) + } + + assert.Equal(t, wantFuncs, gotFuncs) + } + }) + } +} + +func TestT_CleanupNames(t *testing.T) { + cleanup1 := func() {} + cleanup2 := func() {} + cleanup3 := func() {} + + type fields struct { + cleanups []func() + } + + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "nil", + want: []string{}, + }, + { + name: "empty", + fields: fields{cleanups: []func(){}}, + want: []string{}, + }, + { + name: "one func", + fields: fields{cleanups: []func(){cleanup1}}, + want: []string{ + "github.com/jimeh/go-mocktesting.TestT_CleanupNames.func1", + }, + }, + { + name: "many funcs", + fields: fields{cleanups: []func(){cleanup3, cleanup1, cleanup2}}, + want: []string{ + "github.com/jimeh/go-mocktesting.TestT_CleanupNames.func3", + "github.com/jimeh/go-mocktesting.TestT_CleanupNames.func1", + "github.com/jimeh/go-mocktesting.TestT_CleanupNames.func2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{cleanups: tt.fields.cleanups} + + got := mt.CleanupNames() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_HelperNames(t *testing.T) { + type fields struct { + helpers []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "nil", + fields: fields{}, + want: nil, + }, + { + name: "empty", + fields: fields{helpers: []string{}}, + want: []string{}, + }, + { + name: "one helper", + fields: fields{ + helpers: []string{ + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func1", + }, + }, + want: []string{ + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func1", + }, + }, + { + name: "multiple helpers", + fields: fields{ + helpers: []string{ + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func2", + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func1", + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func2", + }, + }, + want: []string{ + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func2", + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func1", + "github.com/jimeh/go-mocktesting.TestT_HelperNames.func2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{helpers: tt.fields.helpers} + + got := mt.HelperNames() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Halted(t *testing.T) { + type fields struct { + halted bool + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "not halted", + fields: fields{halted: false}, + want: false, + }, + { + name: "halted", + fields: fields{halted: true}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{aborted: tt.fields.halted} + + got := mt.Aborted() + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestT_Subtests(t *testing.T) { + type fields struct { + subtests []*T + } + tests := []struct { + name string + fields fields + want []*T + }{ + { + name: "nil", + fields: fields{}, + want: []*T{}, + }, + { + name: "empty", + fields: fields{subtests: []*T{}}, + want: []*T{}, + }, + { + name: "one subtest", + fields: fields{ + subtests: []*T{ + {name: "foo_bar"}, + }, + }, + want: []*T{ + {name: "foo_bar"}, + }, + }, + { + name: "multiple subtests", + fields: fields{ + subtests: []*T{ + {name: "foo_bar"}, + {name: "hello"}, + {name: "world"}, + }, + }, + want: []*T{ + {name: "foo_bar"}, + {name: "hello"}, + {name: "world"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := &T{subtests: tt.fields.subtests} + + got := mt.Subtests() + + assert.Equal(t, tt.want, got) + }) + } +}