JCC Express

Testing

Introduction to Testing

JCC Express MVC includes a full testing layer that mirrors Laravel's approach — you test your app through real HTTP requests, real middleware, and real database queries. There is no HTTP mocking layer; the framework boots a real Express server for each request.


Step 1 — Create your project TestCase

tests/TestCase.ts extends the framework base and tells it how to boot your app:

TypeScript
import { TestCase as BaseTestCase } from "jcc-express-mvc/lib/Testing/TestCase";
import { Application } from "jcc-express-mvc/Core";
import { providers } from "../bootstrap/providers";
import { Kernel } from "../app/Http/kernel";
import { config } from "../app/Config/index";

export class TestCase extends BaseTestCase {
  protected async createApplication(): Promise<Application> {
    const builder = Application.configuration()
      .withRouting([
        { name: "route/api", prefix: "/api" },
        { name: "route/web", prefix: "" },
      ])
      .withConfig(config);

    await builder.withProviders(providers);

    return builder.withKernel(Kernel).withMiddleware().create();
  }
}

createApplication() is abstract — you must implement it. It runs once per setUp() call (i.e. before each test).


Step 2 — Write a test

Feature tests use bun:test and live in tests/Feature/:

TypeScript
import { describe, it, beforeEach, afterEach } from "bun:test";
import { TestCase } from "../TestCase";

describe("Users API", () => {
  const test = new (class extends TestCase {})();

  beforeEach(async () => await test.setUp());
  afterEach(async () => await test.tearDown());

  it("returns 200 on the users index", async () => {
    const response = await test.get("/api/users");
    response.assertOk();
  });
});

The new (class extends TestCase {})() pattern creates a one-off anonymous subclass of your TestCase inline. It lets each describe block own its own instance without needing a separate file-level class.


What setUp and tearDown do

setUp() (call in beforeEach):

  • Calls createApplication() — boots Express with all your routes, middleware, providers
  • Creates an MakesHttpRequests instance wired to that app
  • Creates an InteractsWithDatabase instance
  • Connects to the database

tearDown() (call in afterEach):

  • Disconnects from the database

How HTTP requests work internally

MakesHttpRequests.call() does the following for every get/post/put/patch/delete call:

  1. Creates a real http.createServer(expressApp) instance
  2. Listens on port 0 (OS picks a free port)
  3. Makes a fetch() request to http://localhost:<port><uri>
  4. Closes the server after the response is received
  5. Returns a TestResponse wrapping the status + body

This means your middleware, route guards, validation, and error handling all run exactly as they do in production.


Running tests

Bash
# Unit tests (vitest)
npm run test

# Feature tests (bun)
bun test tests/Feature

# Watch mode (unit tests only)
npm run test:watch