I’ve been writing Go tests using the Ginkgo BDD test library, and its preferred matcher library Gomega for almost four years as of writing. In that time I’ve discovered some recurring practices in the projects that I’ve joined, and in those I’ve started, which hopefully may be of use to you. I hesitate to refer to these as “Best Practices”, instead titling this post “Effective”, because although these work for me in most cases, they may not work for everyone in all cases.
Occasionally I’m going to link to codebases I’ve worked on recently that follows some of these practices, some of the time (hey, I said these weren’t best practices all the time!) but if there’s anything unclear, please leave a comment!
It’s expected that you have some familiarity with these libraries (or others like it), to best understand these patterns. So, without further ado:
One JustBeforeEach per test hierarchy
Consider having one
JustBeforeEach at the top level performing some shared action for your test hierarchy (typically bounded to a file, unless you have suite level setup and teardown).
For example, if you are testing a CLI command, have a
JustBeforeEach that runs some set of arguments that can be modified by other
BeforeEach blocks. You can find an example of this pattern in the ism codebase but here’s an inline snippet (pseudocode) demonstrating how you might use this pattern for testing a binary and its arguments:
Notice how the arguments to the binary are declared at the beginning and then modified throughout the tree structure in
BeforeEach blocks. As a result, we only had to run the command once in the
JustBeforeEach block. The alternative to this model generally is to run the command in each
It as the first line butI prefer
JustBeforeEach to reduce the visual noise, and to keep only assertions inside my
Caution: Don’t get clever and keep your hierarchy simple.
JustBeforeEaches break the descending tree structure and make reasoning harder so avoid having more than one.
BeforeEach should act out the description of its closest Context or Describe
Context and descriptions immediately being followed by a
BeforeEach that takes the relevant action. This allows readers to very easily understand exactly how state is being modified throughout the test hierarchy. Let’s look back at a snippet from the example above and notice how each
BeforeEach takes exactly one action that corresponds to the text description above it:
This pattern holds true for more complicated setup as well. Looking into the
ism codebase, here is a description stating that a broker will be registered (don’t worry, you don’t need to understand what a broker is), which is immediately followed by a helper function that takes registers a broker. Such clarity!
If you find a
BeforeEach doing something that isn’t in a description, consider whether the tests should be structured in some other hierarchy.
Helper functions over expectations in setup and teardown
Instead of setup and teardown directly making assertions, prefer helper functions that use
ExpectWithOffset. Take a look again at the example from the previous section where a broker is being registered, then let’s look at the code that function is encapsulating — I definitely don’t want to see that code everywhere we register a broker. In this case, the authors have decided not to use the variant
WithOffset which will result in Ginkgo printing a failure stack trace that is off by one (see the tradeoff below for why this might be acceptable).
The utility of expectations failing in setup and teardown is useful, but the code can get visually busy very quickly. Most of the time when reading a test you want to quickly parse intent through a well chosen name. It can be easy to allow these helper functions to increase in complexity as you just need a little extra behaviour, but this will cause you pain; ensure that your helper functions are extremely narrow in their scope, acting as thin wrappers that assert correctness e.g:
ExpectWithOffset couples your code to its calling hierarchy due to the stack frame unwinding. Be very careful if you are going to refactor around the helpers that offsets are correct.
Randomise and parallelise from the start
Prefer starting your projects with full randomisation and parallelisation on in Ginkgo via the
-p flags. The cost of hunting test flakes caused by turning these on later can be huge.
See Ed King’s great blog post “The Night is Dark and Full of Flakes” for our experience on the Cloud Foundry Garden team after not following this advice.
Consider the contract Eventually and Consistently place upon your implementation
Consistently are incredibly useful functions that provide the ability to assert that some state will exist within some timeout (defaulted to one second).
These utilities are extremely helpful but they are also placing a hidden constraint on your code (that some action should be time bounded) and may hide other issues such as deadlocks. If you find yourself bumping timeouts to appease these functions, consider whether you should be using something else.
On one project, after experiencing one too many failures that we were unable to debug, we remove the timeout altogether and wrote a tool called
slowmobius that watched over all our test runs and notified us of any that were taking an unusually long time to complete. This allowed us to better distinguish environmental slowness from hangs, and gave us the ability to jump in and debug.
Use goroutines in tests with care
This one is quite simple, be careful with goroutines in your tests, they don’t play well with Ginkgo’s failure model, and if they aren’t carefully managed they can leak between tests.
Dot import the testing libraries
Dot imports in Go pollute your namespace with public functions and types from the imported package — typically this is a big no-no! I suggest dot importing
gomega and its friends
ghttp. However, if you want to see:
Be my guest.
Don’t even think about using the Omega symbol
When Gomega was first created, you could use Ω instead of
This still lurks around in tests we haven’t got round to cleaning up yet. This should be self-explanatory. Don’t do it. Seriously.
Have you got any more effective practices? Do you disagree with any of these? Leave a comment!
If you’re interested in reading more, check out my next post on common Ginkgo and Gomega gotchas.
Thanks for reading!