Skip to content

ritoon/aegot

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Testing with Google App Engine for Go

Utils for testing apps that import (directly or indirectly) "appengine/*" packages.

## Why

Let's say you have a Go app that looks something like this:

package myapp

import (
  "appengine"
  "appengine/datastore"
)

func listTopics(w http.ResponseWriter, r *http.Request) {
  ctx := appengine.NewContext(r)
  // do something with ctx, e.g. use datastore to fetch some entities
}

and you have myapp_test.go:

package myapp

import "testing"

func TestListTopics(t *testing.T) {
  // do some tests here
}

Well, if you try executing go test ./myapp it won't even get to running the tests because Go won't be able to build your app & tests. It'll say something like "Can't import 'appengine'" package. That's because "appengine/*" packages are indeed in a different location (specifically, in SDK/goroot/src/pkg/).

You could symlink SDK/goroot/src/pkg/appengine to your GOPATH/src/appengine but that probably won't solve all the problems:

  • go test won't be able to build appengine package (so that later tests would run quicker)
  • SDK contains the whole Go release, but slightly modified, so you'll definitely bump into issues like "Undefined os.DisableWritesForAppEngine" because indeed, DisableWritesForAppEngine exists only in this specific Go version for App Engine.

So, this little project tries to solve these problems.

Instead of using Go version of App Engine for Go SDK, this tool will:

  • clone the original source files from code.google.com/p/appengine-go
  • apply a patch to a couple files in appengine_internal/ dir
  • build appengine packages to speedup later tests with "go test -i ..."

Usage

Let's say I'm in my app root which has a subdir called "myapp" (from the example above). You only need to do this once:

  • Install "aet" tool: go get github.com/crhym3/aegot/aet
  • Init: aet init ./myapp, which will do a couple things:
    • hg clone code.google.com/p/appengine-go
    • fetch patched versions of appengine_internal/ and overwrite the original files
    • tell Go to build appengine packages with "go test -i ./myapp" (if ./myapp argument was provided)

Sample test from the above will be able to run now with aet test ./myapp, but "aet test" doesn't do much actually. It only manipulates GOPATH env variable and adds appengine-go local clone path to it. So, alternatively, tests can be run with e.g. GOPATH=$GOPATH:$GOPATH/appengine-go go test ./myapp

$ aet -h

Usage: aet {init|test} [flags] ./path/to/*_test.go
  -c="hg clone -u": command to clone the repo; don't specify rev, url or d here
  -d="/Users/alex/go/src/appengine-go": expect appengine-go sources to be in d/src; required
  -rev="1.8.0": App Engine release version or repo revision; required for init
  -uc="hg update -r": command to update previously clonned repo
  -url="https://code.google.com/p/appengine-go/": appengine-go project repository URL

## Testutils

There's small colletion of methods that work sort of like proxies to appengine_internal which you can use to stub out App Engine internal RPCs.

For example, let's say we have myapp/handlers.go with the following content:

package myapp

import (
  "fmt"
  "net/http"

  "appengine"
  "appengine/datastore"
)

type Item struct {
  Id   string `datastore:"-"`
  Name string
}

func get(w http.ResponseWriter, r *http.Request) {
  c := appengine.NewContext(r)
  item := &Item{Id: r.URL.Path[1:]}
  switch err := item.get(c); err {
  case nil:
    fmt.Fprint(w, item.Name)
  case datastore.ErrNoSuchEntity:
    http.NotFound(w, r)
  default:
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

and myapp/items.go (note the "// +build ..." tag):

// +build appengine

package myapp

import (
  "appengine"
  "appengine/datastore"
)

func (item *Item) get(c appengine.Context) error {
  if item.Id == "" {
    return datastore.ErrNoSuchEntity
  }
  key := datastore.NewKey(c, "Item", item.Id, 0, nil)
  return datastore.Get(c, key, item)
}

If we wanted to just test handlers, we could stub the code in items.go with items_stub.go (again, note the "// +build ..." tag):

// +build !appengine

package myapp

import (
  "errors"

  "appengine"
  "appengine/datastore"
)

func (item *Item) get(c appengine.Context) error {
  switch item.Id {
  case "does-not-exist":
    return datastore.ErrNoSuchEntity
  case "error":
    return errors.New("Some fake get error")
  default:
    item.Name = item.Id
  }
  return nil
}

and, assuming you installed aet and did aet init, our handlers_test.go could look like this:

package myapp

import (
  "net/http"
  "net/http/httptest"
  "testing"

  tu "github.com/crhym3/aegot/testutils"
)

func TestGetOk(t *testing.T) {
  const itemId = "valid-id"

  r, deleteContext := tu.NewTestRequest("GET", "/"+itemId, nil)
  defer deleteContext()
  w := httptest.NewRecorder()

  get(w, r)

  if w.Code != http.StatusOK {
    t.Errorf("Expected 200, got %d", w.Code)
  }
  body := string(w.Body.Bytes())
  if body != itemId {
    t.Errorf("Expected %q, got %q", itemId, body)
  }
}

func TestGetErrors(t *testing.T) {
  tt := []*struct {
    path string
    code int
  }{
    {"/does-not-exist", 404},
    {"/error", 500},
  }
  for _, ti := range tt {
    r, deleteContext := tu.NewTestRequest("GET", ti.path, nil)
    defer deleteContext()
    w := httptest.NewRecorder()

    get(w, r)

    if w.Code != ti.code {
      t.Errorf("Expected %d, got %d", ti.code, w.Code)
    }
  }
}

So, that was easy.

What's happening here is when you run the app with dev_appserver, only these files will be used to build the app:

  • items.go
  • handlers.go

handlers_test.go is skipped because it ends with "_test.go", and items_stub.go is ignored because it has // +build !appengine tag at the beginning of file which tells go-app-builder to ignore it when building an app for App Engine.

Now, when you run aet test ./myapp (or GOPATH=... go test ./myapp), these files will be used to build the app + tests:

  • items_stub.go
  • handlers.go
  • handlers_test.go

items.go is ignored because it has // +build appengine tag and since the tag is not present when you run go test (see go help build for -tags option) it'll skip the file.


Now, immagine that you needed to test code in items.go for some reason. Well, you could do that by stubbing out "datastore_v3" service methods. For instance, items_test.go:

package myapp

import (
  "testing"

  "appengine"  
  "code.google.com/p/goprotobuf/proto"
  pb "appengine_internal/datastore"

  tu "github.com/crhym3/aegot/testutils"
)

func TestPutItem(t *testing.T) {
  const (
    itemId   = "some-id"
    itemName = "test"
  )

  putStub := func(in, out proto.Message, _ *tu.RpcCallOptions) error {
    req := in.(*pb.PutRequest)

    if len(req.GetEntity()) != 1 {
      t.Error("Expected 1 entity, got %d", len(req.GetEntity()))
    }
    ent := req.GetEntity()[0]
    id := ent.GetKey().GetPath().GetElement()[0].GetName()
    if id != itemId {
      t.Error("Expected ID %q, got %q", itemId, id)
    }
    if len(ent.GetProperty()) != 1 {
      t.Error("Expected 1 property, got: %d", len(ent.GetProperty()))
    }
    prop := ent.GetProperty()[0]
    if prop.GetName() != "Name" {
      t.Error("Invalid property name: %q", prop.GetName())
    }
    val := prop.GetValue().GetStringValue()
    if val != itemName {
      t.Error("Expected %q, got %q", itemName, val)
    }

    resp := out.(*pb.PutResponse)
    resp.Key = []*pb.Reference{ent.GetKey()}
    return nil
  }
  unregister := tu.RegisterAPIOverride("datastore_v3", "Put", putStub)
  defer unregister()

  r, deleteContext := tu.NewTestRequest("PUT", "/"+itemId, nil)
  defer deleteContext()

  item := Item{Id: itemId, Name: itemName}
  // appengine.NewContext() will use the one created in NewTestRequest() above
  if err := item.put(appengine.NewContext(r)); err != nil {
    t.Error(err)
  }
}

Note that in this case we don't use "// +build ..." tags because we want to test the actual code in items.go.

For more examples see:

Testutils full documentation.

Alternatives

You might also want to check out other projects:

About

Utils for testing apps that import (directly or indirectly) "appengine/*" packages

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 100.0%