In this article I'll share 3 incredibly useful tips for testing Go Apps

Are you a Go programmer or looking to learn Go?

Yes, then this article would be very useful in learning the different ways of testing your code.

Go has a built in testing library. This article is aimed for those who already have some familiarity with the library & who have used it at least few times for testing. Also, you should be familiar with Go concepts like interfaces, composition etc.

Testing often includes mocks and stubs. If you are coming from other languages like Python or Java, there's a good chance that you may go wrong the first time. This article is in no way meant to be a conclusive guide to testing in Go. However, the article is quite useful in learning the right way of writing tests on Go.

1. Use table driven tests with Go subtests

For those who are unfamiliar with table driven tests, we create a table of test cases:

testCases := []struct {
    input          string
    expectedOutput bool
}{
    {
        input:          "correct",
        expectedOutput: true,
    },
    {
        input:          "incorrect",
        expectedOutput: false,
    },
}

Then we iterate through them, and run tests for each item:

for _, tc := range testCases {
    t.Run(
        fmt.Sprintf("for input: %s", tc.input),
        func(t *testing.T) {
            output, err := runMethod(tc.input)
            if err != nil {
                t.Errorf("runMethod failed, error: %s", err)
            }
            if output != tc.expectedOutput {
                t.Errorf(
                    "Expected: %s, Received: %s",
                    tc.expectedOutput,
                    output,
                )
            }

        })
}

Go 1.7 added support for subtests and sub-benchmarks which makes table driven tests much better.

Notice the "t.Run" method. It takes the subtest name as the first argument and the test function for the test cases as the second argument. This creates a subtest for each of our test cases. This has many benefits like cleaner test output, ability to run a single subtest using the -run flag and subtest name:  

go test -run=TestMethod/"For input: correct"

To know more about subtests: https://blog.golang.org/subtests

connect

2. Use interfaces

Stubs and mocks are commonly used in many languages to fake the results of a method or a dependency. In Go, interfaces should be used for the same. Interfaces offer a lot of flexibility when used correctly.

Suppose you have an external dependency in our application, say an http request. You could implement it like this:

func DoSomething() (error){
	...
	resp, err := getDataFromWeb()
	//do something with resp
	...
}
func getDataFromWeb()(map[string]interface{}, error) {
	...
	resp, err := http.Get(url)
	…
	return data, nil
}

But this is not very testable. Your tests would fail if the external dependency, in this case, an http request, fails. Also in many situations, it wouldn’t be possible to pass the correct request parameters while testing, as you have to deal with rate limits etc.

Instead, we could do something like this:

type webInterface interface {
	getDataFromWeb()(map[string]interface{}, error)
}

func DoSomething(web webInterface) (error){
	...
	resp, err := web.getDataFromWeb()
	//do something with resp
	...
}

While testing, we could do this:

type webStub struct{}

func (w *webStub) getDataFromWeb() (map[string]interface{}, error) {
	return map[string]interface{}{
		"name": "TestName",
	}, nil
}

Here we call the getDataFromWeb method of the struct that is passed into DoSomething. Here webStub struct satisfies the interface webInterface and can be used to test DoSomething.

func TestDoSomething(t *testing.T) {
	…
	var w *webStub
	err := DoSomething(w)
	if err != nil {
		t.Errorf(
			"Expected err to be %q but it was %q",
			expectedError,
			err,
		)
	}	
}

This concept can be used to mock the internal service dependencies, grpc calls etc.

Although interfaces offer great control and flexibility, if not used properly can cause its own problems. Sometimes the interfaces get too large and the structs that implement these methods may also want to use some of these methods internally. This makes selectively implementing some methods and testing others often impossible.

In such situations it is important to use composition, which is basically breaking up the interface as a composition of several smaller interfaces.

3. Use httptest for testing http Handlers

Go standard library provides httptest package that contains helpers for testing http end to end tests and unit testing http handlers.

httptest.ResponseRecorder implements the http.ResponseWriter interface and can be passed in to an http Handler. This can be used to test the response from the http handler.

First we create a ResponseRecorder:

rr := httptest.NewRecorder()

Then we create the request:

req := httptest.NewRequest(method, url, body)
req.Header.Set("Authorization", token)

Then we test the handler:

err := handlerToTest(rr, req)

The ResponseRecorder passed into the handler records the response, which we can use for the tests:

if status := rr.Code; status != http.StatusOK {
    t.Errorf(
        "handler returned wrong status code: got %v want %v",
        status,
        http.StatusOK,
    )
}

For integration tests, you can setup a temporary http server using httptest.NewServer() and make requests to this server.

Here are some of the important tips I personally learned while writing tests for Go code. I would recommend taking enough time to learn the best practices in testing as it will help you write code that is testable. Also, writing testable code often leads to better quality code.

connect