liveBook · Manning

Table-driven test

As explained in Go in Action, Second Edition

A table-driven test represents multiple test cases as data records and runs a common test procedure over them.

A table-driven test represents multiple cases as data records and runs one validation procedure over them. It replaces repeated test code with a struct, a slice, and a loop. section 8.5.5

Build the test around the values that vary:

  • Define a test-case struct containing every input and expected result needed for one iteration. section 8.5.5
  • Create a slice whose records represent individual cases, grouped together at the start of the test. section 8.5.5
  • Loop over the slice, execute the shared procedure, log the case values, and compare actual results with expectations. section 8.5.5
  • Add or remove scenarios by changing data records instead of duplicating testing logic. section 8.5.5

Calculator permutations

A Calculate test stores the operation, two operands, and expected result in each record, then checks division, addition, multiplication, and subtraction through one loop. When an operation returns an error, the test reports it and continues to the next case.

Listing 8.30 handler_test.go: Table test for handlers
func TestHandler(t *testing.T) {
	// Arrange
	tt := []struct { #1
		Handler          http.HandlerFunc
		Action           string
		Body             []byte
		StatusCode       int
		ExpectedResponse string
	}{
		{
			Handler:          hello.HelloGet,
			Action:           "GET",
			StatusCode:       http.StatusOK,
			Body:             nil,
			ExpectedResponse: "hello",
		},
		{
			Handler:          hello.HelloPost,
			Action:           "POST",
			StatusCode:       http.StatusCreated,
			Body:             []byte("Go in Action"),
			ExpectedResponse: "hello Go in Action",
		},
	}

	for _, test := range tt { #2
		handler := http.HandlerFunc(test.Handler)
		rr := httptest.NewRecorder()

		req, err := http.NewRequest(
		test.Action, "", bytes.NewBuffer(test.Body))
		if err != nil {
			t.Errorf("unable to create request %s", err)
			t.FailNow()
		}

		// Act
		handler.ServeHTTP(rr, req)

		// Assert
		if rr.Code != test.StatusCode {
			t.Errorf(`expected status %d but received %d`,
				test.StatusCode, rr.Code)
		}

		respBody, err := io.ReadAll(rr.Body)
		if err != nil {
			t.Errorf("unable to read response %s", err)
			t.FailNow()
		}
		if string(respBody) != test.ExpectedResponse {
			t.Errorf("expected '%s' received '%s'",
			test.ExpectedResponse, string(respBody))
			t.Fail()
		}
	}

}

Inspect a handler table that varies HTTP method, request body, expected status, and response while reusing the same request-and-assert procedure. section 8.6

How Go in Action, Second Edition covers it

How deep it goes
Explains struct design, slice construction, looping, logging, and maintenance benefits, but does not develop advanced table-test variations.
Examples
Uses Calculator operations and HTTP GET, POST, and unsupported-method handler cases as concrete test tables.
What it assumes
Assumes familiarity with Go functions, structs, slices, loops, and ordinary test assertions rather than teaching those constructs from zero.
What you can do after
Apply the pattern by defining case records, iterating over them, logging inputs, and asserting each result with go test.
How it connects
Connects table-driven tests with unit testing, test coverage, HTTP handlers, and test-driven development.

Also explained in

Compare how 6 books explain table-driven test →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Go in Action, Second Edition. The section links point to the parts of the book it draws on.