Testing framework

Introduction
Goal of the testing
framework is to provide simple and efficient tools to for
writing effective unit, component, and integration tests in go
.
To accomplish this, the testing
framework provides a couple of extensions for
to standard testing
package of go
that support a simple
setup of gomock
and 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.
Example Usage
The core idea of the mock
/gock
packages is to provide a
short pragmatic domain language for defining mock requests with responses that
enforce validation, while the test
package provides the building
blocks for test isolation.
type UnitParams struct {
mockSetup mock.SetupFunc
input*... *model.*
expect test.Expect
expect*... *model.*
expectError error
}
var testUnitParams = map[string]UnitParams {
"success" {
mockSetup: mock.Chain(
CallMockA(input..., output...),
...
test.Panic("failure message"),
),
...
expect: test.ExpectSuccess
}
}
func TestUnit(t *testing.T) {
test.Map(t, testParams).
Run(func(t test.Test, param UnitParams){
// Given
mocks := mock.NewMock(t).
SetArg("common-arg", local.input*)...
Expect(param.mockSetup)
unit := NewUnitService(
mock.Get(mocks, NewServiceMock),
...
)
// When
result, err := unit.call(param.input*...)
mocks.Wait()
// Then
if param.expectError != nil {
assert.Equal(t, param.expectError, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, param.expect*, result)
})
}
This opinionated test pattern supports a wide range of test in a standardized
way. For variations have a closer look at the 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.
Why parallel tests?
Running tests in parallel make the feedback loop on failures faster, help to
detect failures from concurrent access and race conditions using go test -race
, that else only appear randomly in production, and foster a design with
clear responsibilities. This side-effects compensate for the small additional
effort needed to write parallel tests.
Why isolation of tests?
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
complicated since many tools, patterns, and practices break the test isolation
(see requirements for parallel isolated
tests.
Why strong validation?
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
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
framework makes this approach as simple as possible, but it is
still the responsibility of the developer to setup the validation correctly.
Framework structure
The testing
framework consists of the following sub-packages:
-
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
package - even in if a system under test spans
detached go
-routines.
-
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.
-
gock
provides a drop-in extension for Gock consisting of a
controller and a mock storage that allows to run tests isolated. This allows
to parallelize simple test and parameterized tests.
-
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
to validated the 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
with go test -race
.
Note: there are some general requirements for running test in parallel:
- Tests must not modify environment variables dynamically - utilize test
specific configuration instead.
- Tests must not require reserved service ports and open listeners - setup
services to acquire dynamic ports instead.
- Tests must not share files, folder and pipelines, e.g.
stdin
, stdout
,
or stderr
- implement logic by using wrappers that can be redirected and
mocked.
- Tests must not share database schemas or tables, that are updated during
execution of parallel tests - implement test to setup test specific database
schemas.
- Tests must not share process resources, that are update during execution
of parallel tests. Many frameworks make use of common global resources that
make them unsuitable for parallel tests.
Examples for such shared resources in common frameworks are:
- Using of monkey patching to modify commonly used global functions,
e.g.
time.Now()
- implement access to these global functions using lambdas
and interfaces to allow for mocking.
- Using of
gock
to mock HTTP responses on transport level - make use
of the gock
-controller provided by this framework.
- Using the 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
that supports a service specific configuration.
With a careful design the general pattern provided above can be used to support
parallel test execution.
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.
Building
This project is using go-make, which provides default targets for
most common tasks, to initialize, build, test, and run the software of this
project. Read the go-make manual for more information about
targets and configuration options.
Not: go-make automatically installs pre-commit
and commit-msg
hooks overwriting and deleting pre-existing hooks (see also
Customizing Git - Git Hooks). The pre-commit
hook calls
make commit
as an alias for executing test-go
, test-unit
, lint-<level>
,
and lint-markdown
to enforce successful testing and linting. The commit-msg
hook calls make git-verify message
for validating whether the commit message
is following the conventional commit best practice.
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.