Getting started with Golang unit testing
Unit testing is a way of writing code to isolate individual units or components of a software application. These units can be standalone functions, methods, or entire packages. Unit testing aims to verify that each unit of code works correctly and behaves in a predetermined behavior.
This journey will examine how to test standalone functions and dependent packages. In Golang, we have to import a package for unit testing called testing, which enables us to automate the testing of Go packages.
For testing any module or package in Golang, we need to perform the following conventions to make the code executable:
We create a file with the file’s name that contains our main logic, followed by
_test.go. For example, to test the code ofservice.go, we create a file with the name,service_test.go. For testing the interface, we need to create a separate mock file to mock the interface.We need to follow the conventions for naming our test function, as shown below:
func Test<function to be test>(t *testing){} //For stand-alone functionfunc Test<dependent_type>_<function to be test>(t *testing){} //For type dependent function
func Test: This is mandatory for any test function.<Function to be tested>: This is the name of the specific function that requires testing. If the function depends on a particular type, it’s a good practice to include the<dependent_type>name, although it’s not mandatory.(t *testing): This is mandatory to pass the pointer of thetestingtype.
To run the test, we need to run the command below:
go test # It will execute all the test functions
We can add -v flag to see the verbose output of test passes in the terminal. We can also add ./... to run all tests in the subdirectories.
Standalone-function unit testing
Standalone functions do not depend on external dependencies. Here, we test a simple function that calculates the sum of two numbers:
Note: Press the “Run” button and run the
go test -vcommand in the integrated terminal below.
package main
import (
"testing"
)
func TestSum(t *testing.T) {
// Write test cases for sum function
testCases := []struct {
a int
b int
expected int
}{
{2, 3, 5}, // 2 + 3 = 5
{0, 0, 0}, // 0 + 0 = 0
{-3, 5, 2}, // -3 + 5 = 2
{-2, -2, -4}, // -2 + (-2) = -4
}
for _, tc := range testCases {
result := Sum(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Sum(%d, %d) returned %d, expected %d", tc.a, tc.b, result, tc.expected)
}
}
}Explanation
Line 4: We import the
testingpackage for unit testing.Line 7: We create a test method signature for the
Sumfunction by following the abovementioned convention.Lines 9–8: We create the
testCasesthat contains the input value (a,b) and theexpectedvalue.Lines 20: We run the
forloop to run all the test cases.Line 21: We run the
Sumfunction and store the value inresultvariable.Line 22: We compare the
resultwith theexpectedvalue.
Dependency-injection unit testing
Dependency injection is a technique used to provide dependencies to a service. This technique isolates the actual implementation of the main logic from its dependencies and is particularly useful for testing code by enabling mock dependencies.
We test a simple service that depends on the interface. Let’s suppose we have a Service that depends on an interface like ProductRepository. In actual implementation, this interface can be a database where all the products are stored. But for testing, we do not want to fetch the database; in this scenario, we mock the ProductRepository interface.
Note: Press the “Run” button and run the
go test -vcommand in the integrated terminal below.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestProductService_GetProductByID(t *testing.T) {
// Create a mock repository
repo := NewMockProductRepository()
service := NewService(repo)
// Add a product to the mock repository
product := Product{
ID: 1,
Name: "Product 1",
Price: 29.99,
}
repo.products[1] = product
// Test case1: Retrieve the product
retrievedProduct, err := service.GetProductByID(1)
// Assert no error occur
assert.NoError(t, err)
// Check if the retrieved product matches the added product
assert.Equal(t,retrievedProduct,product )
// Test case2: Retrieve a non-existent product
_, err = service.GetProductByID(2)
assert.Error(t, err)
}
Explanation
First, we create a mock_productService.go file that contains a mock implementation of our dependency, ProductRepository. This mock implementation replicates the behavior of the GetProductByID function. When the GetProductByID function is called in productService_test.go, it invokes the corresponding mock implementation function defined in mock_productService.go:
Line 8: We create a test method signature for the
GetProducByIDfunction by following the convention discussed above.Line 10: We create an instance of the
MockproductRepository.Line 11: We inject the mock dependency into our
Service.Lines 14–19: We add one product to the mock repository.
Line 22: We call the
GetProductByIDfunction forid1 (test case 1: Existing element) and store theretrievedProductvariable.Line 24: We use the assert that no error occurs by using the
assert.NoErrorfunction from the"github.com/stretchr/testify/assert"package.Line 27: We assert that the
retrievedProductis equal to theproduct.Line 30: We call the
GetProductByIDfunction forid2 (test case 2: Non-existing element).Line 31: We assert that an error should occur when retrieving non-existent elements.
Conclusion
Unit testing is a fundamental practice for ensuring the correctness and reliability of your code. Following the conventions and techniques outlined in this Answer, we can write robust tests that help identify and fix issues early in the development process, ultimately leading to higher code quality and more maintainable software.
Unlock your potential: Golang series, all in one place!
To continue your exploration of Golang, check out our series of Answers below:
What is the NewReplacer function in golang?
Learn how Go'sstrings.NewReplacer()efficiently replaces multiple substrings in a single pass, avoiding sequential replacements.Type Assertions and Type Switches in Golang
Learn how type assertions and type switches in Go enable dynamic type handling within interfaces, ensuring type safety and flexibility for robust and efficient programming.What is the fan-out/fan-in pattern in Golang
Learn how the fan-out/fan-in pattern in Go parallelizes tasks using goroutines and channels, enabling concurrent execution and efficient result aggregation.Getting Started with Golang Unit Testing
Learn how to perform unit testing in Go by creating_test.gofiles, using thetestingpackage, and writing clear test cases.How to parse xml file to csv using golang with dynamic shcema?
Learn how to use Go'sencoding/xmlandencoding/csvpackages to dynamically convert XML files to CSV.
Free Resources