Local Unit Testing for Go

Unit testing allows you to check the quality of your code after you've written it, but you can also use unit testing to improve your development process. Instead of writing tests after you finish developing your application, consider writing the tests as you write the app. This helps you design small, maintainable, reusable units of code. It also makes it easier for you to test your code thoroughly and quickly.

When you do local unit testing, you run tests that stay inside your own development environment without involving remote components. App Engine provides testing utilities that use local implementations of datastore and other App Engine services. This means you can exercise your code's use of these services locally, without deploying your code to App Engine.

The development services simulate the behaviour of the real service locally for testing. For example, the datastore usage shown in Writing Datastore and Memcache Tests allows you to test your datastore code without making any requests to the real datastore. Any entity stored during a datastore unit test is stored locally and is deleted after the test run. You can run small, fast tests without any dependency on the datastore itself.

This document describes how to write unit tests against local App Engine services using the Go testing package.

Introducing the Go testing package

The original App Engine SDK for Go comes bundled with goapp, which automates the downloading, building, and testing of Go packages: it is the App Engine-equivalent of the standard go tool.

In particular, the combination of the goapp test command and the standard Go testing package can be used to run unit tests against your application code. For a background on testing with Go, see the Testing section of How to Write Go Code and the testing package reference.

Unit tests are contained in files ending with the suffix _test.go. For example, suppose you want to test a function named composeNewsletter which returns a *mail.Message. The following newsletter_test.go file shows a simple test for that function:

package newsletter

import (
	"reflect"
	"testing"

	"google.golang.org/appengine/mail"
)

func TestComposeNewsletter(t *testing.T) {
	want := &mail.Message{
		Sender:  "newsletter@appspot.com",
		To:      []string{"User <user@example.com>"},
		Subject: "Weekly App Engine Update",
		Body:    "Don't forget to test your code!",
	}
	if msg := composeNewsletter(); !reflect.DeepEqual(msg, want) {
		t.Errorf("composeMessage() = %+v, want %+v", msg, want)
	}
}

This test would be invoked using the goapp test command from within the package's directory:

goapp test

The goapp tool is found in the root directory of the App Engine SDK. We recommend putting this directory in your system's PATH variable to make running tests simpler.

Introducing the aetest package

Many function calls to App Engine services require a context.Context as an argument. The appengine/aetest package provided with the SDK allows you to create a fake context.Context to run your tests using the services provided in the development environment.

import (
	"testing"

	"google.golang.org/appengine/aetest"
)

func TestWithContext(t *testing.T) {
	ctx, done, err := aetest.NewContext()
	if err != nil {
		t.Fatal(err)
	}
	defer done()

	// Run code and tests requiring the context.Context using ctx.
	// ...
}

The call to aetest.NewContext will start dev_appserver.py in a subprocess, which will be used to service API calls during the test. This subprocess will be shutdown with the call to done.

For more control over the underlying instance, you can use aetest.NewInstance instead. This gives you the ability to create multiple contexts, and to associate those with http.Request objects.

import (
	"errors"
	"testing"

	"golang.org/x/net/context"

	"google.golang.org/appengine"
	"google.golang.org/appengine/aetest"
	"google.golang.org/appengine/datastore"
)

func TestMyFunction(t *testing.T) {
	inst, err := aetest.NewInstance(nil)
	if err != nil {
		t.Fatalf("Failed to create instance: %v", err)
	}
	defer inst.Close()

	req1, err := inst.NewRequest("GET", "/gophers", nil)
	if err != nil {
		t.Fatalf("Failed to create req1: %v", err)
	}
	c1 := appengine.NewContext(req1)

	req2, err := inst.NewRequest("GET", "/herons", nil)
	if err != nil {
		t.Fatalf("Failed to create req2: %v", err)
	}
	c2 := appengine.NewContext(req2)

	// Run code and tests with *http.Request req1 and req2,
	// and context.Context c1 and c2.
	// ...
}

Read the aetest package reference for more information.

Writing Datastore and memcache tests

Testing code which uses the datastore or memcache is simple once you create a context.Context with the aetest package: in your test call aetest.NewContext to create a context to pass to the function under test.

The transaction demo application in the SDK has an example of structuring the code to allow testability, and how to test code which uses the datastore:

func TestWithdrawLowBal(t *testing.T) {
	ctx, done, err := aetest.NewContext()
	if err != nil {
		t.Fatal(err)
	}
	defer done()
	key := datastore.NewKey(ctx, "BankAccount", "", 1, nil)
	if _, err := datastore.Put(ctx, key, &BankAccount{100}); err != nil {
		t.Fatal(err)
	}

	err = withdraw(ctx, "myid", 128, 0)
	if err == nil || err.Error() != "insufficient funds" {
		t.Errorf("Error: %v; want insufficient funds error", err)
	}

	b := BankAccount{}
	if err := datastore.Get(ctx, key, &b); err != nil {
		t.Fatal(err)
	}
	if bal, want := b.Balance, 100; bal != want {
		t.Errorf("Balance %d, want %d", bal, want)
	}
}

This test can be run using the goapp test command:

goapp test ./demos/transaction

Tests for memcache follow the same pattern: set up the intial state of the memcache in your test, run the function being tested, and verify the function has queried/modified the memcache in the way you expect.

Monitor your resources on the go

Get the Google Cloud Console app to help you manage your projects.

Send feedback about...

App Engine standard environment for Go