Testing
A Preá app is tested through HTTP, the way its clients use it. You build the app in the test, send it requests and check the replies. No server, no ports.
What you'll learn
- Where tests live
- How
prea.Testbuilds the app for a test - How to test one preá, and the whole app
- How the tests get their own database
Run the tests
prea testsprea tests runs go test ./... with the tests profile, one package at a time. Go's test flags pass through: prea tests -run TestTask -v, prea tests ./preas/task.
Where tests live
- A preá's tests sit next to its code, like
preas/task/task_test.go, in the same package. They can see the preá's unexported parts and build only that preá. - Tests of the whole app live in
tests/. They build every preá, middleware and protection, exactly likemain.go.
Test a preá
prea.Test builds the app like prea.Run does, with the tests profile, and returns it as an http.Handler:
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)
}
}It only builds the preás you pass, here task and the list it imports. The small send helper makes the requests:
// 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
}prea.Test works from any package. It finds the project root, so config/ and migrations/ are always found, and it closes the app and the database when the test ends.
Test the whole app
Tests in tests/ build the app with everything main.go uses:
func app(t *testing.T) http.Handler {
return prea.Test(t, prea.Config{
Modules: preas.Modules,
Middlewares: preas.Middlewares,
Security: preas.Security,
Logging: preas.Logging,
})
}That makes them the right place to check what crosses preás, like a global middleware:
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 test database
The tests profile points to its own database and recreates the tables on every run:
# config/application-tests.yaml
database:
url: ${TEST_DATABASE_URL}
schema: create-dropcreate-drop creates the models' tables when the test builds the app and drops them when it ends. Every test starts with empty tables, and your development data is never touched.
One package at a time
Every test package uses the same test database, so prea tests runs the packages one after the other: two packages creating the same tables at once would collide. Running go test yourself, pass -p 1 for the same effect.
Replace a component
To test a service without its real dependencies, fill the field in the declaration. Preá keeps fields you fill. This works best when the field is an interface:
type Service struct {
Repo *Repository `inject:""`
Lists ListChecker `inject:""` // an interface: Exists(ctx, id) (bool, error)
}type fakeLists struct{}
func (fakeLists) Exists(ctx context.Context, id string) (bool, error) { return true, nil }
func TestWithFakeLists(t *testing.T) {
module := &core.Module{
Name: "task",
Providers: []any{Repository{}, Service{Lists: fakeLists{}}, Handler{}},
}
app := prea.Test(t, prea.Config{Modules: []*core.Module{module}})
// ...
}The fake replaces the real list service only in this test. Depending on interfaces between preás makes this possible.
Next steps
- Deploying
- Build a todo API, with tests in chapter 8