Skip to content

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:

go
// 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:

go
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:

go
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 ​

bash
prea tests
ok  	todo/preas/health	0.330s
ok  	todo/preas/task	0.389s
ok  	todo/tests	0.411s

prea 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.Test builds the app for a test, with the tests profile, and returns an http.Handler.
  • A preá's tests live next to it; tests of the whole app live in tests/.
  • create-drop gives every run empty tables.

Next: ship it →

Released under the MIT License.