9

Local unit testing is supported from version 1.8.6 of the Google App Engine Go SDK. The appengine/aetest package allows me to create a Context to unit test with.

How can I use this with net/http/httptest to test my HTTP handlers?

Dan
  • 5,013
  • 5
  • 33
  • 59

2 Answers2

16

See the top of goroot/src/pkg/appengine/aetest/context.go (updated source is not yet posted at https://code.google.com/p/appengine-go). At first glance, the new testing app looks to be a slightly beefier/different version of appenginetesting so you can do the same sorts of tests, see here for one way to do it with how sampleHandler(w http.ResponseWriter, r *http.Request) is called.

Alternatively, you can make your http.Handler's ContextHandler like as below:

type ContextHandler struct {
    Real func(*appengine.Context, http.ResponseWriter, *http.Request)
}

func (f ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    f.Real(c, w, r)
}

func myNewHandler(c appengine.Context, w http.ResponseWriter, r *http.Request) {
// do something
}

Then you can do this in init() to support production:

http.Handle("/myNewHandler", ContextHandler{myNewHandler})

This makes testing the function easy:

func TestMyNewHandler(t *testing.T) {
    c := aetest.NewContext()
    r, _ := http.NewRequest("GET", "/tasks/findOverdueSchedules", nil)
    w := httptest.NewRecorder()
    myNewHandler(c, w, r)
    if 200 != w.Code {
        t.Fail()
    }
}

Here's what's from context.go inside appengine/aetest:

/* Package aetest provides an appengine.Context for use in tests.

An example test file: package foo_test

import (
    "testing"

    "appengine/memcache"
    "appengine/aetest"
)

func TestFoo(t *testing.T) {
    c, err := aetest.NewContext(nil)
    if err != nil {
        t.Fatal(err)
    }
    defer c.Close()

    it := &memcache.Item{
        Key:   "some-key",
        Value: []byte("some-value"),
    }
    err = memcache.Set(c, it)
    if err != nil {
        t.Fatalf("Set err: %v", err)
    }
    it, err = memcache.Get(c, "some-key")
    if err != nil {
        t.Fatalf("Get err: %v; want no error", err)
    }
    if g, w := string(it.Value), "some-value" ; g != w {
        t.Errorf("retrieved Item.Value = %q, want %q", g, w)
    }
}

The environment variable APPENGINE_API_SERVER specifies the location of the api_server.py executable to use. If unset, the system PATH is consulted. */

mzimmerman
  • 910
  • 6
  • 13
  • It does not seem too elegant to have `var contextCreator func(r *http.Request) appengine.Context = appengine.NewContext` (https://github.com/icub3d/appenginetesting/blob/master/recorder_test.go#L14) in all the non test code handlers. – Dan Oct 16 '13 at 19:26
  • It doesn't seem elegant to have c := appengine.NewContext(r) inside each http.Handler either. I've updated my post to include how I do it. – mzimmerman Oct 17 '13 at 15:55
  • I agree with you and this seems like a great solution. In fact I am using the Gorilla Toolkit and abstracted your example slightly to use [`github.com/gorilla/context`](http://www.gorillatoolkit.org/pkg/context) which enables me to add all sorts of other stuff related to setting up the request. – Dan Oct 18 '13 at 10:41
1

If you are not opposed to using Martini, dependency injection is a nice way to solve the problem. Here is how you can set up your test (with Ginkgo):

var _ = Describe("Items", func() {

    var (
        m *martini.ClassicMartini
    )

    BeforeEach(func() {
        m = martini.Classic()

        // injects app engine context into requests
        m.Use(func(c martini.Context, req *http.Request) {
             con, _ := aetest.NewContext(nil)
             c.MapTo(con, (*appengine.Context)(nil))
        })

        m.Get("/items", func(c martini.Context){
             // code you want to test
        })
    })

    It("should get items", func() {
        recorder := httptest.NewRecorder()
        r, _ := http.NewRequest("GET", "/items", nil)
        m.ServeHTTP(recorder, r) // martini server used
        Expect(recorder.Code).To(Equal(200))
    })
})

In the production the actual context would be injected:

m.Use(func(c martini.Context, req *http.Request) {
    c.MapTo(appengine.NewContext(req), (*appengine.Context)(nil))
})
Logan
  • 349
  • 2
  • 3