Go > Testing and Benchmarking > Unit Testing > Testing with the testing package

Table-Driven Testing

This snippet demonstrates table-driven testing, a popular and efficient technique for writing unit tests in Go when you have multiple test cases for the same function.

Table-Driven Test Structure

This code defines a `Multiply` function and its table-driven test function `TestMultiply`. - A slice of structs is defined, where each struct represents a test case. Each test case includes a name, input values (`a` and `b`), and the expected result. - The test iterates through the test cases in the table. - `t.Run` creates a subtest for each test case, allowing for individual test results and easier debugging. The subtest name is taken from the `test.name` field. - Inside the subtest, the `Multiply` function is called with the input values from the current test case, and the result is compared to the expected value. - `t.Errorf` is used to report an error if the result doesn't match the expectation, including the test name and input values for easier identification of the failing case.

package mypackage

import "testing"

func Multiply(a, b int) int {
	return a * b
}

func TestMultiply(t *testing.T) {
	table := []struct {
		name     string
		a, b     int
		expected int
	}{
		{"Positive Numbers", 2, 3, 6},
		{"Negative Numbers", -2, 3, -6},
		{"Zero", 0, 5, 0},
		{"Both Negative", -2, -3, 6},
	}

	for _, test := range table {
		t.Run(test.name, func(t *testing.T) {
			result := Multiply(test.a, test.b)
			if result != test.expected {
				t.Errorf("Test: %s, Multiply(%d, %d) = %d; expected %d", test.name, test.a, test.b, result, test.expected)
			}
		})
	}
}

Concepts Behind the Snippet

Table-driven testing allows you to write a single test function that executes multiple test cases. This reduces code duplication and makes your tests more organized and maintainable. The key is defining a data structure (typically a slice of structs) that holds the different input values and expected outputs for each test case.

Real-Life Use Case

Consider testing a function that validates email addresses. You can create a table of test cases that includes valid email addresses, invalid email addresses (e.g., missing `@`, invalid characters), and edge cases (e.g., very long email addresses). Table-driven testing makes it easy to add new test cases as needed.

Best Practices

  • Use descriptive names for test cases: This helps to quickly identify failing test cases.
  • Group related test cases together: This improves readability and maintainability.
  • Keep the table data separate from the test logic: This makes it easier to modify the test cases without affecting the test logic.
  • Consider using constants or variables for common expected values: This reduces duplication and improves consistency.

Interview Tip

Be able to explain the benefits of table-driven testing compared to writing separate test functions for each scenario. Emphasize its efficiency in terms of code reuse and maintainability. Also, be prepared to discuss when table-driven testing is most appropriate (e.g., when testing a function with multiple input/output combinations).

When to Use Them

Use table-driven testing when you need to test a function with multiple different inputs and expected outputs. It's particularly useful when the logic being tested is relatively simple, but you need to cover a wide range of scenarios. It's a great way to test functions with lots of edge cases or boundary conditions.

Memory Footprint

The memory footprint is generally low, but can increase if the test table becomes very large. Ensure that the table only contains necessary data and avoids unnecessary copies of large objects.

Alternatives

The primary alternative is writing separate test functions for each test case. However, this can lead to significant code duplication and makes it harder to maintain the tests. Another alternative for complex scenarios might involve using parameterized testing libraries if they exist for Go, though standard table-driven testing is usually preferred for its simplicity.

Pros

  • Reduces code duplication: One test function can handle multiple test cases.
  • Improves readability and maintainability: Test cases are organized in a structured table.
  • Easy to add new test cases: Simply add a new entry to the table.

Cons

  • Can be less readable if the table becomes too large or complex.
  • May require some initial setup to create the test table.

FAQ

  • What is `t.Run` used for?

    `t.Run` creates a subtest with a specific name. This allows you to run individual test cases within a larger test function and get separate results for each case. It also helps with debugging, as you can easily identify which test case failed.
  • How do I handle errors within a subtest?

    Use the standard `t.Errorf`, `t.Fatalf`, `t.Logf` functions within the subtest's function. `t.Fatalf` will terminate the current subtest immediately after reporting the error. `t.Errorf` continues execution of the subtest after reporting the error.