By: Jason Gerard, Sr Software Architect on December 23, 2021
Part of moving quickly with software is having a safety net. At randrr, that safety net consists of several layers of test coverage. Following Mike Cohn's Test Pyramid, the foundation of our strategy is automated unit testing.
I know many developers are groaning at this. The common refrains are that "it takes too long" or that "it's unnecessary." Hopefully I can convince you that it's extremely quick and easy (at least using Go), and that it's absolutely necessary.
TDD Not Required
Just a quick note about Test-Driven Development (TDD). In my experience, it typically doesn't matter if you write code first or tests first, as long as the result is the same. My personal process is to rough out some code, write some tests, then refine the code and add more tests. And remember, unit tests are for testing logic, not integrations.
Unlike most languages, Go has unit testing built-in. Running unit tests in code is as simple as:
To follow along with this article, clone the repo at github. For the purposes of this article, I will assume your projects are run from the following path:
The code in this repo is for a web service that returns sunset data for a given location. Running
go test from the the root directory of your project produces the following output:
Taking a closer look at the project you will notice the following types:
All sunset queries will be handled by a type that implements the
sunsetFinder interface. Anytime you have code that will talk to a system outside of the one you're in, consider an interface. It allows you to abstract away all the implementation and provide test mocks. If done properly, it can protect the rest of your code from changes in the underlying implementation.
handler.go you will see one function as follows:
This function takes in a
sunsetFinder dependency and returns an
http.Handler to be used for our search route. This function has the logic we care about testing such as input validation and error handling. We'll focus our unit tests on this method.
To create a test file in Go, simply add a file with a
_test.go suffix such as
The first view things you'll see in this file are the
MockSunsetFinder and our first test.
Having a mock object that has a
func we can assign to provide the implementation of our interface is a good technique to make it easy to test specific functionality.
Our first test doesn't use our mock but it illustrates some of the nice built-in features of testing in Go.
Go has a package,
httptest that provides lots of helpers for testing http handlers without running a web server. Here we use an
http.ResponseRecorder that implements
One criticism of Go's built-in testing is that it deviates from all the xUnit style test frameworks by not having assertions. Essentially testing in Go is no different than regular Go code. You check for error conditions and "raise" an error instead of asserting a condition is met or not.
Let's checkout another test.
Here we use our mock by simply creating an instance and providing a basic function that will always return a bogus result and an
The final test checks for success.
This test guarantees that our success response always conforms to the
Go also has built in support for code coverage. Run the following:
cover flag tells the Go tool to instrument the code and measure coverage. 34.9% is not great. What do we not have under test? Let's find out.
The first command generates a report of missing coverage and the second command gives a nice HTML report like below.
Here you can see we missed a test for the HTTP method. You can also toggle between files with the drop down at the top. Let's add that missing test.
handler.go is now out 100% coverage but our overall coverage is now 39.5%. We didn't write unit tests for
yahoo.go and we shouldn't necessarily.
main.go simply wires up the handler to the Yahoo implementation and begins listening on port 8080. This will be tested every time the app is ran by either working or not. Likewise,
yahoo.go is our Yahoo API specific implementation. At the very least, it requires an internet connection and Yahoo to be up. This is a job for integration testing. A topic we'll cover in a future post.
So, let's simply exclude these files from our test metrics. We'll add the following build tag:
//+build !test (a new-line is required afterwards). Now run the following:
And there we have it, 100% unit test coverage of our "business logic." I will leave it as an excercise for the reader to refactor the
YahooSunsetFinder to use a mock to simulate HTTP requests and verify logic and error handling in that type.
Now, let's run this thing.
Hit it with curl.
As you can see from this simple example, unit testing in Go is extremely easy. Go's implicit interface implementation makes it easy to make mocks and causes designs to tend toward small, focused components. Testing is a vital part of software engineering. "It works on my machine" is not a valid response. If you take your code seriously, you'll be able to prove that it works all the time.