Setup Unit Test in Go using Table Driven Test in Clean Architecture

Syafdia Okta
9 min readAug 18, 2021

“Tests are stories we tell the next generation of programmers on a project.”

Roy Osherove, The Art of Unit Testing

Image by PublicDomainPictures from Pixabay

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.

Testing Pyramid (https://www.hibri.net/2019/06/10/the_testing_pyramid/)

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 be utils/hello_test.go.
Location of Test File

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.

Non Table Driven Test

We can centralize the testing of a function into a single function block by using table driven testing.

Table Driven Test

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.

Success Test

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.

Fail Test

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

--

--