alepha@docs:~/docs/guides/testing$
cat 1-setup.md
3 min read
Last commit:

#Testing Setup

You know that feeling when you write tests for an Express app? Mocking req, res, setting up supertest, hoping your middleware chain doesn't break...

Alepha makes testing almost enjoyable. Almost.

#Vitest Configuration

Alepha integrates with Vitest. For automatic lifecycle management, you need globals: true in your vitest config:

typescript
1// vitest.config.ts2import { defineConfig } from "vitest/config";3 4export default defineConfig({5  test: {6    globals: true, // required for automatic start/stop7  },8});

With globals: true, Alepha hooks into describe and it to manage the app lifecycle automatically.

#Lifecycle Behavior

Where you create your Alepha instance matters:

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha } from "alepha"; 3  4describe("MyService", () => { 5  // alepha created at describe level = auto started before tests 6  const alepha = Alepha.create().with(MyService); 7  const service = alepha.inject(MyService); 8  9  it("should do something", async () => {10    // alepha is already started here, no need to call start()11    const result = await service.doSomething();12    expect(result).toBe("done");13  });14 15  it("should do something else", async () => {16    // still started, same instance for all tests in this describe17    const result = await service.doSomethingElse();18    expect(result).toBe("done");19  });20});21// alepha.stop() called automatically after all tests

If you create Alepha inside an it block, you need to call start() manually:

typescript
 1describe("MyService", () => { 2  it("should work with manual start", async () => { 3    // alepha created inside it = manual start required 4    const alepha = Alepha.create().with(MyService); 5    const service = alepha.inject(MyService); 6  7    await alepha.start(); // required! 8  9    const result = await service.doSomething();10    expect(result).toBe("done");11  });12  // alepha.stop() still called automatically13});

#Testing Services

Services are just classes. Test them like classes.

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha } from "alepha"; 3import { UserService } from "./UserService"; 4  5describe("UserService", () => { 6  const alepha = Alepha.create().with(UserService); 7  const service = alepha.inject(UserService); 8  9  it("should create a user", async () => {10    const user = await service.create({11      email: "test@example.com",12      name: "Test User",13    });14 15    expect(user.id).toBeDefined();16    expect(user.email).toBe("test@example.com");17  });18});

#Testing Actions

You can test actions in two ways: local (direct call) or HTTP (full request cycle).

#Local Testing with run()

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha, t } from "alepha"; 3import { $action } from "alepha/server"; 4  5class UserApi { 6  getUser = $action({ 7    path: "/users/:id", 8    schema: { 9      params: t.object({ id: t.text() }),10      response: t.object({ id: t.text(), name: t.text() }),11    },12    handler: async ({ params }) => {13      return { id: params.id, name: "John" };14    },15  });16}17 18describe("UserApi", () => {19  const alepha = Alepha.create().with(UserApi);20  const api = alepha.inject(UserApi);21 22  it("should return user by id", async () => {23    // local call, skips HTTP layer entirely24    const user = await api.getUser.run({ params: { id: "123" } });25    expect(user.name).toBe("John");26  });27});

#HTTP Testing with fetch()

Sometimes you need to test the full HTTP cycle. Maybe you have security hooks, rate limiting, or middleware that only runs on real HTTP requests:

typescript
 1describe("UserApi", () => { 2  const alepha = Alepha.create().with(UserApi); 3  const api = alepha.inject(UserApi); 4  5  it("should enforce auth via HTTP", async () => { 6    // forces actual HTTP request through the full middleware chain 7    const response = await api.getUser.fetch({ params: { id: "123" } }); 8  9    // now you can test HTTP-specific behavior10    expect(response.status).toBe(401); // auth required11  });12 13  it("should work with valid token", async () => {14    const response = await api.getUser.fetch({15      params: { id: "123" },16      headers: { Authorization: "Bearer valid-token" },17    });18 19    expect(response.status).toBe(200);20  });21});

Use run() for most tests - it's faster and gives clearer errors. Use fetch() when you specifically need to test HTTP-level behavior like authentication hooks, CORS, rate limiting, or response headers.

#Testing with Databases

When testing with PostgreSQL, Alepha creates an isolated schema for each test file. The schema name is derived from PG_TEST_SCHEMA or generated automatically. After tests complete, the schema is destroyed.

This means:

  • Tests in different files don't interfere with each other
  • No manual cleanup needed
  • Tables are created automatically from your entities
typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha, t } from "alepha"; 3import { $entity, $repository, db } from "alepha/orm"; 4  5const userEntity = $entity({ 6  name: "users", 7  schema: t.object({ 8    id: db.primaryKey(), 9    email: t.email(),10  }),11});12 13class Db {14  users = $repository(userEntity);15}16 17describe("UserRepository", () => {18  const alepha = Alepha.create().with(Db);19  const db = alepha.inject(Db);20 21  it("should create and find user", async () => {22    const user = await db.users.create({ email: "test@test.com" });23    expect(user.id).toBeDefined();24 25    const found = await db.users.findById(user.id);26    expect(found?.email).toBe("test@test.com");27  });28 29  it("should list users", async () => {30    // this runs in the same schema, sees data from previous test31    const users = await db.users.findMany();32    expect(users.length).toBeGreaterThan(0);33  });34});35// schema destroyed after all tests in this file

#In-Memory Providers

Here's something important: all external providers use in-memory implementations during tests.

  • $queueMemoryQueueProvider
  • $topicMemoryTopicProvider
  • $emailMemoryEmailProvider
  • $smsMemorySmsProvider
  • $cacheMemoryCacheProvider
  • $lockMemoryLockProvider

No fear of accidentally sending real emails or SMS during tests. Everything stays in memory.

#Verifying Emails Were Sent

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha } from "alepha"; 3import { MemoryEmailProvider } from "alepha/email"; 4  5describe("SignupService", () => { 6  const alepha = Alepha.create().with(SignupService); 7  const service = alepha.inject(SignupService); 8  const emailProvider = alepha.inject(MemoryEmailProvider); 9 10  it("should send welcome email", async () => {11    await service.signup({ email: "new@user.com" });12 13    // check in-memory store14    const sent = emailProvider.sent;15    expect(sent).toHaveLength(1);16    expect(sent[0].to).toBe("new@user.com");17    expect(sent[0].subject).toContain("Welcome");18  });19});

Same pattern works for SMS:

typescript
1import { MemorySmsProvider } from "alepha/sms";2 3const smsProvider = alepha.inject(MemorySmsProvider);4expect(smsProvider.sent[0].to).toBe("+1234567890");

#Service Substitution (Recommended)

Alepha's .with({ provide, use }) is the recommended way to mock dependencies. It's explicit, type-safe, and doesn't rely on module magic.

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha } from "alepha"; 3  4class PaymentGateway { 5  async charge(amount: number): Promise<string> { 6    // real Stripe call 7    return "ch_real_charge_id"; 8  } 9}10 11class MockPaymentGateway extends PaymentGateway {12  charges: number[] = [];13 14  async charge(amount: number): Promise<string> {15    this.charges.push(amount);16    return "ch_mock_charge_id";17  }18}19 20describe("OrderService", () => {21  const alepha = Alepha.create()22    .with(OrderService)23    .with({ provide: PaymentGateway, use: MockPaymentGateway });24 25  const orderService = alepha.inject(OrderService);26  const mockPayment = alepha.inject(MockPaymentGateway);27 28  it("should charge the correct amount", async () => {29    await orderService.checkout({ total: 99.99 });30 31    expect(mockPayment.charges).toContain(99.99);32  });33});

#What About Vitest Mocks?

Module mocking (vi.mock()) is discouraged. It's brittle, hard to type, and fights against Alepha's dependency injection.

Spies (vi.spyOn()) are fine when you need to verify a method was called without replacing the implementation:

typescript
 1import { vi, describe, it, expect } from "vitest"; 2  3describe("AuditService", () => { 4  const alepha = Alepha.create().with(AuditService); 5  const service = alepha.inject(AuditService); 6  7  it("should log audit events", async () => { 8    const spy = vi.spyOn(service, "log"); 9 10    await service.recordAction("user_login", { userId: "123" });11 12    expect(spy).toHaveBeenCalledWith("user_login", { userId: "123" });13  });14});

But prefer service substitution when possible. It's more explicit about what you're testing.

#Generating Fake Data

Use FakeProvider to generate realistic test data. Always inject it, never use new:

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha, t } from "alepha"; 3import { FakeProvider } from "alepha/fake"; 4  5describe("FakeProvider", () => { 6  const alepha = Alepha.create(); 7  const fake = alepha.inject(FakeProvider); // inject, don't use new! 8  9  it("should generate valid user data", () => {10    const userSchema = t.object({11      id: t.uuid(),12      email: t.email(),13      firstName: t.text(),14      age: t.integer({ minimum: 18, maximum: 99 }),15    });16 17    const user = fake.generate(userSchema);18 19    // uses property names as hints20    // email → generates email21    // firstName → generates first name22    expect(user.email).toContain("@");23    expect(user.age).toBeGreaterThanOrEqual(18);24  });25});

#Test Organization

We recommend this structure:

bash
src/
  users/
    UserService.ts
    UserApi.ts
test/
  users/
    UserService.spec.ts    # unit tests
    UserApi.spec.ts        # action tests
  integration/
    signup-flow.spec.ts    # full flow tests

Name your test files .spec.ts. For browser tests (jsdom), use .browser.spec.ts - see Vitest documentation for configuration details.

#Common Patterns

#Arrange-Act-Assert

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha } from "alepha"; 3  4describe("UserService", () => { 5  const alepha = Alepha.create().with(UserService); 6  const service = alepha.inject(UserService); 7  8  it("should deactivate user", async () => { 9    // arrange10    const user = await service.create({ email: "test@test.com" });11 12    // act13    await service.deactivate(user.id);14 15    // assert16    const updated = await service.findById(user.id);17    expect(updated?.active).toBe(false);18  });19});

#Testing Errors

typescript
 1import { describe, it, expect } from "vitest"; 2import { Alepha } from "alepha"; 3  4describe("UserService", () => { 5  const alepha = Alepha.create().with(UserService); 6  const service = alepha.inject(UserService); 7  8  it("should throw on invalid email", async () => { 9    await expect(10      service.create({ email: "not-an-email" })11    ).rejects.toThrow("Invalid email");12  });13});

#Tips

  1. Use globals: true - enables automatic lifecycle management
  2. Create Alepha at describe level - avoids manual start() calls
  3. Use .with() for mocking - explicit and type-safe
  4. Use run() for most tests - faster than HTTP
  5. Use fetch() for HTTP-specific tests - security hooks, headers, etc.
  6. Inject FakeProvider - never use new FakeProvider()
  7. Trust in-memory providers - no accidental emails or SMS
  8. Avoid vi.mock() - use service substitution instead

Testing with Alepha isn't painful. It's just... testing. The way it should be.

On This Page
No headings found...
ready
mainTypeScript
UTF-8guides_testing_setup.md