Kylndocs

Testing

Kyln runs tests through Bun's built-in test runner. You write plain bun:test files and run them with one command.

Running tests

kyln test

kyln test runs your suite and exits non-zero if anything fails, so it drops straight into CI or a pre-commit hook. Pass through any of Bun's test flags, such as a filename or --watch:

kyln test --watch
kyln test tests/auth.test.ts

A scaffolded project already wires this up as its test script, so kyln test is the one command to remember.

Writing a test

Tests use Bun's test runner: describe, it, and expect from bun:test. A test file lives anywhere your suite looks (the starter app keeps them in tests/):

import { describe, it, expect } from "bun:test";

describe("math", () => {
  it("adds", () => {
    expect(2 + 2).toBe(4);
  });
});

Testing services

Because services are plain classes resolved through the container, you can test them against real dependencies, with no mocking framework required. Register an instance, resolve what you need, and exercise the real code path:

import { describe, it, expect, afterEach } from "bun:test";
import { getContainer } from "@kyln/core";
import { DatabaseService } from "../src/services/db.service";
import { AuthService } from "../src/services/auth.service";

afterEach(() => {
  getContainer().clear();
});

describe("AuthService", () => {
  it("logs in a registered user", () => {
    // Point the database service at a throwaway DB for the test.
    const db = new DatabaseService(":memory:");
    getContainer().registerInstance(DatabaseService, db);

    const auth = getContainer().resolve(AuthService);
    auth.register("a@b.com", "Ada", "pw");

    const result = auth.login("a@b.com", "pw");
    expect(result).not.toBeNull();
  });
});

Two helpers make this work:

  • getContainer().registerInstance(Class, instance) substitutes a specific instance: here, a DatabaseService pointed at an in-memory database so the test touches no real data.
  • getContainer().resolve(Class) returns a fully wired instance, injecting the registrations you set up.

Clearing the container in afterEach keeps tests isolated from each other.

Testing rendered output

For view logic, Kyln ships helpers that render a component or route to a DOM and hand you query and interaction methods to assert against.

DOM setup

Render helpers need a DOM, which Bun's test runner doesn't provide on its own. Kyln ships a one-line preload. Add happy-dom as a dev dependency and point bunfig.toml at the preload module:

bun add -d happy-dom
# bunfig.toml
[test]
preload = ["@kyln/core/testing/dom"]

No hand-rolled globals are needed. If happy-dom isn't installed, the preload fails with exactly that instruction rather than a cryptic resolution error.

Rendering a component

renderComponent renders a component and returns a RenderResult with query and interaction helpers:

import { describe, it, expect } from "bun:test";
import { signal, html, renderComponent } from "@kyln/core";

describe("counter", () => {
  it("increments on click", () => {
    const count = signal(0);
    const result = renderComponent(() => html`
      <div>
        <span class="count">${() => count()}</span>
        <button @click=${() => count.set(count() + 1)}>+</button>
      </div>
    `);

    expect(result.query(".count")?.textContent).toBe("0");
    result.click("button");
    expect(result.query(".count")?.textContent).toBe("1");

    result.destroy();
  });
});

A RenderResult gives you query(selector), queryAll(selector), text(), click(selector), input(selector, value), and destroy() for teardown.

In tests, write reactive interpolations and handlers in the explicit () => form, like ${() => count()} and @click=${() => count.set(...)} above. The concise auto-tracked form from Templates & reactivity (${count()}) is produced by Kyln's compiler at build time; a plain bun test runs the template without that compile step, so the function form is what stays reactive under test.

Rendering a route

renderRoute(routes, path) matches a path against your route definitions and renders the result, which is useful for exercising a page end to end without a browser:

import { renderRoute, type RouteDefinition } from "@kyln/core";

And createTestContainer() gives you a fresh DI container when a test needs to register services in isolation.

Next

When a failing test tells you something is wrong but not why, Debugging shows where to set breakpoints, in the browser for client routes and in Bun's inspector for server-side code.