Exploring Unit Testing in Golang
Unit Testing is a must for any successful implementation. Let's explore how to write unit testing in Golang using built-in Golang features.
Join the DZone community and get the full member experience.
Join For FreeUnit testing is a fundamental practice in software development that focuses on testing individual units of code, typically at the function or method level. A unit is the smallest testable part of an application, such as a function, method, or class. The objective of unit testing is to verify that each unit of code works as expected in isolation.
Here Are Some Key Aspects of Unit Testing
Isolation
Unit tests are designed to be independent and isolated from other units of code. This means that each unit(a.k.a functions, methods) is tested in isolation, without dependencies on other units or external resources. This is usually achieved by using test doubles, such as mock objects or stubs, to simulate the behavior of dependencies.
Automation
Unit tests are typically automated, which means they can be executed repeatedly and reliably. Automated unit tests can be written using a testing framework or library that provides tools for defining test cases, running tests, and asserting expected results.
Granularity
Unit tests focus on testing small, cohesive units of code. This allows for fine-grained testing and helps identify issues at an early stage. By testing individual units, developers can pinpoint specific areas of code that are not functioning correctly.
Coverage
Unit tests aim to achieve high code coverage, meaning that as much of the code as possible is tested by unit tests. This helps ensure that all the branches, conditions, and edge cases of the code are exercised and validated.
Fast Execution
Unit tests should execute quickly, allowing developers to get rapid feedback on the correctness of their code. Fast execution helps maintain developer productivity and encourages running tests frequently, such as after each code change.
Test-Driven Development (TDD)
Unit testing is often associated with Test-Driven Development. In TDD, developers write the unit tests before writing the code itself. This practice helps drive the development process, as developers focus on writing code that fulfills the requirements defined by the tests.
Benefits of Unit Testing
- Early Bug Detection: Unit tests can catch bugs and issues at an early stage, enabling developers to fix them before they propagate to other parts of the system.
- Improved Code Quality: Unit testing encourages developers to write modular, well-structured code that is easier to test and maintain.
- Facilitates Refactoring: Unit tests provide a safety net when refactoring code. They ensure that changes made to the codebase do not break existing functionality.
- Documentation and Code Examples: Unit tests serve as living documentation and code examples that demonstrate how to use and interact with units of code.
Unit Testing in Golang
Let's take a look at the unit testing in Golang. For any development project, there is a possibility of introducing bugs unintentionally. Having a comprehensive list of test cases with different variances would help to discover these bugs well in advance. In fact, the best-recommended way of writing code is by creating a unit test first before start implementing the actual code unit. Go language provides a built-in package for writing unit tests called "testing." Developers can simply import this package and start creating their unit test cases.
Built-in Testing Package in Golang
The testing
package is the backbone of Golang unit testing. It helps developers to create unit tests with different types of test functions. The testing.T
type offers methods to control test execution, such as running tests in parallel with Parallel()
, skipping tests with Skip()
, and calling a test teardown function with Cleanup()
.
Errors and Logs
The testing.T
type provides various practical tools to interact with the test workflow:
t.Fail*()
, which helps to indicate the failure of the test case execution. It provides very limited details compared tot.Error*().
t.Errorf()
, which prints out an error message and sets the test as failed.t.Error*
does not stop the execution of the test. Instead, all encountered errors will be reported once the test is completed.t.Fatal*()
It should be used to fail the execution, and it is beneficial in scenarios where it's the requirement to exist the test execution.t.Log*()
function is used to print information during the test execution; it can be handy in some situations.
Conventions of Unit Testing in Go Language
“testing”
is the name of the built-in package for supporting unit test creations.“go test”
It is the command for executing tests quickly.- Unit tests are written in separate files that are named with
“_test.go”
. For example, a given“Operations.go”
source code will have a unit test file name“Operations_test.go”
with the list of test cases written in it. - All Unit Tests are placed in the same package along with the actual code.
- Go Unit test functions start with Test and are appended with the function name. Ex:
“TestFuncName()”
- Unit Test Function always has only one parameter
*testing.T
Ex:TestFuncName(test *testing.T)
test.Error,
test.Fail
,test.Fatal
Are the calls used to indicate test errors or failures.
Simple Functionality to Implement
Start with a simple use case of finding the difference between two integer numbers. This difference must be a positive number.
- The difference between 5 and 2 is 3.
- The difference between 8 and 2 is 6.
- The difference between 3 and 9 is 6.
- The difference between 0 and 2 is 2.
- The difference between 4 and 0 is 4.
Let's create operations.go
file with a simple Difference(num1, num2) function, which returns the actual difference between those two numbers.
package mathops
// Difference function that finds the difference between two integers.
// It returns positive numbers always
func Difference(num1, num2 int) (difference int) {
if num1 > num2{
return num1 - num2
}else{
Return num2 - num1
}
}
Writing Unit Test Cases
In the Test-driven development approach, unit tests are written well before writing the actual code. For this demonstration, Let's create unit test cases for the above function. All Unit Test functions are written in a separate file that has a file name ending with _test.go
. For this case, let's create operations_test.go
with the Unit test function as TestDifference(test *testing.T).
All Go unit test files _test.goare typically
placed in the same package along with the actual source code files. Also, the Go compiler will take care of excluding these _test.go
files during the build process.
Create operations_test.go
package mathops
import "testing"
func TestDifference(test *testing.T) {
// Test 1: Positive
actual := Difference(4, 6)
expected := 2
if actual != expected {
test.Errorf("actual value: %d, expected value: %d", actual, expected)
} else {
test.Logf("actual value: %d, expected value: %d", actual, expected)
}
}
Testing using go test
command. PASS.
Testing using go test
command. FAIL.
Table Driven Testing
So far, It's a single test for each test function. This would lead to a lot of code to maintain for multiple unit test cases. This is a better way to organize various unit test scenarios using Table driven unit testing approach in Go lang.
The table-driven test approach starts with defining the input data structure. It's like describing the column of the table.
type diffTest struct {
name string
num1, num2, expected int
}
Based on this data structure, Developer should create test data set as a predefined variable. Each row of the table lists a test case to execute. Once the table is defined, you write the execution loop.
var diffTests = []diffTest{
{"1st Test: values 2,3 ", 2, 3, 1},
{"2nd Test: values 4,8 ", 4, 8, 3},
{"3rd Test: values 6,9 ", 6, 9, 3},
{"4th Test: values 10,13 ", 10, 13, 3},
}
The execution loop calls, which defines a subtest. As a result, each row of the table defines a subtest named. [NameOfTheFuction]/[NameOfTheSubTest]
.
This way of writing tests is very popular and considered the canonical way to write unit tests in Go.
Let's Take a look at the following sample:
package mathops
import "testing"
type diffTest struct {
name string
num1, num2, expected int
}
var diffTests = []diffTest{
{"1st Test: values 2,3 ", 2, 3, 1},
{"2nd Test: values 4,8 ", 4, 8, 3},
{"3rd Test: values 6,9 ", 6, 9, 3},
{"4th Test: values 10,13 ", 10, 13, 3},
}
func TestDifference(t *testing.T) {
for _, test := range diffTests {
t.Run(test.name, func(t *testing.T) {
actual := Difference(test.num1, test.num2)
if actual != test.expected {
t.Errorf(test.name, " FAIL: actual %d not equal to expected %d", actual, test.expected)
} else {
t.Logf(test.name, " PASS : actual %d not equal to expected %d", actual, test.expected)
}
})
}
}
Sample Execution:
Code Coverage
Writing unit test cases is the initial stride toward attaining quality. One can gauge the extent of code covered by these unit tests. Developers should strive to achieve comprehensive code coverage to uncover defects during testing effectively. Golang offers a built-in feature for determining the code coverage of unit tests.
To achieve more coverage, developers must include all possible scenarios to help unit tests execute complete code.
Following unit tests helps to achieve 100% code coverage for the sample code:
The"testing
"package in Golang provides several additional features for unit testing. Here are a few notable ones:
- Parallel Execution: By default, tests are run sequentially, but you can use the
t.Parallel()
Method to indicate that a test should be executed in parallel. Tests calling this method will be paused and resumed in parallel once the non-parallel tests have finished executing. - Skip Unit Tests: The
t.Skip()
method allows you to distinguish between unit tests and integration tests. Integration tests typically validate multiple functions and components together and are slower to execute. Using, you can choose to run only the unit tests when necessary. - Test Tear Down with Cleanup: The
t.Cleanup()
method offers a convenient way to manage test teardown. It ensures that the specified function is executed at the end of each test, including subtests. This helps communicate the intended behavior to anyone reading the test. - Benchmark Testing: Benchmark tests evaluate the performance of your code. These tests measure the runtime and memory usage of an algorithm by executing the same function multiple times. They provide insights into the efficiency of your code.
- Fuzz Testing: Fuzz testing is an exciting technique that uses random input to uncover bugs or identify edge cases. Go's fuzzing algorithm intelligently generates multiple input combinations to cover as many statements in your code as possible.
Conclusion
In conclusion, testing is crucial for validating the logic of your code and identifying bugs during development. Golang offers a comprehensive set of tools for testing your applications out of the box.
Opinions expressed by DZone contributors are their own.
Comments