8. Testing
Up to now you tested by hand, with curl. In this chapter you write tests that do the same thing in a second, every time you change something. They test through HTTP, the way clients use the API, against the real test database.
Test the task preá
A preá's tests live next to its code. Create preas/task/task_test.go, starting with a small helper that sends a request and decodes the reply:
// send makes a request to the app and decodes the data of the reply.
func send(t *testing.T, app http.Handler, method, path, body string) (int, Task) {
t.Helper()
w := httptest.NewRecorder()
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
app.ServeHTTP(w, req)
var reply struct{ Data Task }
if w.Code < 300 && w.Code != http.StatusNoContent {
if err := json.Unmarshal(w.Body.Bytes(), &reply); err != nil {
t.Fatalf("%s %s: %v: %s", method, path, err, w.Body)
}
}
return w.Code, reply.Data
}Then the test:
func TestTask(t *testing.T) {
// Only this preá (and list, which it imports), with the tests profile.
app := prea.Test(t, prea.Config{Modules: []*core.Module{Module}})
code, task := send(t, app, "POST", "/task", `{"title": " Buy coffee "}`)
if code != http.StatusCreated || task.Title != "Buy coffee" || task.Done {
t.Fatalf("create: %d %+v", code, task)
}
code, task = send(t, app, "PATCH", "/task/"+task.ID, `{"done": true}`)
if code != http.StatusOK || !task.Done || task.Title != "Buy coffee" {
t.Fatalf("complete: %d %+v", code, task)
}
if code, _ := send(t, app, "POST", "/task", `{"title": " "}`); code != http.StatusBadRequest {
t.Errorf("blank title: %d, want 400", code)
}
unknown := `{"title": "Call mom", "listId": "6d0b1f5e-3c2a-4b7e-9f10-2a3b4c5d6e7f"}`
if code, _ := send(t, app, "POST", "/task", unknown); code != http.StatusUnprocessableEntity {
t.Errorf("unknown list: %d, want 422", code)
}
}prea.Test builds the app the way prea dev does, but with the tests profile, and only with the preás you pass: here task, and list, which it imports. It returns the app as an http.Handler, so the requests never touch the network. When the test ends, it closes everything.
Remember create-drop in the tests profile: the tables are created when the test builds the app, and dropped at the end. Every run starts empty.
Test the whole app
Tests in tests/ build the app with everything main.go has, including the API key middleware. The project already has tests/app_test.go, with a helper that builds the whole app. Add a test for the key:
func TestWritesNeedTheKey(t *testing.T) {
app := app(t)
post := func(key string) int {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/list", strings.NewReader(`{"name": "Groceries"}`))
req.Header.Set("Content-Type", "application/json")
if key != "" {
req.Header.Set("X-API-Key", key)
}
app.ServeHTTP(w, req)
return w.Code
}
if code := post(""); code != http.StatusUnauthorized {
t.Errorf("without the key: %d, want 401", code)
}
// The key of config/application-tests.yaml.
if code := post("tests-key-0123456789"); code != http.StatusCreated {
t.Errorf("with the key: %d, want 201", code)
}
}The key comes from config/application-tests.yaml, the one you added in the last chapter.
Run them
prea testsok todo/preas/health 0.330s
ok todo/preas/task 0.389s
ok todo/tests 0.411sprea tests runs the packages one after the other, because they all use the same test database.
Go's test flags work as usual: prea tests -run TestTask -v runs one test and shows each step.
Try breaking a rule on purpose, like changing maxTitle to 2, and run the tests again. They catch it.
What you learned
prea.Testbuilds the app for a test, with thetestsprofile, and returns anhttp.Handler.- A preá's tests live next to it; tests of the whole app live in
tests/. create-dropgives every run empty tables.