Unit Test in Golang

Hendro Prabowo
Tunaiku Tech
Published in
9 min readMar 4, 2020
Photo By Sergey Kolodiy from toptal

Software testing is very important in software development. Usually, this task is carried out by a tester or QA, however, it is different for unit test.

The person who writes and runs the unit test is the developer, not QA or tester.

Now I’m going to explain what unit test is, especially in golang.

Unit test is a process of testing a function of software independently from other functions.

If a function depends or calls another function, the function that is called must be mocked (will be explained later).

Software Engineering Theory and Practice (Pfleeger, 2001)

Unit test is the smallest part of software testing in which the creators are its developers. By testing every function that is in our repo and making sure it runs as expected, the possibility of errors that occur when executing the whole function will be smaller.

Good Unit Test :

  1. Readable — A good unit test should be easy to read. If the unit test fails, we easily can detect the problem.
  2. Reliable — Unit test should fail if there’s a bug in the system that we test.
  3. Fast — Usually unit test runs faster than the actual function because in the unit test we create a mock function instead of the real function.
  4. Truly unit, not integration — Our unit test cannot call other functions, we must test the smallest part of our function. If a function calls another function, we must mock that function. Unless the ones that are called are private functions.
  5. Prints a clear description of the problem — It should print a clear description of the problem so the developer can easily see which code that make the function error.

(Kolodiy,2020)

A Test is Not a Unit Test If :

  • It talks to the database.
  • It communicates across the network.
  • It can’t run correctly at the same time as any of your other unit tests.
  • You have to do special things to your environment.

Now imagine if we have a flow such as shown below.

If we want to test Service 1 in that image with a unit test, then we must mock the Repo.

But how do we mock the repo?

The answer is by using an interface.

One of the advantages of golang programming language is its interface.

If a struct implements all functions in that interface, then that struct is the interface itself.

With this interface, we can direct which functions we will call, whether it is actual functions or mock functions.

Golang test tool provides 3 functions :

  1. Test function.
  2. Benchmark function.
  3. Example function.

(Donovan & Kernighan, 2006)

Test Function

The first one is the test function. In this function, we need to test our function whether the output matches our expectation or not.

For example :

Imagine we have IsPalindrome function like that and we want to make a unit test to test that function.

func IsPalindrome(text string) bool {
if len(text) <= 1 {
return true
}
if len(text) == 2 && text[0] == text[1] {
return true
}
if text[0] != text[len(text)-1] {
return false
}
return IsPalindrome(text[1 : len(text)-1])
}

In that palindrome function, we expect that function will return true if the parameter that we give is palindrome and return false if the parameter that we give is not palindrome.

From that snippet code, we create 2 unit test functions:

  1. Positive — we insert a palindrome text as a parameter and we expect the function will return true.
  2. Negative — we insert a non-palindrome text as a parameter and we expect the function will return false.

Based on that we can create several unit tests to test all the condition :

func TestIsPalindrome_WithLengthLessThanOrEqualOne(t *testing.T) {
expectation := true
actual := IsPalindrome("a")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

func TestIsPalindrome_WithLengthExactlyTwoAndSameCharacter(t *testing.T) {
expectation := true
actual := IsPalindrome("aa")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

func TestIsPalindrome_WithFirstCharAndLastCharAreDifferent(t *testing.T) {
expectation := false
actual := IsPalindrome("abc")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

func TestIsPalindrome_WithPalindromeText(t *testing.T) {
expectation := true
actual := IsPalindrome("kayak")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

func TestIsPalindrome_WithNotPalindromeText(t *testing.T) {
expectation := false
actual := IsPalindrome("palindrome")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

Then we run with the function with the command “go run -v” or with tools in your IDE or text editor and see the result.

=== RUN   TestIsPalindrome_WithLengthLessThanOrEqualOne
--- PASS: TestIsPalindrome_WithLengthLessThanOrEqualOne (0.00s)
=== RUN TestIsPalindrome_WithLengthExactlyTwoAndSameCharacter
--- PASS: TestIsPalindrome_WithLengthExactlyTwoAndSameCharacter (0.00s)
=== RUN TestIsPalindrome_WithFirstCharAndLastCharAreDifferent
--- PASS: TestIsPalindrome_WithFirstCharAndLastCharAreDifferent (0.00s)
=== RUN TestIsPalindrome_WithPalindromeText
--- PASS: TestIsPalindrome_WithPalindromeText (0.00s)
=== RUN TestIsPalindrome_WithNotPalindromeText
--- PASS: TestIsPalindrome_WithNotPalindromeText (0.00s)
PASS

As we can see, all the unit tests are passed.

Now we want to try to mock a function.

To make it easier for us, we use the testify library.

Imagine we have code like this.

// Model
type User struct {
Username string `json:"username"`
Password string `json:"password"`
}

// Interface
type UserRepositoryInterface interface {
GetAllUsers() ([]User, error)
}

// Service
type UserService struct {
UserRepositoryInterface
}
func (s UserService) GetUser() ([]User, error){
users, _ := s.UserRepositoryInterface.GetAllUsers()
for i := range users {
users[i].Password = "*****"
}
return users, nil
}

// Repository
type UserRepository struct {}
func (r UserRepository) GetAllUsers() ([]User, error) {
users := []User{
{"real", "real"},
{"real2", "real2"},
}
return users, nil
}

// Main
func main(){
repository := UserRepository{}
service := UserService{repository}
users, _ := service.GetUser()
fmt.Println(users)
}

In that code, we want to make mock on GetAllUsers function. That function belongs to the UserRepository struct.

Like the picture shown above, Interface is the key. As you can see, UserService struct has UserRepositoryInterface as its attributes, but in the main function, UserRepositoryInterface as an attribute in UserService struct is assigned with the UserRepository struct? How?

This is the key :

If a struct implements all the methods in that interface, then that struct is the interface itself.

If we run that code, then it will print “[{real *****} {real2 *****}]” which means the user real comes from the real function.

Now we create our mock function in file main_test.go

package main

import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
)

type UserRepositoryMock struct {
mock.Mock
}
func (r UserRepositoryMock) GetAllUsers() ([]User, error) {
args := r.Called()
users := []User{
{"mock", "*****"},
}
return users, args.Error(1)
}

func TestService_GetUser(t *testing.T) {
repository := UserRepositoryMock{}
repository.On("GetAllUsers").Return([]User{}, nil)

service := UserService{repository}
users, _ := service.GetUser()
for i := range users{
assert.Equal(t, users[i].Password, "*****", "user password must be encrypted")
}
fmt.Println(users)
}

In that snippet, UserRepositoryMock also implements UserRepositoryInterface, so the UserRepositoryMock also the RepositoryInterface itself.

We can assign the UserRepositoryMock to UserService struct so the service will call the mock GetAllUsers function, not the real function from the real repository.

One thing that must exist in the test is verification that the function runs as expected. In that Test function, we use package assert to verify the result. Function test.Equal will be used to verify that the password form user already encrypted as expected.

Then we run the test using the command “go test -v” and see the result.

The result will be displayed below.

=== RUN   TestRepository_GetAllUsers
[{mock *****}]
--- PASS: TestRepository_GetAllUsers (0.00s)
PASS

All the tests are passed and the user that we print is [{mock *****}] with the password encrypted. It shows that the model user comes from mocking functions.

Benchmark Function

In developing software, sometimes we have a different way with our partners or maybe we have many solutions to solve one problem. How can we measure which is the best solution? One of the answers is to test the performance from all of our solutions and chose the fastest.

Imagine we have 2 ideas to create IsPalindrome function.

First with a recursive function :

func IsPalindrome(text string) bool {
if len(text) <= 1 {
return true
}
if len(text) == 2 && text[0] == text[1] {
return true
}
if text[0] != text[len(text)-1] {
return false
}
return IsPalindrome(text[1 : len(text)-1])
}

And the second with for loop function :

func IsPalindromeLoop(text string) bool {
n := len(text)/2
for i:=0; i<n; i++{
if text[i] != text[len(text)-1-i] {
return false
}
}
return true
}

Before the benchmark test, first, we must make sure that our function already passes the first unit test.

IsPalindrome function already passes the unit test that explained in the Test Function chapter.

Now we will test the IsPalindromeLoop with this test function.

func TestIsPalindromeLoopWithPalindromeText(t *testing.T) {
expectation := true
actual := IsPalindromeLoop("kasurnababanrusak")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

func TestIsPalindromeLoopWithNonPalindromeText(t *testing.T) {
expectation := false
actual := IsPalindromeLoop("palindrome")
if actual != expectation {
t.Errorf("Expected %v but got %v", expectation, actual)
}
}

Then we run the command “go test -v” or from your IDE and see the result.

=== RUN   TestIsPalindromeLoopWithPalindromeText
--- PASS: TestIsPalindromeLoopWithPalindromeText (0.00s)
=== RUN TestIsPalindromeLoopWithNonPalindromeText
--- PASS: TestIsPalindromeLoopWithNonPalindromeText (0.00s)
PASS

We pass the test and ready to benchmark the function.

If we want to find out what program is the best, we can write the benchmark unit test.

func BenchmarkIsPalindrome(b *testing.B) {
input := "kasurnababanrusak"
for i:=0; i<b.N; i++ {
IsPalindrome(input)
}
}

func BenchmarkIsPalindromeLoop(b *testing.B) {
input := "kasurnababanrusak"
for i:=0; i<b.N; i++ {
IsPalindromeLoop(input)
}
}

Then we run both unit tests and see the result.

BenchmarkIsPalindrome-8        33516276         32.9 ns/op
BenchmarkIsPalindromeLoop-8 106128772 10.3 ns/op

From the benchmark results above, we can conclude that the palindrome function with loops has a faster speed than the recursive ones, so it can be said that functions that use for loop are better used.

Example Function

Example function is a little same with the Test function, that is to make sure that our program runs as we expected.

The Example function is intended for programmers who want to quickly create unit tests. The example unit test function automatically compares the standard output print (fmt) in golang with our output comment, so we don't have to create check expected logic.

Because the syntax doesn’t require additional libraries and we don’t need to make logic to match the results obtained with expectations, making unit tests can be done faster.

There are 2 advantages for creating example unit test :

  1. Faster to create a unit test. Because the code in the unit test is less than usual, so the programmer can faster to make a unit test.
  2. Easy to do experimentations

Here is an example :

Imagine we have CheckNumber function that checks number input if it below 0 then the function will return a small number, if it above 0 and below 100 the function will return a medium number and if it above the 100 than the function will return a big number.

func CheckNumber(n int) string {
if n <=0 {
return "small number"
}
if n > 0 && n <= 100 {
return "medium number"
}else {
return "big number"
}
}

Now we create several unit tests to test all the conditions on the function.

func ExampleCheckNumber_WithInputLessThanZero() {
fmt.Println(CheckNumber(-1))
// Output:
// small number
}

func ExampleCheckNumber_WithInputGreaterThanZeroAndBelowOneHundred() {
fmt.Println(CheckNumber(91))
// Output:
// medium number
}

func ExampleCheck_WithInputGreaterThanOneHundred() {
fmt.Println(CheckNumber(101))
// Output:
// big number
}

and run that unit test with you editor or “go test -v” command

=== RUN   ExampleCheckNumber_WithInputLessThanZero
--- PASS: ExampleCheckNumber_WithInputLessThanZero (0.00s)
=== RUN ExampleCheckNumber_WithInputGreaterThanZeroAndBelowOneHundred
--- PASS: ExampleCheckNumber_WithInputGreaterThanZeroAndBelowOneHundred (0.00s)
=== RUN ExampleCheck_WithInputGreaterThanOneHundred
--- PASS: ExampleCheck_WithInputGreaterThanOneHundred (0.00s)
PASS

All the test is passed.

Now we put a bug in our code. We change the range medium number from 0 to 90.

func CheckNumber(n int) string {
if n <=0 {
return "small number"
}
if n > 0 && n <= 90{
return "medium number"
}else {
return "big number"
}
}

If we put 100 as a parameter, we expect that function will return “medium number”, but that function will return “big number” because we put a bug in that function.

Then we run all the unit test again.

func ExampleCheckNumber_WithInputLessThanZero() {
fmt.Println(CheckNumber(-1))
// Output:
// small number
}

func ExampleCheckNumber_WithInputGreaterThanZeroAndBelowOneHundred() {
fmt.Println(CheckNumber(91))
// Output:
// medium number
}

func ExampleCheck_WithInputGreaterThanOneHundred() {
fmt.Println(CheckNumber(101))
// Output:
// big number
}

and see the result.

=== RUN   ExampleCheckNumber_WithInputLessThanZero
--- PASS: ExampleCheckNumber_WithInputLessThanZero (0.00s)
=== RUN ExampleCheckNumber_WithInputGreaterThanZeroAndBelowOneHundred
--- FAIL: ExampleCheckNumber_WithInputGreaterThanZeroAndBelowOneHundred (0.00s)
got:
big number
want:
medium number
=== RUN ExampleCheck_WithInputGreaterThanOneHundred
--- PASS: ExampleCheck_WithInputGreaterThanOneHundred (0.00s)
FAIL

It shows the unit test in the second condition fails and gives us the summary that we got a big number from our function and we want “medium number” without if logic.

--

--