How to Write Table-Driven Tests in Go

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setup
  4. Creating Table-Driven Tests
  5. Examples and Best Practices
  6. Conclusion

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:

  1. Create a new directory for your project: mkdir table-driven-tests
  2. Navigate to the project directory: cd table-driven-tests
  3. Initialize a new Go module: go mod init example.com/table-driven-tests
  4. Create a new Go file: touch example_test.go

  5. 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.