Re-usable test setups und teardowns for go(lang) testing
tests.
fixture
implements a micro-framework ontop of the standard library's
testing
package that allow writing of reusable test setup and teardown code.
This module uses golang modules and can be installed with
go get github.com/halimath/fixture@main
fixture
defines a very simple (and assumingly well-known) lifecycle for
code to execute before, inbetween and after tests. A fixture may hook into
this lifecycle to setup or teardown resources needed by the tests. As multiple
tests may share some amount of these resources fixture
provides a simple
test suite functionality that plays well with the resource initialization.
The lifecycle is shown in the following picture:
A fixture (in terms of this package) is any go value. A fixture may satisfy
a couple of additional interfaces to execute code at the given lifecycle
phases. The interfaces are named after the lifecycle phases. Each interface
contains a single method (named after the interface) that receives the
*testing.T
and returns an error
which will abort the test (calling
t.Fatal
).
Using a fixture is done using the With
function, which starts a new test
suite. Calling Run
registers a test to run using this fixture.
With(t, new(myFixture)).
Run("test 1", func(t *testing.T, f *myFixture) {
// Test code
}).
Run("test 2", func(t *testing.T, f *myFixture) {
// Test code
})
To implement a fixture simply create a type to hold all the values your fixture will provide. You can also add receiver functions to ease interaction with the fixture. Then, implement the desired hook interfaces.
Typically, a fixture implements the hook methods via a pointer receiver. This
allows using just new
to create a fixture. Use either BeforeAll
or
BeforeEach
to initialize the code.
The following example uses a fixture to spawn a httptest.Server
with a simple
handler (in a real world the handle would have been some real production code).
It provides a sendRequest
method to send a simple request, handle errors by
failing the test and returns the http.Response
.
The TestExample
executes two tests both using the same running server.
// A simple test fixture holding a httptest.Server.
type httpServerFixture struct {
srv *httptest.Server
}
// BeforeAll hooks into the fixture lifecycle and creates and starts the
// httptest.Server before the first test is executed.
func (f *httpServerFixture) BeforeAll(t *testing.T) error {
f.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Tracing-Id", "1")
w.WriteHeader(http.StatusNoContent)
}))
return nil
}
// AfterAll hooks into the fixture lifecycle and disposes the httptest.Server
// after the last test has been executed.
func (f *httpServerFixture) AfterAll(t *testing.T) error {
f.srv.Close()
return nil
}
// sendRequest is a convenience function making it easier to read the test code.
func (f *httpServerFixture) sendRequest(t *testing.T) *http.Response {
r, err := http.Get(f.srv.URL)
if err != nil {
t.Fatal(err)
}
return r
}
func TestExample(t *testing.T) {
fixture.With(t, new(httpServerFixture)).
Run("http status code", func(t *testing.T, f *httpServerFixture) {
got := f.sendRequest(t).StatusCode
if got != http.StatusNoContent {
t.Errorf("expected %d but got %d", http.StatusNoContent, got)
}
}).
Run("tracing header", func(t *testing.T, f *httpServerFixture) {
resp := f.sendRequest(t)
got := resp.Header.Get("X-Tracing-Id")
if got != "1" {
t.Errorf("expected %q but got %q", "1", got)
}
})
}
fixture
contains some ready to use generic fixtures. All these fixtures
have dependencies only to the standard library and cause no external module to
be required.
Creating and removing a temporary directory for filesystem related tests is
easy with the TempDirFixture
and the TempDir
function.
With(t, TempDir("someprefix")).
Run("create file", func(t *testing.T, d *TempDirFixture) {
f, err := os.Create(d.Join("test"))
if err != nil {
t.Fatal(err)
}
defer f.Close()
}).
Run("expect file", func(t *testing.T, d *TempDirFixture) {
_, err := os.Stat(d.Join("test"))
if err != nil {
t.Error(err)
}
})
The HTTPServerFixture
creates a HTTP server using httptest.NewServer
which
will be started on BeforeAll
and closed on AfterAll
. The server uses a
http.ServerMux
as its handler and handler functions can be registered at any
stage. The server uses HTTP/2 but no TLS; both can be changed easily.
f := new(HTTPServerFixture)
f.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
With(t, f).
Run("/", func(t *testing.T, f *HTTPServerFixture) {
res, err := http.Get(f.URL())
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Errorf("expected 200 but got %d", res.StatusCode)
}
})
Copyright 2022 Alexander Metzner.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.