liveBook · Manning

Table-driven test

As explained in Shipping Go: Develop, deliver, discuss, design, and go again

Table-driven testing defines multiple test cases as structured data and executes a common test procedure for each case.

When several tests call the same function and repeat nearly identical assertions, adding the next case becomes tedious. A table-driven test puts the inputs and expected results together as data, then runs one shared check for each case. In Go, this keeps the assertion small and makes new scenarios easier to add. section 3.3

Listing 3.7 translator_test.go
package translation_test
 
import (
    "testing"
    "github.com/holmes89/hello-api/translation"
)
 
func TestTranslate(t *testing.T) {
    // Arrange
    tt := []struct {                                             #1
        Word        string
        Language    string
        Translation string
    }{
        {                                                        #2
            Word:        "hello",
            Language:    "english",
            Translation: "hello",
        },
        {
            Word:        "hello",
            Language:    "german",
            Translation: "hallo",
        },
        {
            Word:        "hello",
            Language:    "finnish",
            Translation: "hei",
        },
        {
            Word:        "hello",
            Language:    "dutch",
            Translation: "",
        },
    }
 
    for _, test := range tt {                                    #3
        // Act
        res := translation.Translate(test.Word, test.Language)   #4
 
        // Assert
        if res != test.Translation {                             #5
            t.Errorf(
                `expected "%s" to be "%s" from "%s" but received "%s"`,
                test.Word, test.Language, test.Translation, res)
        }
    }
}

The translation test gathers words, languages, and expected translations in a slice of structs before checking the cases with shared test code. section 3.3

Start with the behavior already covered, run the tests after refactoring, and then add cases that challenge your assumptions. For a translator intended to handle only “hello,” an unfamiliar word is worth testing; casing and extra spaces are further input cases. Let failures guide the next small implementation change rather than designing far ahead. section 3.3

Listing 3.16 translator_test.go
func TestTranslateAPI(t *testing.T) {
    tt := []struct {                                           #1
        Endpoint            string
        StatusCode          int
        ExpectedLanguage    string
        ExpectedTranslation string
    }{
        {
            Endpoint:            "/hello",
            StatusCode:          http.StatusOK,
            ExpectedLanguage:    "english",
            ExpectedTranslation: "hello",
        },
        {
            Endpoint:            "/hello?language=german",
            StatusCode:          http.StatusOK,
            ExpectedLanguage:    "german",
            ExpectedTranslation: "hallo",
        },
    }
 
    handler := http.HandlerFunc(rest.TranslateHandler)         #2
 
    for _, test := range tt {                                  #3
        rr := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", test.Endpoint, nil)
 
        handler.ServeHTTP(rr, req)
 
        if rr.Code != test.StatusCode {
            t.Errorf(`expected status %d but received %d`,
                test.StatusCode, rr.Code)
        }
 
        var resp rest.Resp
        json.Unmarshal(rr.Body.Bytes(), &resp)
 
        if resp.Language != test.ExpectedLanguage {
            t.Errorf(`expected language "%s" but received %s`,
                test.ExpectedLanguage, resp.Language)
        }
 
        if resp.Translation != test.ExpectedTranslation {
            t.Errorf(`expected Translation "%s" but received %s`,
                test.ExpectedTranslation, resp.Translation)
        }
    }
}

The same pattern extends to an HTTP handler: each row supplies an endpoint and expected status, language, and translation, while the test loops through the rows. section 3.5

Add a missing-translation case

After testing successful requests for “/hello” and “/hello?language=german,” add “/hello?language=dutch” with an expected 404 status and empty response values. Run the test, observe the failure, and update the handler to return Not Found when no translation exists. section 3.5

How Shipping Go: Develop, deliver, discuss, design, and go again covers it

How deep it goes
Works through struct-based cases, shared checks, and incremental additions without discussing test-framework internals.
Examples
Tests translations across languages, then checks HTTP language queries and a missing-translation 404.
What it assumes
Assumes familiarity with writing Go tests while showing the case-table arrangement through concrete test code.
What you can do after
Practice/perform refactoring repeated Go tests into case tables and adding cases that expose missing behavior.
How it connects
Connects test refactoring and edge-case testing with HTTP status codes and JSON response checks.

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 Shipping Go: Develop, deliver, discuss, design, and go again. The section links point to the parts of the book it draws on.