Merge pull request #1 from jimeh/initial-implementation

This commit is contained in:
2021-11-22 00:01:17 +00:00
committed by GitHub
15 changed files with 3912 additions and 1 deletions

87
.github/workflows/ci.yml vendored Normal file
View File

@@ -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 ./...

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/*
coverage.out

96
.golangci.yml Normal file
View File

@@ -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

20
LICENSE Normal file
View File

@@ -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.

205
Makefile Normal file
View File

@@ -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))

146
README.md
View File

@@ -1 +1,145 @@
# mocktesting
<h1 align="center">
go-mocktesting
</h1>
<p align="center">
<strong>
Mock *testing.T for the purpose of testing test helpers.
</strong>
</p>
<p align="center">
<a href="https://pkg.go.dev/github.com/jimeh/go-mocktesting"><img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" alt="Go Reference"></a>
<a href="https://github.com/jimeh/go-mocktesting/actions"><img src="https://img.shields.io/github/workflow/status/jimeh/go-mocktesting/CI.svg?logo=github" alt="Actions Status"></a>
<a href="https://codeclimate.com/github/jimeh/go-mocktesting"><img src="https://img.shields.io/codeclimate/coverage/jimeh/go-mocktesting.svg?logo=code%20climate" alt="Coverage"></a>
<a href="https://github.com/jimeh/go-mocktesting/issues"><img src="https://img.shields.io/github/issues-raw/jimeh/go-mocktesting.svg?style=flat&logo=github&logoColor=white" alt="GitHub issues"></a>
<a href="https://github.com/jimeh/go-mocktesting/pulls"><img src="https://img.shields.io/github/issues-pr-raw/jimeh/go-mocktesting.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests"></a>
<a href="https://github.com/jimeh/go-mocktesting/blob/master/LICENSE"><img src="https://img.shields.io/github/license/jimeh/go-mocktesting.svg?style=flat" alt="License Status"></a>
</p>
## 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)

189
example_test.go Normal file
View File

@@ -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
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/jimeh/go-mocktesting
go 1.15
require github.com/stretchr/testify v1.7.0

11
go.sum Normal file
View File

@@ -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=

23
mocktesting.go Normal file
View File

@@ -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()
}

30
mocktesting_test.go Normal file
View File

@@ -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
}

426
t.go Normal file
View File

@@ -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
}

31
t_go116.go Normal file
View File

@@ -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
}

182
t_go116_test.go Normal file
View File

@@ -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)
})
}
}

2460
t_test.go Normal file

File diff suppressed because it is too large Load Diff