Skip to content

Step 00: Basic Test Function

In this step, we implement the most basic test function test() with async/await support.

Goals

  • Register test cases with test(name, fn)
  • Provide it(name, fn) as an alias for test
  • Support both synchronous and asynchronous tests
  • Run tests and display results with runTests()

Implementation

src/index.ts

typescript
export type TestFunction = () => void | Promise<void>;

interface TestCase {
  name: string;
  fn: TestFunction;
}

export interface TestResult {
  name: string;
  status: "passed" | "failed";
  error?: Error;
}

export interface TestSummary {
  total: number;
  passed: number;
  failed: number;
  results: TestResult[];
}

const testCases: TestCase[] = [];

export function test(name: string, fn: TestFunction): void {
  testCases.push({ name, fn });
}

export const it = test;

export async function runTests(): Promise<TestSummary> {
  const results: TestResult[] = [];

  for (const testCase of testCases) {
    try {
      await testCase.fn();
      results.push({ name: testCase.name, status: "passed" });
      console.log(`  ✓ ${testCase.name}`);
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      results.push({ name: testCase.name, status: "failed", error: err });
      console.log(`  ✗ ${testCase.name}`);
      console.log(`    Error: ${err.message}`);
    }
  }

  const passed = results.filter((r) => r.status === "passed").length;
  const failed = results.filter((r) => r.status === "failed").length;

  console.log("");
  console.log(`Tests: ${passed} passed, ${failed} failed, ${results.length} total`);

  return { total: results.length, passed, failed, results };
}

export function clearTests(): void {
  testCases.length = 0;
}

Usage Example

typescript
import { test, it, runTests } from "../src/index.js";

test("should pass a simple test", () => {
  const result = 1 + 1;
  if (result !== 2) {
    throw new Error(`Expected 2 but got ${result}`);
  }
});

it("should work with it alias", () => {
  const arr = [1, 2, 3];
  if (arr.length !== 3) {
    throw new Error("Array length should be 3");
  }
});

test("should handle async tests", async () => {
  const result = await Promise.resolve(42);
  if (result !== 42) {
    throw new Error(`Expected 42 but got ${result}`);
  }
});

test("should fail intentionally", () => {
  throw new Error("This test is expected to fail");
});

runTests().then((summary) => {
  if (summary.failed > 0) {
    process.exit(1);
  }
});

Output Example

  ✓ should pass a simple test
  ✓ should work with it alias
  ✓ should handle async tests
  ✗ should fail intentionally
    Error: This test is expected to fail

Tests: 3 passed, 1 failed, 4 total

Exported API

APIDescription
test(name, fn)Register a test case
it(name, fn)Alias for test
runTests()Run tests and return results
clearTests()Clear all registered test cases

How to Run

bash
cd impls/00-basic-test-function
bun run example

Or:

bash
pnpm example:00

Key Concepts

Test Case Collection

The test() function simply registers test cases into a global array. This allows tests to be automatically collected when the file is loaded.

Async/Await Support

By using await testCase.fn() inside runTests(), both synchronous and asynchronous test functions are handled transparently.

Error Handling

When an exception is thrown inside a test, that test is marked as failed. At this stage, we use simple throw new Error() for assertions, but we'll introduce expect() in the next step.