Skip to content

Creating your first tests

rocambille edited this page May 4, 2026 · 4 revisions

Summary: Learn how to test your application, starting from the simplest requests to dealing with security tokens and understanding why StartER uses an advanced "contract" system.

The testing continuum

StartER uses an advanced Contract-driven testing architecture (located in tests/contracts.ts) to automatically verify the core framework's health. While this is powerful, it is abstract and not recommended for beginners.

When building your own features, the best way to learn is by writing standard, "linear" tests using vitest, supertest, and testing-library. Let's explore how tests evolve from simple to complex.

Level 1: the simplest test (GET)

The easiest way to test your API is with a GET request, as it doesn't modify data and doesn't require security tokens.

Create a file right next to your repository, for example src/express/modules/group/group.test.ts:

import { describe, it, expect } from "vitest";
import request from "supertest";

// Import the main Express application
import app from "../../../../app"; 

describe("Group API", () => {
  it("should fetch all groups successfully", async () => {
    // 1. Arrange & Act: Send a GET request
    const response = await request(app).get("/api/groups");

    // 2. Assert: Verify the status code and body
    expect(response.status).toBe(200);
    expect(Array.isArray(response.body)).toBe(true);
  });
});

Tip

Where to put your tests?

  • Colocation: keep group.test.ts next to groupActions.ts for perfect context.
  • Dedicated folder: move all tests to a central tests/ directory to keep your source folders clean. Both are valid industry practices! StartER lets you choose what fits your workflow best.

Level 2: mutation and CSRF (POST)

When you try to test a POST, PUT, or DELETE request, you will likely encounter a 403 Forbidden error. Why?

StartER enforces double-submit cookie CSRF protection on all mutative routes to prevent malicious attacks. To test a POST request, you must manually simulate this protection by sending a matching cookie and header.

  it("should create a group successfully", async () => {
    // 1. Arrange: Create a fake CSRF token
    const fakeCsrfToken = "test-csrf-token";

    // 2. Act: Send the POST request with the token in both the cookie and the header
    const response = await request(app)
      .post("/api/groups")
      .set("Cookie", [`__Host-x-csrf-token=${fakeCsrfToken}`])
      .set("X-CSRF-Token", fakeCsrfToken)
      .send({ name: "My Test Group" });

    // 3. Assert
    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty("insertId");
  });

Level 2.5: testing the interface (React)

Testing React components involves rendering the component and simulating user interactions. For components that depend on routing (like links or navigation), you should use a Route Stub.

Create your test file next to your component, for example src/react/components/group/GroupCreate.test.tsx:

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router"; 
import { vi } from "vitest";

import GroupCreate from "./GroupCreate";

describe("GroupCreate Component", () => {
  it("should submit the form correctly", async () => {
    // 1. Arrange: Manually mock the browser's fetch API
    const fetchMock = vi.spyOn(global, "fetch").mockResolvedValue({
      status: 201,
      json: async () => ({ insertId: 1 }),
    } as Response);

    const user = userEvent.setup();

    // 2. Act: Create a route stub for the component
    const Stub = createRoutesStub([
      { path: "/groups/new", Component: GroupCreate }
    ]);

    render(<Stub initialEntries={["/groups/new"]} />);

    // 3. Act: Simulate user interaction
    await user.type(screen.getByLabelText(/name/i), "My Test Group");
    await user.click(screen.getByRole("button", { name: /submit/i }));

    // 4. Assert: Verify the fetch call
    expect(fetchMock).toHaveBeenCalledWith(
      expect.stringContaining("/api/groups"),
      expect.objectContaining({ method: "POST" })
    );
  });
});

Level 3: the tipping point (why contracts exist)

Now, imagine your POST /api/groups route also requires the user to be authenticated.

To test this manually, you would need to:

  1. Generate a mock JWT token using jsonwebtoken.
  2. Mock the database to ensure the user exists.
  3. Pass the __Host-auth=jwt cookie.
  4. Pass the __Host-x-csrf-token=abcd cookie.
  5. Pass the X-CSRF-Token: abcd header.

The same applies to React: as your components make more API calls, mocking fetch for every single test becomes a massive burden.

This is exactly why the contract system exists.

The contract system abstracts away the repetitive boilerplate of setting up auth, CSRF, and database states. It allows you to define the behavior once in a central place and use it to verify both your backend and your frontend automatically.

Running your tests

Vitest is configured to automatically find any file ending in .test.ts or .test.tsx. To execute your tests, run:

npm run test

See also

Clone this wiki locally