Go > Testing and Benchmarking > Unit Testing > Test functions and naming

Go Table-Driven Testing Example

Demonstrates how to use table-driven testing in Go to test multiple input/output scenarios efficiently.

Table-Driven Testing

This snippet shows how to use table-driven testing. A slice of structs defines different test cases. Each struct contains the input values (a and b), the expected output (expected), and a descriptive name (name) for the test case. The code iterates through each test case and calls t.Run to execute a subtest with the given name. This approach allows you to test multiple scenarios without duplicating the test logic.

package main

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},
		{"Mixed numbers", -2, 3, -6},
		{"Zero", 0, 5, 0},
	}

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

Benefits of Table-Driven Tests

  • Reduced Code Duplication: Avoids repeating the same test logic for different inputs.
  • Improved Readability: Makes tests easier to read and understand.
  • Easier to Add New Test Cases: Simplifies adding new test cases by simply adding a new entry to the table.
  • Better Organization: Groups related test cases together, making the test suite more organized.

The t.Run function

The t.Run function, introduced in Go 1.7, allows you to define subtests within a test function. This is particularly useful for table-driven tests because it allows you to run each test case as a separate subtest, which makes it easier to identify which test cases are failing. The first argument to t.Run is the name of the subtest, and the second argument is a function that contains the test logic for that subtest.

Concepts behind the snippet

The core concept revolves around data structures. The table is a slice of structs. Each struct represents a specific test case, clearly defining the input and the expected output. This approach centralizes the test data, making it easier to review, modify, and expand the test suite as the code evolves or new scenarios are identified. The use of t.Run further enhances the clarity of test results by providing individual success/failure notifications for each table entry.

Real-Life Use Case Section

Imagine validating user input for a form. You can use table-driven testing to verify different input types, lengths, and formats against expected outcomes (valid/invalid). You'd create a table with sample input values, expected validation results, and descriptive names like 'Valid Email', 'Invalid Email', 'Too Short Password', etc. This ensures thorough validation logic testing.

Best Practices

  • Use meaningful names for the test cases in the table.
  • Include a variety of test cases, covering edge cases and boundary conditions.
  • Keep the table concise and easy to read.
  • Consider using helper functions to simplify the test logic within each test case.
  • Make sure the test cases are independent of each other.

Interview Tip

If asked about testing in Go, demonstrate knowledge of table-driven testing. Explain its benefits and show an example of how you've used it in your projects to write more concise and readable tests. Be prepared to discuss when table-driven tests are appropriate and when other testing techniques might be more suitable.

When to use them

Table-driven tests are ideal when you need to test a function or method with multiple input values and expected outputs. This pattern is particularly useful for testing functions that perform calculations, data validation, or format conversions.

Memory footprint

The memory footprint of table-driven tests depends on the size of the table and the data types used within each test case. Keep the table as small as possible by only including the necessary test cases. Using efficient data types (e.g., ints instead of strings when possible) can also help reduce the memory footprint.

alternatives

The primary alternative is writing individual test functions for each input/output combination. However, this becomes repetitive and harder to maintain as the number of test cases increases. Fuzzing can also be an alternative, automatically generating inputs to find edge cases, but it's more suitable for finding unexpected behavior than verifying specific outputs.

pros

  • Conciseness: Significantly reduces code duplication compared to individual test functions.
  • Readability: Easier to understand the different test scenarios at a glance.
  • Maintainability: Adding, modifying, or removing test cases is straightforward.
  • Organization: Groups related test cases logically, enhancing test suite structure.

cons

  • Initial setup: Requires setting up the table structure, which might seem a bit more complex initially.
  • Error reporting: Identifying the exact source of failure within complex logic inside a table test can be slightly more challenging than with dedicated test functions, although the 'name' field is helpful here.

FAQ

  • How do I handle more complex test cases with table-driven testing?

    For more complex test cases, consider using helper functions to encapsulate the test logic within each test case. This can help keep the table concise and improve the readability of the tests.
  • Can I use table-driven testing with external dependencies?

    Yes, you can use table-driven testing with external dependencies. However, you may need to use mocking or other techniques to isolate the code being tested from the dependencies. Use interfaces to abstract your dependencies.
  • Is table driven testing appropriate for ALL tests?

    No. Table-driven tests are most appropriate when testing functions with a clear set of inputs and expected outputs. For more complex scenarios or integration tests, other testing techniques may be more appropriate.
  • How can I test for errors in table driven tests?

    Include an error field in your struct and check the returned error against the expected value using errors.Is or errors.As. You can have boolean fields in your struct to assert error type and not nil value.