When I was researching the topic of test fixtures, I couldn’t find much about their beginnings. My first search was about the name of the person who coined “test fixtures”. Unfortunately, that was not a fruitful edeavour. The next logical step was to look for etymology of the phrase “test fixtures”, but the only search result that made sense was a Wikipedia page on the topic.
Judging by the Wiki page, it’s clear that test fixutures as a concept has been heavily popularized by Ruby on Rails. Likely though, folks that have been in the industry for a longer time will say that the idea of test fixutres is older than Rails itself. What I feel is more important in this discussion is putting the historical facts aside and who/what is to blame to the popularization of the concept. Instead, we should focus on understanding the motivation behind it and how we can improve its implementations.
Test fixtures contribute to setting up the system for the testing process by providing it with all the necessary data for initialization. This is done to satisfy any preconditions there may be for the code under test. For example, code that we want to test might require some configuration before it can be executed or tested. This means that every time we have to test such code, we would have to recreate these preconditions to run the code.
More annoyingly, if the configuration of the tested code would change, we would have to update the structure of the configuration everywhere where we test that particular code.
To avoid such scenarios, we use fixtures. Fixtures allow us to reliably and repeatably create the state that our code relies on, without worrying about the details. If the required state for the code under test would change, we need only to tweak a fixture, instead of scouring all of our tests for the code that needs to be changed.
I know, I know. My introduction made you dizzy from all the praise of fixtures. Let’s stop the sales pitch here and move on to see how simple fixtures can be and how you can master them as another tool in your testing toolbelt.
Making a simple gradebook
As always, talking about code without having code to talk about is not great. Let’s introduce an example representing a gradebook that will be populated from a CSV file, using a builder function. After, we will create a lookup method per column and add some tests for both functions.
Record type will have three attributes:
all three of type
Gradebook type is just a slice of
Next, let’s create a builder function for a
Gradebook. We want the function
to be simple - receive a path to a CSV file as argument and return a
Gradebook with all of the records parsed from the CSV.
Although a bit bloated, the function actually doesn’t do much. It receives an
io.Reader as argument (which is the file reader), wraps it in a CSV reader
and reads it line by line. For each line it reads, it will create a new
Record struct and
append it to the collection of
After parsing the whole file it will exit the loop and return the gradebook.
Of course, in true Go fashion, in every step of the reading and parsing the
file we gracefully handle the errors. If in any scenario there’s an error, the
function will return the error along with the empty
The last piece of the puzzle is the function which will find all records in the gradebook for a particular student:
FindByStudent function takes the
student name as argument. Then, it
will loop through all the records of the
Gradebook and will collect all of
the records where the name of the student matches. Lastly, it will return the
records found for the particular
To manually test the code, let’s create a small CSV file, called
Jane,Chemistry,A John,Biology,A Jane,Algebra,B Jane,Biology,A John,Algebra,B John,Chemistry,C
main function of the file we will parse it and then get all of Jane’s
The output of the function will be:
From the output it is clear what are Jane’s grades in the gradebook we have created. Having these two types and two functions is good enough to explain how we can use fixtures in the testing we’re about to do.
Testing the builder function
Whenever we need to test a piece of code, we have to identify what are its key
components. In other words, we have to understand what are the important steps
that that code takes to accomplish its mission. For example, to test the
NewGradebook function an overly simplified breakdown of its doings would look
- Read through each of the lines of the CSV
- When reading through each line, create a new struct from the data
- Put the new struct in the collection of structs
- Return the collection of structs
Now, there’s no need to test if opening a file and parsing it works - we trust
Go to take care of that. There are two things we are interested at: will our
function handle invalid CSV files gracefully, and will it create a
that what we expect from a valid file?
To test the error handling, we will introduce a test function:
To run these test cases, we will need three accompanying CSV files, in the root
of our project:
valid.csv. An empty CSV, an
invalid CSV and a valid CSV file, respectively.
Each of these files are actually fixtures - files that go together with the
test suite of this project, enabling us to assume the state of the system that
we run our tests on. Now, the content of these files should be obvious from the
file names. The
invalid.csv will contain just text, not in a CSV format
empty.csv will be just an empty file, while the
will be a real CSV that our function can parse and use. Lastly, the
nonexisting.csv actually will not be a file – we want our tests to fail when
this path is passed to the
NewGradebook function. And this is the first thing
we need to remember about fixtures: we can (and should) create as many fixture
files as it makes sense, but not more than that.
Fixtures should always be placed in a directory (in our example
the root of our project. In fact, we should always place our fixtures in the
testdata directory at the root of our project because
go test will ignore
that path when building our packages. Quoting the ouput of
go help test:
The go tool will ignore a directory named “testdata”, making it available to hold ancillary data needed by the tests.
Placing it in the root of the directory works great because when we run
go test, for each package in the directory tree, the test binary will be executed
with its working directory set to the source directory of the package under
test. (Read more about it in Dave Cheney’s
article on the topic.)
In the example above, we used two nested directories:
This is because we want to logically group our fixtures and leave the room for
other kind of fixtures within the same project, if need be. Software is built
to grow, so why not set some sane defaults from the start.
The functionality of the
FindByStudent function is a linear search though a
Gradebook type (which is a slice of
Records). It compares the student name
from the argument and the name of each of the records in the
a match is found, the matching record is added to the collection
Testing this function is can be based on couple of state assumptions. The first
one is that to test
FindByStudent we have to have a
Gradebook can be in three states: empty, without a matching
Record that matches the student name from the argument. If we would
flip this on its head, it would mean that to test the function we will need
Gradebooks: one empty, one without a matching
one with a matching
To be able to create such
Gradebooks we can take two different approaches:
Gradebooks directly in the test, or use a fixture file. Using the
first approach might be more preferred by some, but for the purpose of seeing
how we can use fixtures we will use the second approach. While we already have
the fixture files from the previous test, we can use them in the test of the
In this test function we have defined two test cases: the first one uses the
empty.csv fixture, while the other uses
valid.csv fixture. By looking at
the test cases it is clear what we expect to get from each one. When working
with the empty CSV we expect to get an empty gradebook - no grades, no gradebook.
But, when working with the
valid.csv we expect to get a
Gradebook that will
have all of the grades for the student specified in that particular test case,
in this case
The test function does not have any magic. It merely builds a
NewGradebook function and the fixture file. Then, we invoke the
FindByStudent function on the
Gradebook and we make srue that all of the
grades that we got are the ones we expected.
If we run the test, we’ll get an output looking like this:
The tests pass - building the
Gradebooks with the fixtures worked well, so we
range over the test cases and test our expectations.
Tidying up our tests
Looking at both test functions that we wrote, at the beginning of the
blocks we can notice that we have to create a new
Gradebook by using the
NewGradebook builder function. In essence, this is the test setup in these
two test functions - we have to have an instance of the
Gradebook type to run
When we use fixtures the failure to use a fixture can mean that the tests cannot be run - they depend on the fixture files being available and usable. In case where the fixture renders to be unusable, we have to stop the execution of the tests futher and bail out with an error.
For such reasons it is a quick win to extract a test helper that can be used in the test setup. By doing that, we all of the error handling for loading the fixture and test setup can be extracted outside of the tests functions. Let’s create a small function that will do just that:
buildGradebook is simply a wrapper around the call to
with one key difference: if a
Gradebook cannot be produced using
NewGradebook it will actually mark the as failed. This is done using
t.Fatalf, where instead of returning an empty
Gradebook we immediately make
the test fail. In other words: being unable to create a
Gradebook is an
unrecoverable error. A nice sideffect of this is that the caller function of
buildGradebook does not need to handle the error that might be returned from
NewGradebook - that will all be handled by
If we revisit our
TestFindByStudent function now, it will not have changed
much. Still, it will contain the improvements coming from the
If we would remove any of the fixture files, we will see how the test will be
marked as failed due to the
By having another function that takes care of building the
able to offload the complexity of the missing fixtures outside of the tests
themselves. While these concepts are simple, they’re powerful as they lead to
cleaner tests and functions that are local to the test and easy to maintain.
EDIT October 8, 2019: As Andreas Schröpfer
suggested in the comments, it’s more idiomatic Go when the function receives a
io.Reader instead of a file path. I have updated the example code and the
article to reflect that. Thanks Andreas!