On this page

On this page

HTTP Tests

Introduction

Feature tests hit your routes without opening a real TCP port. TestClient (and TestCase helpers that wrap it) build a Request, run it through the same kernel your server uses, and return a TestResponse with fluent asserts.

Import from @bunyad/testing. Boot the client with your app’s createApplication so providers, routes, and middleware match production.

Making requests

const response = await this.get("/");
const json = await this.getJson("/api/users");

await this.post("/login", { email: "ada@example.com", password: "secret" });
await this.postJson("/api/users", { name: "Ada" });

await this.putJson(`/api/users/${id}`, { name: "Ada Lovelace" });
await this.patchJson(`/api/users/${id}`, { name: "Ada" });
await this.deleteJson(`/api/users/${id}`);

*Json helpers set Accept: application/json (and Content-Type on writes). Non-GET requests warm a session cookie and attach X-CSRF-TOKEN when the app issues one, so browser-style form posts work without extra setup.

Pass extra headers as the last argument:

await this.get("/report", { "X-Request-Id": "abc" });

Authentication

Session auth uses actingAs. The client needs a session cookie and app.instance("session.store") (starters wire this):

import User from "@/Models/User.ts";

const user = await User.create({
  name: "Ada",
  email: "ada@example.com",
  password: await Hash.make("secret"),
});

await this.actingAs(user);
const response = await this.get("/dashboard");
response.assertOk();

Bearer tokens use withToken / withoutToken:

this.withToken("1|plain-text-token");
const response = await this.getJson("/api/user");
this.withoutToken();

Authorization helpers

When a user is acting, assert Gate / policy outcomes without a full HTTP round trip:

await this.actingAs(owner);
await this.assertCan("delete", post);

await this.actingAs(other);
await this.assertCannot("delete", post);

can(ability, …args) returns a boolean if you prefer an explicit check.

Status asserts

response.assertOk(); // 200
response.assertSuccessful(); // 2xx
response.assertCreated(); // 201
response.assertNoContent(); // 204 by default
response.assertStatus(418);

response.assertUnauthorized(); // 401
response.assertForbidden(); // 403
response.assertNotFound(); // 404
response.assertUnprocessable(); // 422
response.assertTooManyRequests(); // 429
response.assertServerError(); // 5xx
response.assertClientError(); // 4xx

Redirect helpers:

response.assertRedirect("/dashboard");
response.assertRedirectContains("dashboard");
response.assertLocation("/dashboard");
response.assertFound(); // 302

Headers and content

response.assertHeader("Content-Type", "application/json");
response.assertHeaderContains("Content-Type", "json");
response.assertHeaderMissing("X-Debug");

await response.assertSee("Welcome");
await response.assertDontSee("stack trace");
await response.assertContent("exact body string");

JSON asserts

await response.assertJson({ message: "Hello" });
await response.assertExactJson({ id: 1, name: "Ada" });
await response.assertJsonFragment({ email: "ada@example.com" });
await response.assertJsonMissing({ role: "admin" });
await response.assertJsonPath("user.email", "ada@example.com");
await response.assertJsonCount("data", 3);
await response.assertJsonStructure({
  data: [{ id: undefined, name: undefined }],
});
await response.assertJsonIsArray();
await response.assertJsonIsObject();

assertJson checks a subset. assertExactJson requires the whole body. assertSimilarJson compares after sorting object keys.

Conditional chaining

await response
  .assertOk()
  .tap(async (res) => {
    // inspect res
  });

await response.when(debug, async (res) => {
  await res.assertHeader("X-Debug");
});

Full example

tests/Feature/DashboardTest.ts
import { Hash } from "@bunyad/auth";
import { TestCase, test, testCase } from "@bunyad/testing";
import { createApplication } from "../../bootstrap/app.ts";
import User from "@/Models/User.ts";

@testCase({
  createApplication,
  migrationsPath: "./database/migrations",
  beforeBoot() {
    process.env.SESSION_DRIVER = "memory";
    process.env.DATABASE_PATH = "./storage/testing.sqlite";
  },
})
class DashboardTest extends TestCase {
  @test()
  async guests_are_redirected(): Promise<void> {
    const response = await this.get("/dashboard");
    response.assertRedirect("/login");
  }

  @test()
  async members_see_their_name(): Promise<void> {
    const user = await User.create({
      name: "Ada",
      email: "ada@example.com",
      password: await Hash.make("secret"),
    });

    await this.actingAs(user);
    const response = await this.get("/dashboard");
    response.assertOk();
    await response.assertSee("Ada");
  }
}