/fixture

A golang micro-framework for writing reusable test fixtures on top of package "testing"

Primary LanguageGoApache License 2.0Apache-2.0

fixture

Re-usable test setups und teardowns for go(lang) testing tests.

CI Status Go Report Card Package Doc Releases

fixture implements a micro-framework ontop of the standard library's testing package that allow writing of reusable test setup and teardown code.

Installation

This module uses golang modules and can be installed with

go get github.com/halimath/fixture@main

Usage

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:

lifecycle

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

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

Implementing a fixture

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

Fixtures already provided by fixture

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.

TempDir

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

HTTPServerFixture

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

License

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.