Golang Unit Testing
1. Unit Testing
a. What is unit testing?
Testing is part of the software development process and the purpose is to produce better software, more robust, with fewer bugs, and more stable.
There are different kinds of testing that can be done to improve software quality. Starting with unit testing, integration testing, end to end testing, and we will focus on unit testing in this blog post.
In the software testing pyramid image above, we can see that unit testing is cheaper and faster to do. That is why we should cover unit test as much as possible to reduce the testing need to be done on integration and end-to-end testing.
b. How unit testing can improve software quality
Unit testing can help us verify code behavior at the function/method level. For example, if we have an add
function that takes 2 params we expect the return to be the addition of both params. For this, we can unit test the method by providing some test cases that verify the function behavior. The expectation is that if every unit is working properly proven by its test cases then the overall software behavior should also work as intended.
Another advantage of unit testing is we will have more confidence in refactoring our code since as long as all UTs pass then we have confidence that the behavior of the code is still the same and should work fine. This is important since we need to keep improving our code and we should have confidence that we are not breaking anything in doing so. But of course, we should define our UT test cases properly that cover all conditions and edge cases.
c. Unit test coverage
There is a metric to measure how much percent of our code base is covered with unit testing. The metric name is Unit Test Coverage. In Halodoc we set a really high number for our unit test coverage, it's more than 95% for overall code base Unit Test Coverage and more than 95% for new code to be merged to the main branch (deployment). If the coverage condition is not met new code could not be deployed to production. Halodoc enforces this condition to force the implementation of Unit Testing and get the benefit from it.
In GoLang, Unit Test Coverage is calculated from how many lines of code are executed when running testing. For certain packages that do not require unit testing, we can ignore them so that the package does not drop the coverage.
We could not blindly believe that higher Unit Test Coverage means better software quality since being said earlier, Unit Test Coverage only calculates lines of code that are executed on running tests.
Assertionless unit testing can also increase the Unit Test Coverage, but assertionless unit testing is useless. Assertionless unit testing is unit testing that only runs the function without having any assertion of the function/method behavior.
2. How to do Unit Testing in GoLang
a. Unit Test
To create unit testing on go, we should follow this convention:
- Create a file with a name that ends with
_test.go
. The best practice is to name the file following the file it's testing. For example, if we havemath.go
the unit test file should bemath_test.go
. And it's also best practice to put the test file and the implementation in the same package. - Create a function in the test file and start the name with Test. The function should take
*testing.T
from the GoLang testing package. The test function signature isTestXXX(t *testing.T)
with XXX as the name of the function we are testing.
Those two steps are the only mandatory step to create unit testing. But we can follow some best practices to structure our tests and test cases:
- Structure our test as test tables with many test cases
- Properly assert the function behavior
- Name test cases as descriptive as possible
Example:
We have a function called Add
in mymath.go file on mymath
package defined like this:
We can create mymath_test.go
in the same package and define this function to the Add
function:
We can run the GoLang lang unit test using go test
terminal command, which comes from within GoLang itself:
- Use
go test ./...
to run all tests within the entire code base../...
means all files. We can only run a specific test file if needed.
-
Use
go test -race ./...
to run the unit test with a check for a race condition. GoLang has good support for concurrency and we should utilize that functionality, however, this concurrency can sometimes introduce a race condition, GoLang provides support to check race conditions on unit testing by passing-race
argument in the test command. -
Use
go test ./... -coverprofile=coverage.out
to run the unit test along with generating the unit test coverage profile. We can consume thecoverage.out
file to display Unit Test Coverage information.○ Use
go tool cover -func=coverage.out
to display coverage information on the terminal console.
○ Use go tool cover -html=coverage.out
to display the coverage information on HTML format in browser.
In GoLang a function can have public or private access. The convention to specify that in go is by name the function starts with an uppercase letter for public and starts with a lowercase letter to set it as private. In GoLang, private means package private, which means code within the same package can still access the private function. This functionality will allow us to test private functions easily since we are putting the unit test file in the same package as the functions.
b. Mocking
In unit test we only test our code in isolation, so if the function/method use dependency on any other code it would be better to mock that dependency since our focus is to test the function/method. If you are familiar with java unit tests, mocking in java with help from a library like mockito is easy. Mock can be created by using annotation or simply create mock by passing the class as a parameter.
Mocking in go is different, mock in go can only be implemented to an interface, and it is not actually a mock. We simply create an implementation of the interface for mocking purposes.
Example
We have a user service and user DAO defined like this:
Service:
DAO:
We can test the UserService like this:
We should write code for mock implementation in go, but there are some libraries from the GoLang community to generate mock from an interface in GoLang. In Halodoc we are using mockery to generate our mocking code. This generated mocking should also be put on our code base git repo because this mock code is also part of our codebase.
We can generate mock using mockery by this command:
mockery --dir dao/UserDao.go --name UserDaoInterface --output mocks
Mockery Usage Example:
To make generate mock easier when the size of our interface growing, we can utilize go generate in our interface like this:
We can use GoLang cli command go generate
to generate the mocks. go generate
will look for file with go:generate
tag and execute its command.
For example we can use this command to generate the interface above:
go generate ./...
HTTP Server Mock
If you have code that uses an HTTP client in it to communicate with other services and want to test its behavior you can generate a mock or you can still use a real HTTP call but point the URL to a mock HTTP server. GoLang comes up with the httptest
package which has good support for the HTTP test. We can create a mock HTTP server and set the server behavior to suit our test cases.
Example:
We have this function that call API:
We can test it like this:
Conclusion
In this blog, I have covered ways of writing unit tests in Golang that will help to improve code quality, early detection of bugs and deliver bug-free feature releases. Additionally, We can also have guardrails like code-build pipeline gates to enforce UT threshold coverage which could significantly aid in improving the overall quality of the product being delivered to our customers.
Join us
Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.
About Halodoc
Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek and many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.ˀ