Table of Contents
Introduction
Writing effective tests is crucial in software development. Table-driven tests offer a structured approach to testing various inputs and expected outputs. In this tutorial, we will learn how to write table-driven tests in Go. By the end of this tutorial, you will understand the fundamental concepts of table-driven testing and be able to apply them in your own Go projects.
Prerequisites
To follow along with this tutorial, you should have:
- Basic knowledge of the Go programming language
- Go development environment set up on your machine
Setup
Before we dive into creating table-driven tests, let’s set up a simple Go project. Open your terminal or command prompt and follow these steps:
- Create a new directory for your project:
mkdir table-driven-tests
- Navigate to the project directory:
cd table-driven-tests
- Initialize a new Go module:
go mod init example.com/table-driven-tests
-
Create a new Go file:
touch example_test.go
-
Open the
example_test.go
file in a text editor of your choice.Now we are ready to start writing our tests!
Creating Table-Driven Tests
Table-driven tests involve using data tables to define inputs and expected outputs for a specific test case. This approach allows us to write concise and scalable tests that cover multiple scenarios. Let’s go ahead and create a simple function and a corresponding table-driven test for it.
package main
import "testing"
func Sum(x, y int) int {
return x + y
}
func TestSum(t *testing.T) {
testCases := []struct {
x int
y int
expectedResult int
}{
{2, 3, 5},
{10, -5, 5},
{0, 0, 0},
{-10, -20, -30},
}
for _, tc := range testCases {
result := Sum(tc.x, tc.y)
if result != tc.expectedResult {
t.Errorf("Sum(%d, %d) = %d, expected %d", tc.x, tc.y, result, tc.expectedResult)
}
}
}
In the example above, we have a Sum
function that takes two integers and returns their sum. The corresponding test, TestSum
, uses a table-driven approach to define multiple test cases.
The testCases
variable is a slice of structs, where each struct represents a test case. Each struct contains the input values (x
and y
) and the expected result (expectedResult
).
We then iterate over each test case using a for
loop. For each test case, we invoke the Sum
function with the input values and compare the result with the expected result using an if
statement. If the two values differ, we use t.Errorf
to log an error message.
To run the test, execute the following command in your terminal or command prompt:
go test -v
If everything is set up correctly, you should see the test output indicating whether the tests passed or failed.
Examples and Best Practices
Now that you have a basic understanding of table-driven tests, let’s explore some best practices and examples to enhance your testing skills.
Structuring Test Cases
When defining test cases in a table format, consider grouping related tests together. For example, if you have multiple test cases for a specific function, you can group them under a common subheading in the test function. This helps to improve readability and maintainability of your tests.
func TestSum(t *testing.T) {
// Test cases for positive numbers
testCasesPos := []struct {
x int
y int
expectedResult int
}{
// ...
}
// Test cases for negative numbers
testCasesNeg := []struct {
x int
y int
expectedResult int
}{
// ...
}
// Test cases for zero values
testCasesZero := []struct {
x int
y int
expectedResult int
}{
// ...
}
// ...
}
Parameterized Tests
Table-driven tests can also be used to create parameterized tests, where one test function handles multiple inputs. This approach allows you to avoid duplicating test logic and makes it easier to add or update test cases.
func TestSqrt(t *testing.T) {
testCases := []struct {
input float64
expectedOutput float64
}{
{4.0, 2.0},
{9.0, 3.0},
{16.0, 4.0},
}
for _, tc := range testCases {
result := math.Sqrt(tc.input)
if result != tc.expectedOutput {
t.Errorf("Sqrt(%.1f) = %.1f, expected %.1f", tc.input, result, tc.expectedOutput)
}
}
}
In the above example, the TestSqrt
function tests the math.Sqrt
function with different inputs. Instead of writing separate test functions for each input, we define a single test case slice and iterate over it. This way, we can easily add or modify test cases without duplicating the test logic.
Table-Driven Subtests
Go also supports subtests, which enable us to group related test cases even further. By grouping test cases into subtests, we can provide more granular reporting and quickly identify which specific test cases failed.
func TestCalculate(t *testing.T) {
testCases := []struct {
input int
expectedResult int
}{
{2, 4},
{3, 6},
{4, 8},
}
for _, tc := range testCases {
tc := tc // Capture range variable
t.Run(fmt.Sprintf("input=%d", tc.input), func(t *testing.T) {
result := Calculate(tc.input)
if result != tc.expectedResult {
t.Errorf("Calculate(%d) = %d, expected %d", tc.input, result, tc.expectedResult)
}
})
}
}
In this example, the TestCalculate
function contains subtests, one for each test case. Using t.Run
, we provide a descriptive name for each subtest based on the input value. If a subtest fails, the error message will include the specific input value, making it easier to identify and debug the issue.
Conclusion
In this tutorial, we learned how to write table-driven tests in Go. We started by setting up a Go project and then created a simple function and test case using the table-driven approach. We explored best practices such as structuring test cases, parameterized tests, and table-driven subtests.
Table-driven tests provide a structured and scalable way to test multiple scenarios with minimal code duplication. By applying these techniques, you can improve the quality and reliability of your Go applications.
Remember, testing is an essential part of software development, and adopting good testing practices will greatly benefit your projects.
Now it’s time to apply your knowledge and start writing table-driven tests in your own Go projects. Happy testing!
I hope you find this tutorial helpful! Let me know if you have any questions or need further clarification.