Skip to content

Commit

Permalink
refactor: basic test concepts to introduce test timeout (#93)
Browse files Browse the repository at this point in the history
Signed-off-by: Tronje Krop <tronje.krop@zalando.de>
  • Loading branch information
tkrop authored Oct 19, 2024
1 parent 9f2193c commit 428d035
Show file tree
Hide file tree
Showing 29 changed files with 1,838 additions and 1,528 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export GOPATH ?= $(shell $(GO) env GOPATH)
export GOBIN ?= $(GOPATH)/bin

# Setup go-make version to use desired build and config scripts.
GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.105
GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.106
INSTALL_FLAGS ?= -mod=readonly -buildvcs=auto
# Request targets from go-make targets target.
TARGETS := $(shell command -v $(GOBIN)/go-make >/dev/null || \
Expand Down
89 changes: 45 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ writing effective unit, component, and integration tests in [`go`][go].

To accomplish this, the `testing` framework provides a couple of extensions for
to standard [`testing`][testing] package of [`go`][go] that support a simple
setup of [`gomock`][gomock] and [`gock`][gock] in isolated, parallel, and
parameterized tests using a common pattern to setup with strong validation of
mock request and response that work under various failure scenarios and even in
the presense of [`go`-routines][go-routines].
setup of test cases using [`gomock`][gomock] and [`gock`][gock] in isolated,
parallel, and parameterized tests using a common pattern with strong validation
of mock request and response that work under various failure scenarios and even
in the presence of spawned [`go`-routines][go-routines].

[go-routines]: <https://go.dev/tour/concurrency>

Expand Down Expand Up @@ -90,6 +90,7 @@ var testUnitParams = map[string]UnitParams {

func TestUnit(t *testing.T) {
test.Map(t, testParams).
Timeout(50 * time.Millisecond)
Run(func(t test.Test, param UnitParams){

// Given
Expand Down Expand Up @@ -124,11 +125,10 @@ way. For variations have a closer look at the [test](test) package.

### Why parameterized test?

Parameterized test are an efficient way to setup a high number of related test
cases cover the system under test in a black box mode from feature perspective.
With the right tools and concepts - as provided by this `testing` framework,
parameterized test allow to cover all success and failure paths of a system
under test as outlined above.
Parameterized test are an effective way to set up a systematic set of test
cases covering a system under test in a black box mode. With the right tools
and concepts — as provided by this `testing` framework, parameterized test
allow to cover all success and failure paths of a system under test.


### Why parallel tests?
Expand All @@ -142,7 +142,7 @@ effort needed to write parallel tests.

### Why isolation of tests?

Test isolation is a precondition to have stable running test - especially run
Test isolation is a precondition to have stable running test especially run
in parallel. Isolation must happen from input perspective, i.e. the outcome of
a test must not be affected by any previous running test, but also from output
perspective, i.e. it must not affect any later running test. This is often
Expand All @@ -155,58 +155,60 @@ tests](#requirements-for-parallel-isolated-tests).

Test are only meaningful, if they validate ensure pre-conditions and validate
post-conditions sufficiently strict. Without validation test cannot ensure that
the system under test behaves as expected - even with 100% code and branch
the system under test behaves as expected even with 100% code and branch
coverage. As a consequence, a system may fail in unexpected ways in production.

Thus it is advised to validate mock input parameters for mocked requests and
to carefully define the order of mock requests and responses. The
[`mock`](mock) framework makes this approach as simple as possible, but it is
still the responsibility of the developer to setup the validation correctly.
Thus, it is advised to validate input parameters for mocked requests and to
carefully define the order of mock requests and responses. The [`mock`](mock)
framework makes this approach as simple as possible, but it is still the
responsibility of the test developer to set up the validation correctly.


## Framework structure

The `testing` framework consists of the following sub-packages:

* [`test`](test) provides a small framework to simply isolate the test execution
and safely check whether a test fails or succeeds as expected in coordination
with the [`mock`](mock) package - even in if a system under test spans
detached [`go`-routines][go-routines].
* [`test`](test) provides a small framework to isolate the test execution and
safely check whether a test fails or succeeds as expected in combination with
the [`mock`](mock) package even in if a system under test spans detached
[`go`-routines][go-routines].

* [`mock`](mock) provides the means to setup a simple chain or a complex network
of expected mock calls with minimal effort. This makes it easy to extend the
usual narrow range of mocking to larger components using a unified pattern.
* [`mock`](mock) provides the means to set up a simple chain as well as a
complex network of expected mock calls with minimal effort. This makes it
easy to extend the usual narrow range of mocking to larger components using
a unified test pattern.

* [`gock`](gock) provides a drop-in extension for [Gock][gock] consisting of a
controller and a mock storage that allows to run tests isolated. This allows
to parallelize simple test and parameterized tests.
* [`gock`](gock) provides a drop-in extension for the [Gock][gock] package
consisting of a controller and a mock storage that allows running tests
isolated. This allows parallelizing simple test as well as parameterized
tests.

* [`perm`](perm) provides a small framework to simplify permutation tests, i.e.
a consistent test set where conditions can be checked in all known orders
with different outcome. This is very handy in combination with [`test`](test)
to validated the [`mock`](mock) framework, but may be useful in other cases
with different outcome. This was very handy in combination with [`test`](test)
for validating the [`mock`](mock) framework, but may be useful in other cases
too.

Please see the documentation of the sub-packages for more details.


## Requirements for parallel isolated tests

Running tests in parallel not only makes test faster, but also helps to detect
race conditions that else randomly appear in production when running tests
Running tests in parallel makes test not only faster, but also helps to detect
race conditions that else randomly appear in production, when running tests
with `go test -race`.

**Note:** there are some general requirements for running test in parallel:

1. Tests *must not modify* environment variables dynamically - utilize test
1. Tests *must not modify* environment variables dynamically utilize test
specific configuration instead.
2. Tests *must not require* reserved service ports and open listeners - setup
2. Tests *must not require* reserved service ports and open listeners setup
services to acquire dynamic ports instead.
3. Tests *must not share* files, folder and pipelines, e.g. `stdin`, `stdout`,
or `stderr` - implement logic by using wrappers that can be redirected and
or `stderr` implement logic by using wrappers that can be redirected and
mocked.
4. Tests *must not share* database schemas or tables, that are updated during
execution of parallel tests - implement test to setup test specific database
execution of parallel tests implement test to set up test specific database
schemas.
5. Tests *must not share* process resources, that are update during execution
of parallel tests. Many frameworks make use of common global resources that
Expand All @@ -215,17 +217,17 @@ with `go test -race`.
Examples for such shared resources in common frameworks are:

* Using of [monkey patching][monkey] to modify commonly used global functions,
e.g. `time.Now()` - implement access to these global functions using lambdas
e.g. `time.Now()` implement access to these global functions using lambdas
and interfaces to allow for mocking.
* Using of [`gock`][gock] to mock HTTP responses on transport level - make use
* Using of [`gock`][gock] to mock HTTP responses on transport level make use
of the [`gock`](gock)-controller provided by this framework.
* Using the [Gin][gin] HTTP web framework which uses a common `json`-parser
setup instead of a service specific configuration. While this is not a huge
deal, the repeated global setup creates race alerts. Instead use [`chi`][chi]
that supports a service specific configuration.
deal, the repeated global setup creates race alerts. Instead, use
[`chi`][chi] that supports a service specific configuration.

With a careful design the general pattern provided above can be used to support
parallel test execution.
With a careful system design, the general pattern provided above can be used
to create parallel test for a wide range of situations.


## Building
Expand Down Expand Up @@ -272,17 +274,16 @@ is following the [conventional commit][convent-commit] best practice.

## Terms of Usage

This software is open source as is under the MIT license. If you start using
the software, please give it a star, so that I know to be more careful with
changes. If this project has more than 25 Stars, I will introduce semantic
versions for changes.
This software is open source under the MIT license. You can use it without
restrictions and liabilities. Please give it a star, so that I know. If the
project has more than 25 Stars, I will introduce semantic versions `v1`.


## Contributing

If you like to contribute, please create an issue and/or pull request with a
proper description of your proposal or contribution. I will review it and
provide feedback on it.
provide feedback on it as fast as possible.


[testing]: <https://pkg.go.dev/testing>
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ require (
github.com/h2non/gock v1.2.0
github.com/huandu/go-clone v1.6.0
github.com/stretchr/testify v1.9.0
golang.org/x/text v0.13.0
golang.org/x/text v0.18.0
golang.org/x/tools v0.26.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
Expand Down
13 changes: 8 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
Expand All @@ -22,10 +23,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
Expand All @@ -51,8 +54,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
Expand Down
20 changes: 15 additions & 5 deletions internal/mock/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ const (
dirTop = "../../.."
dirMock = "mock"

dirTest = "./test"
dirSubTest = "./test"
dirOther = "../other"
dirUnknown = "../unknown"
dirTesting = "../../test"
dirTest = "../../test"

pathMock = "github.com/tkrop/go-testing/internal/mock"
pathTest = "github.com/tkrop/go-testing/internal/mock/test"
Expand All @@ -43,7 +43,7 @@ const (
fileOther = "mock_other_test.go"
fileTemplate = "mock_template_test.go"
fileUnknown = "unnkown_test.go"
fileTesting = "testing.go"
fileContext = "context.go"

aliasMock = "mock_" + pkgTest
aliasInt = "internal_" + aliasMock
Expand All @@ -60,6 +60,8 @@ const (
var (
errAny = errors.New("any error")

absUnknown, _ = filepath.Abs(dirUnknown)

nameIFace = &Type{Name: iface}
nameIFaceMock = &Type{Name: ifaceMock}

Expand Down Expand Up @@ -126,7 +128,7 @@ func methodsMockIFaceFunc(mocktest, test, mock string) []*Method {
}, {
Name: "CallC",
Params: []*Param{{
Name: "test", Type: aliasType(test, "Tester"),
Name: "test", Type: aliasType(test, "Context"),
}},
Results: []*Param{},
Variadic: false,
Expand All @@ -136,7 +138,7 @@ func methodsMockIFaceFunc(mocktest, test, mock string) []*Method {
var (
// Use two different singleton loaders.
loaderMock = NewLoader(DirDefault)
loaderTest = NewLoader(dirTest)
loaderTest = NewLoader(dirSubTest)
loaderFail = NewLoader(dirUnknown)

// Use singleton template for testing.
Expand Down Expand Up @@ -170,6 +172,14 @@ var (
pathTest, pathTesting, pathMock)

methodsTestTest = []*Method{{
Name: "Deadline",
Params: []*Param{},
Results: []*Param{
{Name: "deadline", Type: "time.Time"},
{Name: "ok", Type: "bool"},
},
Variadic: false,
}, {
Name: "Errorf",
Params: []*Param{
{Name: "format", Type: "string"},
Expand Down
12 changes: 9 additions & 3 deletions internal/mock/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/packages"

"github.com/tkrop/go-testing/test"

Expand All @@ -36,7 +37,7 @@ var (
return dir
}()

fileFailure = filepath.Join(testDirGenerate, dirTest, fileUnknown)
fileFailure = filepath.Join(testDirGenerate, dirSubTest, fileUnknown)
)

var testGenerateParams = map[string]GenerateParams{
Expand All @@ -49,8 +50,13 @@ var testGenerateParams = map[string]GenerateParams{
"failure parsing": {
file: filepath.Join(testDirGenerate, MockFileDefault),
args: []string{pathUnknown},
expectStderr: "argument invalid [pos: 3, arg: " + pathUnknown +
"]: not found\n",
expectStderr: NewErrArgFailure(3, ".",
NewErrPackageParsing(pathUnknown, []*packages.Package{
{Errors: []packages.Error{{
Msg: "no required module provides package " + pathUnknown +
"; to add it:\n\tgo get " + pathUnknown,
}}},
})).Error() + "\n",
expectCode: 1,
},

Expand Down
4 changes: 2 additions & 2 deletions internal/mock/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ var testLoaderLoadParams = map[string]LoaderLoadParams{
"failure loading": {
loader: loaderFail,
source: targetTest.With(&Type{
File: filepath.Join(dirUp, dirMock, dirTest, fileIFace),
File: filepath.Join(dirUp, dirMock, dirSubTest, fileIFace),
}),
expectError: NewErrLoading(pathTest, fmt.Errorf(
"err: chdir %s: no such file or directory: stderr: ",
Expand Down Expand Up @@ -315,7 +315,7 @@ var testLoaderIFacesParams = map[string]LoaderIFacesParams{
"failure loading": {
loader: loaderFail,
source: targetTest.With(&Type{
File: filepath.Join(dirUp, dirMock, dirTest, fileIFace),
File: filepath.Join(dirUp, dirMock, dirSubTest, fileIFace),
}),
expectError: NewErrLoading(pathTest, fmt.Errorf(
"err: chdir %s: no such file or directory: stderr: ",
Expand Down
4 changes: 2 additions & 2 deletions internal/mock/mock_iface_test.gox
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ func (mr *MockIFaceRecorder) CallB() *gomock.Call {
}

// CallC is the mock method to capture a coresponding call.
func (m *MockIFace) CallC(test testing_test.Tester) {
func (m *MockIFace) CallC(test testing_test.Context) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "CallC", test)
}

// CallC is the recorder method to indicates an expected call.
func (mr *MockIFaceRecorder) CallC(test testing_test.Tester) *gomock.Call {
func (mr *MockIFaceRecorder) CallC(test testing_test.Context) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallC",
reflect.TypeOf((*MockIFace)(nil).CallC), test)
Expand Down
Loading

0 comments on commit 428d035

Please sign in to comment.