Setup Unit Test in Go using Table Driven Test in Clean Architecture
“Tests are stories we tell the next generation of programmers on a project.”
— Roy Osherove, The Art of Unit Testing
As you may know, an application contains a small number of components, which could be a struct, function, or class. These components are referred to as unit components, and the reliability of our application is dependent on them. As engineers, our task is to ensure that the unit components do not break our application.
The only way to ensure it is to use unit testing, which is a method of testing our unit components that should be isolated from our external system.
Working Effectively with Legacy Code author Michael Feathers stated that a test is NOT a unit test if:
- Communicates with the database.
- Requires a connection across the network.
- Touches the file system.
- Requires system configuration.
- Cannot be run concurrently with any other test.
Since we can’t talk to the database or the network, how do we test a function that is responsible for querying data from a SQL database?
To enable those testing requirements, we must use a method known as mocking, which is a process used in unit testing when the unit has external dependencies so that we can isolate the unit and focus on the code being tested rather than the behavior or state of the external dependencies. Dependencies are replaced with tightly controlled replacement objects that mimic the behavior of the real ones.
Built-in Test Library in Go
In this article, we’ll go over how to write unit tests in Go. Go already has a testing
package built into its standard library, which provides the tools we need to write unit and benchmark tests. The following are some prerequisites for writing unit tests:
- Test file should have the suffix
_test.go
because the compiler ignores files with that suffix when compiling packages. - Test function name should be PascalCase formatted with prefix
Test
. Example:func TestHelloWorld(t *testing.T)
- It is preferable to place the test file in the same package as the targeted file and with a similar file name. For example, if the target file is
utils/hello.go
the test file should beutils/hello_test.go
.
Table Driven Test
As the name suggests, this is a method of validating a function or method against multiple parameters and results. When a table contains multiple test cases, the test simply iterates through all table entries and runs the necessary tests. The test code is written only once and is amortized across all table entries.
We can centralize the testing of a function into a single function block by using table driven testing.
Table driven test is just one of the choice that you can pick for your unit test. if you prefer to use expressive Behavior-Driven Development (BDD) style tests for example, you can look into Ginkgo for your unit test.
Let’s get started by writing unit tests with the built-in Go standard library.
Assume we have a function that calculates the factorial of a positive integer that we put in a file called calc.go
.
Then we create test function called func TestFactorial(t *testing.T)
on calc_test.go
file in the same folder of calc.go
.
We declare tests
with an anonymous struct inside the test function to store our table-driven test cases. We run each test case on its own subtest which a Go’s testing
package construct that divides our test functions into granular test processes. It executes our test case in a separate goroutine and waits for our function to complete.
Using the Go equals operator, we simply compare the expected and actual results. If you require more advanced assertion, a third-party library such as Testify can be used.
Let’s run our code with go test -cover {YOUR_PROJECT_NAME}/calc
command, all test cases will run successfully and our test coverage for cal
will be displayed.
To make sure we are writing the right code, let’s make our test fail by adding a new case, where we’ll use 5 as the n value and expect it to return 0, and it will, of course, fail on the TestFactorial/N_is_5
case.
Testing Using Mock in Clean Architecture
We should not talk to the database during unit testing, so if our function calls another function that performs a database query, we could mock the database query functionality. To mock an external resource in our function or method, we should never directly call the function that is responsible for accessing the external resource. We could use Dependency Injection, with the parameter based on abstractions and use Go interfaces as abstraction instead of concrete implementations.
For example, we had an application to create articles that was accessible via an HTTP end point. The file structure will look like this because we use Clean Architecture to organize our code structure.
The entity
is not dependent on any other layer; it is simply a struct that holds our data. The repo
is in charge of accessing external resources (Redis, PostgreSQL, API calls, etc.) and relies on libraries to do so, such as go-redis, sql, and http.
The usecase
layer is in charge of executing our business logic and should only rely on the repo
module. The last layer, the delivery
layer, is in charge of determining how the data should be presented to users. Because our app exposes an HTTP end point, the delivery layer will include some sort of HTTP handler, which will be determined by our usecase
layer.
Testing Repository Layer
On our example application, the repository layer will connect to a PostgreSQL database by using sqlx library. Since Go has implicit interface, we could extract sqlx function to an interface, and inject it to out repository layer, or we could use go-sqlmock to to simulate any sql driver behavior in tests, without needing a real database connection. I prefer using go-sqlmock library, since it already implementing sql/driver on Go standard library and also compatible with sqlx.
The repository layer in our example application will connect to a PostgreSQL database via the sqlx
library. We could extract the sqlx
function to an interface and inject it into our repository layer, or we could use go-sqlmock
to simulate any SQL driver behavior in tests without requiring a real database connection. I prefer the go-sqlmock
library because it already implements sql/driver
on the Go standard library and is also sqlx
compatible.
And here is our test for UserRepo
implementation, which we put in user/repo_test.go
.
As in the previous example, we create table-driven test cases and run each test case on a subtest. However, within our subtest, we create a sqlmock
instance and set it to sqlx
so that we can control the behavior of our query using the sqlmock
instance. And for each test case, we add a beforeTest
hook and inject the sqlmock
instance via our hook, so that each test case can access its own sqlmock
instance to prepare for expected behavior. When we run our test, all of the cases will succeed, and if we change the expected result to an invalid one, it will fail as expected.
As you can see, we don’t connect to the real database in the test file, and we don’t even set up the actual DB connection in our main code yet. If you prefer to use Test Driven Development on your project, you can first write the test and prepare the expected query before proceeding with the actual implementation.
This can be applied to a variety of resources (i.e. Redis, API call, Kafka, etc).
If you can’t find a suitable library, you can use Go’s implicit interface feature to create your own interface for those libraries and your own mock.
Testing UseCase Layer
Because the UseCase layer is dependent on our repository layer, we must first mock our repository before testing the UseCase layer. We could simply create another implementation to be used for testing because our repository is built on interfaces.
We could create custom implementation of our repository based on our test cases. But it a repetitive task since for each test case we need to create new implementation, by using gomock we could generate source code for a mock implementation based on given interface to be mocked. Remember how we use sqlmock on previous section? By using gomock we could create a universal mock of our repository and control the behavior based on our need.
Based on our test cases, we could create a custom implementation of our repository. However, because we need to create a new implementation for each test case, using gomock
allows us to generate source code for a mock implementation based on the given interface to be mocked. Remember how we used sqlmock
in the previous section? We could use gomock
to create a universal mock of our repository and control the behavior based on our requirements.
Here is interface and implementation of our use case, let’s call it RegisterUserUseCase
, the use case is responsible for registering new user. So we will need UserRepo
as a dependency on that use case.
Here is the interface and implementation of our use case, which we’ll call RegisterUserUseCase
because it’s in charge of registering new users. As a result, UserRepo
will be required as a dependency on that UseCase.
After installing gomock, let’s create the mock of our UserRepo
so we can use it on RegisterUserUseCase
by running this command on our terminal.
By running this command on our terminal after installing gomock
, we can create a mock of our UserRepo
to use on RegisterUserUseCase
.
It will generate our mock file on internal/user/repo_mock_test.go
. For simplicity, I just create the mock file on same package as implementation. Some people prefer to put the mock file on difference package, but feel free to adjust the file location and package based on your need. Here is the example of newly generated UserRepo
mock file by gomock.
It will create our mock file on the internal/user/repo_mock_test.go
. To keep things simple, I create the mock file in the same package as the implementation. Some people prefer to place the mock file in a different package, but you are free to change the file location and package to suit your needs. Here’s an example of a newly generated UserRepo
mock file generated by gomock
.
On /user/usecase_test.go
, we create new test function TestNewRegisterUserUseCase(t *testing.T)
for testing RegisterUserUseCase
.
We inject instance of MockUserRepo
to our use case, and we can control its behavior on beforeTest
hook since mockgen provide us some additional method to MockUserRepo
instance. You can see list of generated method on gomock github repository.
We inject a MonoUserRepo
instance into our use case, and we can control its behavior on the beforeTest
hook because mockgen
provides us with some additional methods to MockUserRepo
instances. The list of generated methods can be found in the gomock
github repository.
Testing Delivery Layer
Because our application is exposed via HTTP, we define POST /users
as an end point for registering new users. Let’s write the HTTP handler for that endpoint.
We must generate a mock for RegisterUserUseCase
using gomock
, just as we did for our UserRepo
. Furthermore, Go standard library already provide use with httptest
package, so we don’t have to create mock for http.ResponseWriter
.
We simulate success and failure scenarios by injecting the RegisterUseCase
mock implementation and comparing the status, headers, and body of the HTTP response provided by httptest.ResponseRecorder
. Then, when we run our test file, all of the tests should pass.
We could use the Go project’s built-in library to implement unit testing.
Finally, managing dependency between components via interface will make it easier to mock the implementation and control the behavior of mocked dependencies for our unit tests.
You can see full source code for the article on this repository. Thank you.
Reference
- https://medium.com/rungo/unit-testing-made-easy-in-go-25077669318
- https://smartbear.com/learn/automated-testing/what-is-unit-testing
- https://www.telerik.com/products/mocking/unit-testing.aspx
- https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go
- https://github.com/golang/go/wiki/TableDrivenTests
- https://ieftimov.com/post/testing-in-go-subtests
- https://www.manning.com/books/unit-testing