How to Achieve 100% Code Coverage in Golang Unit Test?

Kelvin Benzali
CodeX
Published in
6 min readFeb 23, 2022

I know some of you are here because you are struggling to reach that 100% coverage. I know that feeling because I experienced it myself. But before we discuss how we are going to do it, we need to understand the purpose of the unit test. The unit test serves as the first line of defense on program testing. The goal is to ensure the correctness of every individual part of the code. Therefore, a reliable unit test translates to a reliable function.

Mindset is important when creating unit tests. Pursuing coverage alone does not ensure a good unit test. It is about creating a reliable function that properly tests 100% of its code.

However, achieving 100% code coverage on a unit test is tricky and difficult. You could achieve this if and only if you understand the code itself. Once you understand the code, you might realize that the code does not support a full-coverage unit test and needs refactoring. In this article, we will try to discuss the proper way to implement unit tests and achieve 100% coverage.

Interface Type is Essential

A truly unit test is only testing a unit or individual function. Hence, the unit test must not allow its function to communicate with other functions or resources. This can be done by mocking the function’s dependencies.

The flow on the left is the original code. The flow on the right is the unit test. Notice that the unit test flow removes the dependency to the database by using a mock response as a replacement.

How do we mock the function’s dependencies? The interface type comes to the rescue. In this case, we can make the function’s dependency become interface type. Therefore, we can do dependency injection with our mock then the function will call the mock rather than the actual one.

The interface allows us to freely use any struct that implements all the functions in that interface. Let that struct be a mock.

The interface is the main key to achieving 100% code coverage. The unit test can run every possible use case by mocking all the responses of the external functions. Hence, proving the function’s correctness effectively. For example, suppose we have a retail service and warehouse service as below.

Under normal circumstances, the initialization of the service should be:

myWarehouse := warehouse.New(db)
myRetail := retail.New(myWarehouse)
// Get my products
products, err := myRetail.GetMyProduct()

However, the retail function cannot be executed on the unit test since it depends on the warehouse that is using the database. It is because the unit test should not use the real database or other external dependencies. Therefore, we need to create a warehouse mock that implements all warehouseItf functions to mock the response from warehouse service in the unit test.

You might notice this method of creating the mock for unit test is too limited. The mock response is fixed or not flexible enough to cover every use case in the unit test. This can be solved by creating a multi purpose mock that can be created and assign a response for every test case. A very flexible mock. Fortunately, you do not need to create that from scratch since there are a lot of mocking tools for Golang out there already.

Mocking Frameworks

There are several ways to mock services to complement Golang testing. You can create your own mock from the scratch or use an external mocking framework for flexibility and efficiency. Some of these frameworks are:

  1. Gomock. It is the most common and popular mocking framework to mock interfaces. You can read more details in the doc.
  2. Go Sqlmock. DATA-DOG sqlmock is one of the common frameworks to mock Golang’s SQL. You can read more details in the doc.
  3. Testify. This library is also a great alternative to mock objects and HTTP activities. I use this library for its great assertion tool. You can read more details in the doc.

These libraries are my preferences. There is a lot of assertion and Golang mocking tools out there. Feel free to explore and find your preferences.

I use all of these frameworks personally all the time. They fulfill all the needs of Golang’s unit test most of the time from HTTP, interface, and database SQL. Feel free to find other third-party frameworks or create your own based on your own needs. However, I don’t recommend using monkey patching even for unit tests. You are free to try but try it at your own risk.

The Importance of Assertion

When you are creating a unit test and its test cases, you will realize that the bigger and more complex a function becomes, maintaining or expanding the unit test becomes more difficult. We can overcome this problem by simplifying the unit test by making all the mock as localized as possible.

One of the most common reasons a unit test becomes harder to maintain is that we can’t track the flow for each test case. Hence, it is recommended that all mock expectations and assertions are as detailed as possible. Avoid using general expectations such as assert.NoError(), gomock.Any() or sqlmock.AnyArg() which has the potential of producing a false-positive result.

Coverage may represent the unit test exposure to the function. But, it does not represent the correctness of a function. That is the role of assertion.

Make It Local!

The mocking of interfaces alone does not guarantee you to cover 100% of your code. There are several cases in which interface cannot be used and yet you need to mock the response of the function such as json.Marshal() or time.Now().

These public functions can be mocked by creating your own library to contain the public functions as method receivers. Basically, this library will have a struct that implements all the functions from the imported package public functions and act as a wrapper.

This way, mocking public functions will not be a problem anymore. As long as the unit test is still within the scope for testing function receiver, this mocking method will help to mock every possible scenario, thus the 100% coverage.

Overwrite by Variable

There are several cases in which interface alone is not enough or too much hassle for simple functions such as fmt.Printf or log.Fatal. One such example is the normal function without a receiver.

How are you going to mock these functions in your func main() and achieve 100% coverage?

The alternative solution is to make public functions overwritten by using dependency injection via variable. You could achieve this in several ways. The simplest one is to create a variable referencing to the public function like this:

var logFatal = log.Fatal

By overwriting this variable, you are now able to mock the public function in the unit test by replacing the variable function with the mock function. Lastly, do not forget to recover the function back into the variable, or else the variable will always refer to the mock function during the unit test.

func TestLogFatal(t *testing.T) {
logFatal = func(v ...interface{}) {
return
}
defer func() {
logFatal = log.Fatal
}()
// Do your unit test
}

Kind Reminder: When overwriting the public functions into the local variables, please assign the function itself into the variables, not the result of the executed function. This is a common mistake and yet a fatal one.

Don’t : var fmtPrintf = fmt.Printf()

Do: var fmtPrintf = fmt.Printf

Warning: Although this method is much more simple and easier to implement. It comes with risks. Since the mock is using a global variable that can be accessed throughout the package scope, the possibility of conflict when overwriting the variable will make the unit test unreliable. Especially when running the unit test in parallel.

Summary

Unit test is indeed hard, tricky, and, need a lot of extra work. On the other hand, a unit test is also essential as first-line protection against bugs and addressing them early. A good and reliable unit test is the best guardian your code will ever have in its entire lifespan. Just like in real life, a good security system should never have any unguarded parts or loopholes that can be exploited by the intruder. Therefore, a 100% coverage unit test is your best security system for your codes. I hope this article helps you with your journey to full coverage. Good luck with your unit test!

--

--

Kelvin Benzali
CodeX
Writer for

Software Engineer at Tokopedia. Technology and history enthusiast.