# Unit Tests

Alepha uses Vitest as the test runner. Every `alepha init` scaffolds a Vitest config and a sample test file - Vitest ships embedded in `alepha`, so there is nothing to install.

The scaffolded config turns on Vitest's `globals: true`, and Alepha relies on it at runtime: that is how `Alepha.create()` finds `beforeAll` and `afterAll` (see [Lifecycle Management](#lifecycle-management)). The scaffolded `tsconfig.json` does not declare the global types, though, so specs import what they use, as the starter spec does:

```typescript
import { describe, expect, it } from "vitest";
```

To write specs without that line, add `"types": ["vitest/globals"]` to `compilerOptions` in your `tsconfig.json`. That entry resolves from the hoisted `node_modules` the scaffold sets up: under a strict layout, such as pnpm's isolated linker, `vitest` is not visible from your project, so it fails like the import above. The examples below leave the import out for brevity.

## Setup

Every Alepha project is scaffolded with test support - no flag needed:

```bash
alepha init my-app
```

`init` writes the Vitest `test` block into `vite.config.ts` (it pins `test.root` to the project directory) and a starter `test/dummy.spec.ts` - there is no separate Vitest config file. Specs live in `test/`, named `*.spec.ts`.

Run tests with `alepha test`. An optional positional argument is forwarded to Vitest as a filename filter:

```bash
alepha test                    # All tests
alepha test auth               # Only specs whose path matches "auth"
alepha test test/user.spec.ts  # A single file
```

| Flag              | Effect                                                                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--config`, `-c`  | Use another Vitest config file than `vite.config.ts`.                                                                                                   |
| `--project`, `-p` | Run only these Vitest projects, comma-separated. Globs work: `-p web*` also picks up `web:jsdom` (see [React Tests](https://latest.alepha.dev/docs/guides-testing-react-tests.md)). |
| `--shard <i>/<n>` | Run one slice of the spec files, e.g. `--shard 2/6` as one leg of a CI matrix. Files are split by count, not by duration.                               |
| `--coverage`      | Measure coverage and write the reports below.                                                                                                           |

See [`alepha test`](https://latest.alepha.dev/docs/cli-commands-toolchain.md#test) in the CLI reference.

### Coverage

`--coverage` measures the run and leaves two machine-readable reports in
`coverage/`, which is already gitignored:

```bash
alepha test --coverage
```

|                                  |                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| `coverage/coverage-summary.json` | totals plus one entry per file, each with `lines`, `statements`, `functions` and `branches`. |
| `coverage/test-results.json`     | the run itself: how many tests passed, failed, were skipped, and per-file timings.           |

The browsable HTML report is still written beside them, and the normal test
output still prints. Both are easy to lose by hand: asking Vitest for the JSON
coverage reporter replaces the HTML one rather than adding to it, and asking for
the JSON test reporter replaces the default one, so a CI job that spelled this
itself would trade every readable failure for a file nobody reads on a green run.

The paths are fixed rather than configurable, because they are a convention that
other tools read. `lore quality push` finds both files without being told
where they are.

`VITEST_ARGS` is appended after everything above, so an explicit caller flag
still lands last on the command line.

## Lifecycle Management

`Alepha.create()` wires itself to Vitest's hooks in test environments (this needs `globals: true`). You do not need `beforeAll`/`afterAll` for lifecycle:

- Created at module or `describe` scope, the container is started in `beforeAll` and stopped in `afterAll`.
- Created inside an `it`, it is stopped when that test finishes, but `beforeAll` has already run: call `await alepha.start()` yourself, as below.

```typescript
describe("Lifecycle", () => {
  it("should inject a service", async () => {
    const alepha = Alepha.create().with(MyService);
    const svc = alepha.inject(MyService);
    await alepha.start();

    const result = await svc.doSomething();
    expect(result).toBe("done");
  });
});
```

## Testing Actions

Actions expose two call modes: `.run()` for in-process invocation and `.fetch()` for HTTP simulation.

```typescript
import { Alepha, z } from "alepha";
import { $action } from "alepha/server";

class UserController {
  getUser = $action({
    method: "GET",
    path: "/users/:id",
    schema: { params: z.object({ id: z.uuid() }) },
    handler: async ({ params }) => ({ id: params.id, name: "Alice" }),
  });
}

describe("UserController", () => {
  it("should return user by id", async () => {
    const alepha = Alepha.create().with(UserController);
    const ctrl = alepha.inject(UserController);
    await alepha.start();

    const id = "0b3c3d6e-6f1a-4c3e-9d2b-8a7f5e4d3c2b";

    // Local call - no HTTP overhead
    const result = await ctrl.getUser.run({ params: { id } });
    expect(result.name).toBe("Alice");

    // HTTP simulation - goes through the full request pipeline
    const response = await ctrl.getUser.fetch({ params: { id } });
    expect(response.status).toBe(200);
    expect(response.data.name).toBe("Alice");
  });
});
```

## DI Substitution (No vi.mock)

Never use `vi.mock()` or `vi.spyOn()`. Alepha's dependency injection system replaces the need for traditional mocking. Use `.with({ provide, use })` to swap implementations.

```typescript
const alepha = Alepha.create().with({
  provide: PaymentService,
  use: FakePaymentService,
});

const svc = alepha.inject(PaymentService);
// svc is now an instance of FakePaymentService
```

## Memory Providers

Alepha ships memory implementations for all I/O-bound services. These run in-process with no external dependencies and include test assertion helpers.

| Provider                    | Import                | Replaces                                                                                    |
| --------------------------- | --------------------- | ------------------------------------------------------------------------------------------- |
| `MemoryFileSystemProvider`  | `alepha/system`       | File system                                                                                 |
| `MemoryShellProvider`       | `alepha/system`       | Shell commands                                                                              |
| `MemoryQueueProvider`       | `alepha/queue`        | Job queues                                                                                  |
| `MemoryTopicProvider`       | `alepha/topic`        | Pub/sub topics                                                                              |
| `MemoryLockProvider`        | `alepha/lock`         | Distributed locks                                                                           |
| `MemoryCacheProvider`       | `alepha/cache`        | Caching layer                                                                               |
| `MemoryFileStorageProvider` | `alepha/bucket`       | File storage (S3, R2, etc.)                                                                 |
| `MemoryEmailProvider`       | `alepha/email`        | Email sending                                                                               |
| `MemorySmsProvider`         | `alepha/sms`          | SMS sending                                                                                 |
| `MemoryCaptchaProvider`     | `alepha/captcha`      | Captcha verification                                                                        |
| `MemoryPaymentProvider`     | `alepha/api/payments` | Payment service provider (see [Payments](https://latest.alepha.dev/docs/guides-features-payments.md#local-development)) |

In test environments, `FileSystemProvider` and `ShellProvider` both default to
their memory implementations automatically (so do email, SMS and captcha) - file
writes and shell commands stay inside the container unless a test opts back into
the real thing:

```typescript
import { FileSystemProvider, NodeFileSystemProvider } from "alepha/system";

// Only when a test really needs the disk (e.g. real fixture files):
const alepha = Alepha.create().with({
  provide: FileSystemProvider,
  use: NodeFileSystemProvider,
});
```

### Example: File System

```typescript
import { MemoryFileSystemProvider } from "alepha/system";

const alepha = Alepha.create(); // memory file system is the test default

const fs = alepha.inject(MemoryFileSystemProvider);

// Run code that writes files...
await myService.generateReport();

// Assert with built-in helpers
expect(fs.wasWritten("/path/report.txt")).toBe(true);
expect(fs.wasWrittenMatching("/path/report.txt", /summary/)).toBe(true);
expect(fs.wasDeleted("/path/temp.txt")).toBe(true);
```

A call is recorded only once it succeeded, so a write that threw is not
reported as written. `wasWrittenMatching` reads the file's **final**
content: a path written twice matches on the second write, which is what
you want when a generator emits a placeholder before the real thing. Use
`wasEverWrittenMatching` for the rarer case where an intermediate write is
the subject.

Other helpers: `wasAppended(path)`, `wasWrittenWithMode(path, mode)` and
`getFileContent(path)`. To exercise an error path, set `writeFileError`,
`readFileError` or `mkdirError` to an `Error` and every matching call throws it
until `reset()`, which clears files, recorded calls and injected errors.

### Example: Shell Commands

```typescript
import { MemoryShellProvider } from "alepha/system";

const alepha = Alepha.create(); // memory shell is the test default

const shell = alepha.inject(MemoryShellProvider);

// Run code that executes shell commands...
await myService.installDependencies();

expect(shell.wasCalled("yarn install")).toBe(true);

// Structured results work too: a configured error becomes exitCode 1,
// mirroring ShellProvider.capture() on the real runtimes.
shell.errors.set("git diff --quiet", "dirty");
const result = await shell.capture("git diff --quiet");
expect(result.exitCode).toBe(1);
```

Canned output works the same way: `shell.outputs.set(command, output)`, or
`shell.configure({ outputs, errors, installedCommands })` to set several at
once. Every call lands in `shell.calls`; `wasCalledMatching(regex)` and
`getCallsMatching(regex)` match them by pattern.

## Test Data with Faker

`alepha/testing/faker` generates data from your Zod schemas, so fixtures follow the schema when it changes. It is built on Faker, which you install yourself:

```bash
npm install -D @faker-js/faker
```

Register `AlephaFake` and inject `FakeProvider`:

```typescript
import { Alepha, z } from "alepha";
import { AlephaFake, FakeProvider } from "alepha/testing/faker";

const userSchema = z.object({
  id: z.uuid(),
  firstName: z.text(),
  email: z.email(),
  age: z.integer().optional(),
});

const alepha = Alepha.create().with(AlephaFake);
const fake = alepha.inject(FakeProvider);

const user = fake.generate(userSchema);
const users = fake.generateMany(userSchema, 10);
```

Formats and constraints in the schema are respected, and property names refine plain strings: `firstName`, `email` or `city` get a matching value, not random text. Output is deterministic: each provider owns its own seeded Faker instance (seed `12345`), so a spec gets the same data on every run, and two containers never reset each other's sequence.

`configure()` tunes the generation and returns the provider:

| Option                 | Default              | Effect                                                     |
| ---------------------- | -------------------- | ---------------------------------------------------------- |
| `seed`                 | `12345`              | Reseeds the provider.                                      |
| `locale`               | `"en"`               | Faker locale.                                              |
| `optionalProbability`  | `0.3`                | Chance an optional field is left `undefined`.              |
| `nullableProbability`  | `0.2`                | Chance a nullable field is `null`.                         |
| `defaultArrayLength`   | `5`                  | Longest array when the schema sets no bound.               |
| `maxArrayLength`       | `20`                 | Cap on array length; `Infinity` respects `maxItems` alone. |
| `defaultRecordEntries` | `{ min: 2, max: 5 }` | Number of entries in a record.                             |

For one-off values, the module also exports Faker itself as `fake` (`fake.person.fullName()`). See [`alepha/testing/faker`](https://latest.alepha.dev/docs/packages-alepha-testing-faker.md).

## Time Travel

Code that reads time through the injected `DateTimeProvider` (never `Date.now()`) is testable by moving the clock:

```typescript
const dateTime = alepha.inject(DateTimeProvider);
dateTime.pause(); // freeze the clock
await dateTime.travel([2, "hours"]); // jump forward
```

`travel()` also resolves `CronProvider` waits - **every `$job` cron in the container fires**, not just the one you are testing. Assert end state, not call counts, or a second cron firing will fail an otherwise-correct test.

## Database Testing

Without a `DATABASE_URL`, a test container uses an in-memory SQLite database, fresh for each container.

Alepha can also run tests against real Postgres, with the `alepha/orm/postgres` module (`AlephaOrmPostgres`) registered. Under test, each container creates its own throwaway schema (`test_alepha_<epoch>_<random>`), runs the migrations into it, and drops it on stop; schemas older than an hour left by a crashed run are swept on the next start.

Point `DATABASE_URL` at your test database via the `env` block of `vite.config.ts`'s `test` section (the scaffolded config doesn't set one):

```typescript
env: {
  DATABASE_URL: "postgres://postgres:postgres@127.0.0.1:5432/postgres",
}
```

### Without transactions, as on D1

Cloudflare D1 has no transactions: `$transactional()` runs its body in place there, and a failure halfway keeps every write made before it. The SQLite drivers used by specs do roll back, and they run transactions one at a time, so a spec on them cannot see a race or a partial write that D1 would produce.

`DATABASE_TRANSACTIONS: false` switches transactions off for one container. `$transactional()` then runs its body in place, `afterCommit` callbacks run at once and `Repository.transaction()` throws, exactly as on D1. The switch is read only when `NODE_ENV` is `test`, so it can never reach production.

To reproduce a failure halfway, substitute the collaborator that fails rather than mocking it:

```typescript
import { Alepha, AlephaError } from "alepha";
import { AuditService } from "alepha/api/audits";

class ThrowingAuditService extends AuditService {
  public override async create(): Promise<never> {
    throw new AlephaError("audit insert failed");
  }
}

const alepha = Alepha.create({
  env: { DATABASE_URL: "sqlite://:memory:", DATABASE_TRANSACTIONS: false },
}).with({ provide: AuditService, use: ThrowingAuditService });

// The action's own write is kept, whatever the audit did.
```

## TestProvider Pattern

To unit test protected methods on a class, create a test subclass that exposes them:

```typescript
class TestCliProvider extends CliProvider {
  public testParseFlags = this.parseFlags.bind(this);
  public testResolveCommand = this.resolveCommand.bind(this);
}

const alepha = Alepha.create();
const cli = alepha.inject(TestCliProvider);
const result = cli.testParseFlags(["--verbose"], flagDefs);
```
