Golang Testing with TDD
By: William Infante
Tags: Golang, Test Driven Development, Go, Tdd, Testify

Coverage and Refactoring in Go Tests

For the next function, we’d like to test and create the function OddOrEven() that accepts an integer and tells if it is an odd or even integer.

Initially, we wanted to check if the function accepts an integer like 45 , the function returns the string "45 is an odd number" :

Like the first function we had, we’ll create a function with the same signature in starter.go to remove the syntax error and run the test:

A quick run of go test will provide the error:

To minimally satisfy the test requirement, we probably can just pass a string "45 is an odd number" but let’s make it more dynamic and use the input.

This would make the test pass. But if we have another test item for an even number,

the test would fail:

We can use existing functions like the modulo math.Mod() and just need a bit of conversion since math.Mod() requires float64 as an input. We can see the following results to a modified function:

It satisfies the previous two items ( 45 and 42 ), but can it satisfy 0 ?

It does! Thinking about the test case, we also have negative values in integers. So, let’s try negative numbers this time like -45 .

Noticed that the test failed when we thought we almost had it. For this module, return value is -1 instead of 1 . So you can already see some parallelism to real-life use cases that could make your supposedly-very-reliable function to still potentially fail with some edge cases. So, we modified the test to:

We’ll also test that for a negative even number like -42 and it works.

Subtests and refactoring

When we’re testing a function, there may be instances where we want to group the test items into meaningful subcategories. For example, for a more complicated function, we may want to have a subtest for the happy path, another subtest for an edge-case and maybe another subtest for error-handling.

In our example, we may want to create subgroups for non-negative cases and negative cases. Golang still allows us to do these subtests and it’s also a good way to reuse input values.

This also makes it easier to read the test cases. In our example, we don’t have common values that we can refactor for the simple usecase, but we can group the test items to easily see that the created function can work for both non-negative and negative integers like so:

It’s also easy to understand and see a summary of the test when we run go test -v as they’re grouped together.

Coverage

Speaking of creating a summary for the test, it’s also important to ensure that the conditions we mentioned the function that will actually be tested. A coverage to the desired output like coverage.out is simply:

go test -v -coverprofile coverage.out

We can also create an html formatted document by running a command similar to:

go tool cover -html=coverage.out -o coverage.html

Currently, we have 100% coverage!

Suppose that we did not check the even integers and only checked the odd integers, we may only get a coverage of 80% (if we include the SayHello() testing).

Just note that a line coverage of 100% does not automatically mean there are no additional tests needed. If we removed all the negative subtests, the coverage would still show 100% coverage but we have not tested the case when math.Mod(float64(num), 2) == -1 . So yes, a 100% coverage is definitely a good goal but there’s more to line coverage that we’ll need.

Because we have followed the TDD approach, coverage will not be much of an issue for us! We are making sure that we are “covering” all the processes in our function since we created the tests first even before proceeding to create the function. We’re building the function incrementally while achieving almost 100% coverage in each step. Even during the times where we partially created the test, we have almost 100% coverage! Who wouldn’t want that, right? This is just one of the ways that we can benefit from TDD. Oh, and I also like that the tests using the TDD approach create a handy and automatically updated documentation for that package/module.

Advanced Testing Modules

For the third function in starter.go , I intentionally did not follow the TDD approach as my main purpose here is to demonstrate how we can leverage advanced testing modules.

Now for example, we just wanted to quickly create a health check if a page could be loaded. For us to easily accomplish this task, we can make use of the http package in golang with import "net/http" . We can simply create a health check like:

Now, how do we plan to check this http call? We can leverage the included testing library using import "net/http/httptest" . It’s not just important to do the line coverage here but to simulate the http behaviour. We can create our mock writer and request using httptest.NewRequest() and httptest.NewRecorder() . And after running our CheckHealth() function, we should expect that the body contains the "health check passed" like what has been written in the CheckHealth() function.