notexOPENOverview
Notex/Languages/Using Stubs in Go
05.25LESSON 25 OF 29 · 9 MIN READ

Using Stubs in Go

Always free to read

So far, you’ve written tests for functions that didn’t depend on other code. This is not typical, as most code is filled with dependencies. As you saw in the related discussion, Go allows you to abstract function calls in two ways: defining a function type and defining an interface. These abstractions help you write not only modular production code but also unit tests that take advantage of stubs, which are code used during testing to return a specific value for a given input.

Tip

When your code depends on abstractions, it’s easier to write unit tests!

Let’s take a look at an example in the examples/solver directory. Define a type called Processor:

type Processor struct {
    Solver MathSolver
}

It has a field of type MathSolver:

type MathSolver interface {
    Resolve(ctx context.Context, expression string) (float64, error)
}

You’ll implement and test MathSolver in a bit.

Processor also has a method that reads an expression from an io.Reader and returns the calculated value:

func (p Processor) ProcessExpression(ctx context.Context, r io.Reader)
                                    (float64, error) {
    curExpression, err := readToNewLine(r)
    if err != nil {
        return 0, err
    }
    if len(curExpression) == 0 {
        return 0, ErrNoExpression
    }
    answer, err := p.Solver.Resolve(ctx, curExpression)
    return answer, err
}

Let’s write the code to test ProcessExpression. First, you need a stub implementation of the Resolve method to write your test:

type mathSolverStub struct{}

func (ms mathSolverStub) Resolve(ctx context.Context, expr string)
                                (float64, error) {
    switch expr {
    case "2 + 2 * 10":
        return 22, nil
    case "( 2 + 2 ) * 10":
        return 40, nil
    case "( 2 + 2 * 10":
        return 0, &InvalidExpressionErr{Expression: "( 2 + 2 * 10"}
    }
    return 0, nil
}

Next, you write a unit test that uses this stub:

func TestProcessor_ProcessExpressions(t *testing.T) {
    p := Processor{mathSolverStub{}}
    in := strings.NewReader(`2 + 2 * 10
( 2 + 2 ) * 10
( 2 + 2 * 10`)
    data := []struct {
        name string
        val  float64
        err  error
    }{
        {name: "case1", val: 22, err: nil},
        {name: "case2", val: 40, err: nil},
        {name: "case3", val: 0,
         err: &InvalidExpressionErr{Expression: "( 2 + 2 * 10"}},
    }
    for _, d := range data {
        t.Run(d.name, func(t *testing.T) {
            result, err := p.ProcessExpression(t.Context(), in)
            if !errors.Is(err, d.err) {
                t.Errorf("expected error %v, got %v", d.err, err)
            }
            if result != d.val {
                t.Errorf("Expected result %f, got %f", d.val, result)
            }
        })
    }
}

You can then run your test and see that everything works.

While most Go interfaces specify only one or two methods, you’ll sometimes find yourself with an interface that has many methods. Let’s take a look at the code in the examples/stub directory. Assume you have an interface that looks like this:

type Entities interface {
    GetUser(id string) (User, error)
    GetPets(userID string) ([]Pet, error)
    GetChildren(userID string) ([]Person, error)
    GetFriends(userID string) ([]Person, error)
    SaveUser(user User) error
}

There are two patterns for testing code that depends on large interfaces. The first is to embed the interface in a struct, which automatically defines all the interface’s methods on the struct. It doesn’t provide any implementations of those methods, so you need to implement the methods that you care about for the current test. Let’s assume that Logic is a struct that has a field of type Entities:

type Logic struct {
    Entities Entities
}

Assume you want to test this method:

func (l Logic) GetPetNames(userId string) ([]string, error) {
    pets, err := l.Entities.GetPets(userId)
    if err != nil {
        return nil, err
    }
    out := make([]string, len(pets))
    for _, p := range pets {
        out = append(out, p.Name)
    }
    return out, nil
}

This method only uses the GetPets method declared on Entities. Rather than creating a stub that implements every single method on Entities just to test GetPets, you can write a stub struct that implements only the method you need to test this method:

type getPetNamesStub struct {
    Entities
}

func (ps getPetNamesStub) GetPets(userID string) ([]Pet, error) {
    switch userID {
    case "1":
        return []Pet{{Name: "Bubbles"}}, nil
    case "2":
        return []Pet{{Name: "Stampy"}, {Name: "Snowball II"}}, nil
    default:
        return nil, &InvalidIDError{ID: userID}
    }
}

You then write your unit test, with your stub injected into Logic:

func TestLogicGetPetNames(t *testing.T) {
    data := []struct {
        name     string
        userID   string
        petNames []string
        err      error
    }{
        {"case1", "1", []string{"Bubbles"}, nil},
        {"case2", "2", []string{"Stampy", "Snowball II"}, nil},
        {"case3", "3", nil, &InvalidIDError{ID: "3"}},
    }
    l := Logic{getPetNamesStub{}}
    for _, d := range data {
        t.Run(d.name, func(t *testing.T) {
            petNames, err := l.GetPetNames(d.userID)
            if !errors.Is(err, d.err) {
                t.Errorf("expected error %v, got %v", d.err, err)
            }
            if diff := cmp.Diff(d.petNames, petNames); diff != "" {
                t.Error(diff)
            }
        })
    }
}

(By the way, the GetPetNames method has a bug. Did you see it? Even simple methods can sometimes have bugs.)

Warning

If you embed an interface in a stub struct, make sure you provide an implementation for every method that’s called during your test! If you call an unimplemented method, your tests will panic.

If you need to implement only one or two methods in an interface for a single test, this technique works well. The drawback comes when you need to call the same method in different tests with different inputs and outputs. When that happens, you need to either include every possible result for every test within the same implementation or reimplement the struct for each test. This quickly becomes difficult to understand and maintain. A better solution is to create a stub struct that proxies method calls to function fields. For each method defined on Entities, define a function field with a matching signature on your stub struct:

type entitiesStub struct {
    getUser     func(id string) (User, error)
    getPets     func(userID string) ([]Pet, error)
    getChildren func(userID string) ([]Person, error)
    getFriends  func(userID string) ([]Person, error)
    saveUser    func(user User) error
}

Then make entitiesStub meet the Entities interface by defining the methods. In each method, invoke the associated function field. For example:

func (es entitiesStub) GetUser(id string) (User, error) {
    return es.getUser(id)
}

func (es entitiesStub) GetPets(userID string) ([]Pet, error) {
    return es.getPets(userID)
}

Once you create this stub, you can supply different implementations of different methods in different test cases via the fields in the data struct for a table test:

func TestLogicGetPetNames2(t *testing.T) {
    data := []struct {
        name     string
        getPets  func(userID string) ([]Pet, error)
        userID   string
        petNames []string
        err      error
    }{
        {"case1", func(userID string) ([]Pet, error) {
            return []Pet{{Name: "Bubbles"}}, nil
        }, "1", []string{"Bubbles"}, nil},
        {"case2", func(userID string) ([]Pet, error) {
            return []Pet{{Name: "Stampy"}, {Name: "Snowball II"}}, nil
        }, "2", []string{"Stampy", "Snowball II"}, nil},
        {"case3", func(userID string) ([]Pet, error) {
            return nil, &InvalidIDError{ID: "3"}
        }, "3", nil, &InvalidIDError{ID: "3"}},
    }
    for _, d := range data {
        t.Run(d.name, func(t *testing.T) {
            l := Logic{Entities: entitiesStub{getPets: d.getPets}}
            petNames, err := l.GetPetNames(d.userID)
            if diff := cmp.Diff(d.petNames, petNames); diff != "" {
                t.Error(diff)
            }
            if !errors.Is(err, d.err) {
                t.Errorf("Expected error %v, got %v", d.err, err)
            }
        })
    }
}

Add a field of function type to data’s anonymous struct. In each test case, specify a function that returns the data that GetPets would return. When you write your test stubs this way, it’s clear what the stubs should return for each test case. Each test run instantiates a new EntitiesStub and assigns the getPets function field in the test data to the getPets function field in EntitiesStub.

Mocks and Stubs

The terms mock and stub are often used interchangeably, but they are actually two different concepts. Martin Fowler, a respected voice on all things related to software development, wrote a blog post on mocks that, among other things, covers the difference. In short, a stub returns a canned value for a given input, whereas a mock validates that a set of calls happen in the expected order with the expected inputs.

You used stubs in the examples to return canned values to a given response. You can write your own mocks by hand, or you can use a third-party library to generate them. The two most popular options are the gomock library from Google and the testify library from Stretchr, Inc.

Using httptest

It’s important to ensure that calls to HTTP services send the right requests and handle responses properly. While integration testing is essential for validating a multi-service deployment, it is often slow. Quick-running unit tests for HTTP clients provide confidence during development. The Go standard library includes the net/http/httptest package, which makes it easy to stub HTTP services. Let’s go back to the examples/solver directory and provide an implementation of MathSolver that calls an HTTP service to evaluate expressions:

type RemoteSolver struct {
    MathServerURL string
    Client        *http.Client
}

func (rs RemoteSolver) Resolve(ctx context.Context, expression string)
                              (float64, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet,
        rs.MathServerURL+"?expression="+url.QueryEscape(expression), nil)
    if err != nil {
        return 0, err
    }
    resp, err := rs.Client.Do(req)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    contents, err := io.ReadAll(resp.Body)
    if err != nil {
        return 0, err
    }
    if resp.StatusCode != http.StatusOK {
        return 0, &RequestErr{
            StatusCode: resp.StatusCode,
            Contents:   string(contents),
        }
    }
    result, err := strconv.ParseFloat(string(contents), 64)
    if err != nil {
        return 0, err
    }
    return result, nil
}

Now let’s see how to use the httptest library to test this code without standing up a server. The code is in the TestRemoteSolver_Resolve function in examples/solver/remote_solver_test.go, but here are the highlights. First, you want to make sure that the data that’s passed into the function arrives on the server. In your test function, define a type called info to hold your input and output and a variable called io that is assigned the current input and output:

type info struct {
    expression string
    code       int
    body       string
}
var io info

Next, set up your fake remote server and use it to configure an instance of RemoteSolver:

server := httptest.NewServer(
    http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        // validate the incoming request to make sure the client is
        // sending the expected data
        if req.URL.Query().Get("expression") != io.expression {
            rw.WriteHeader(http.StatusBadRequest)
            return
        }
        rw.WriteHeader(io.code)
        rw.Write([]byte(io.body))
    }))
defer server.Close()
rs := RemoteSolver{
    MathServerURL: server.URL,
    Client:        server.Client(),
}

The httptest.NewServer function creates and starts an HTTP server on a random unused port. You need to provide an http.Handler implementation to process the request. Since this is a server, you must close it when the test completes. The server instance has its URL specified in the URL field of the server instance and a preconfigured http.Client for communicating with the test server. Pass these into RemoteSolver.

The rest of the function works like every other table test that you’ve seen:

data := []struct {
    name   string
    io     info
    result float64
    err    error
}{
    {"case1", info{"2 + 2 * 10", http.StatusOK, "22"}, 22, nil},
    {"case2", info{"( 2 + 2 ) * 10", http.StatusOK, "40"}, 40, nil},
    {"case3", info{"( 2 + 2 * 10", http.StatusBadRequest,
                    "invalid expression: ( 2 + 2 * 10"}, 0,
        &RequestErr{
            StatusCode: http.StatusBadRequest,
            Contents:   "invalid expression: ( 2 + 2 * 10",
        }},
}
for _, d := range data {
    t.Run(d.name, func(t *testing.T) {
        io = d.io
        result, err := rs.Resolve(t.Context(), d.io.expression)
        if result != d.result {
            t.Errorf("io `%f`, got `%f`", d.result, result)
        }
        if !errors.Is(err, d.err) {
            t.Errorf("expected error %v, got %v", d.err, err)
        }
    })
}

The variable io has been captured by two closures: the one for the stub server and the one for running each test. You write to it in one closure and read it in the other. This is a bad idea in production code, but it works well in test code within a single function.

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K