Unit Test Mocking
Call.run() directly on tasks with mock dependencies. This bypasses the full runtime and middleware:
import { r } from "@bluelibs/runner";
// Define the real resource
const emailService = r.resource("app.emailService")
.init(async () => ({
send: async (to: string, subject: string) => {
// Real email sending logic
await sendGridClient.send({ to, subject });
}
}))
.build();
// Define task that depends on it
const notifyUser = r.task("app.notifyUser")
.dependencies({ emailService })
.run(async (email: string, { emailService }) => {
await emailService.send(email, "Welcome!");
return { notified: true };
})
.build();
// Unit test - provide mock directly
it("sends notification email", async () => {
const mockEmailService = {
send: jest.fn().mockResolvedValue(undefined)
};
const result = await notifyUser.run("[email protected]", {
emailService: mockEmailService
});
expect(result.notified).toBe(true);
expect(mockEmailService.send).toHaveBeenCalledWith(
"[email protected]",
"Welcome!"
);
});
- No runtime initialization needed
- Bypasses middleware and validation
- Fastest possible test execution
- Perfect for testing business logic in isolation
Integration Test Overrides
Useoverride() to replace resource implementations for integration tests:
import { r, run, override } from "@bluelibs/runner";
// Production resources
const database = r.resource("app.db")
.init(async () => {
const client = new PostgresClient();
await client.connect();
return client;
})
.dispose(async (client) => client.disconnect())
.build();
const emailService = r.resource("app.emailService")
.init(async () => ({
send: async (to: string, subject: string) => {
await sendGridClient.send({ to, subject });
}
}))
.build();
// Production app
const app = r.resource("app")
.register([database, emailService, /* tasks */])
.build();
// Test overrides
const testDb = override(database, {
init: async () => new InMemoryDatabase(),
dispose: async (db) => db.clear(),
});
const mockEmailService = override(emailService, {
init: async () => ({
send: jest.fn().mockResolvedValue(undefined)
})
});
// Create test app with overrides
const testApp = r.resource("test.app")
.register([database, emailService, /* tasks */])
.overrides([testDb, mockEmailService])
.build();
const { runTask, getResourceValue, dispose } = await run(testApp);
try {
// Full pipeline runs with test doubles
await runTask(notifyUser, "[email protected]");
const mailer = await getResourceValue(emailService);
expect(mailer.send).toHaveBeenCalled();
} finally {
await dispose();
}
Override Patterns
Pattern: Spy on Real Implementation
Wrap real logic with spies for observation:const database = r.resource("app.db")
.init(async () => ({
query: async (sql: string) => {
// Real query logic
return await executeQuery(sql);
}
}))
.build();
const spyDb = override(database, {
init: async () => {
const querySpy = jest.fn();
return {
query: async (sql: string) => {
querySpy(sql);
return await executeQuery(sql); // Still executes real logic
},
querySpy, // Expose spy for assertions
};
}
});
Pattern: Conditional Behavior
Create mocks that behave differently based on input:const mockEmailService = override(emailService, {
init: async () => ({
send: jest.fn().mockImplementation(async (to: string) => {
if (to.includes("@blocked.com")) {
throw new Error("Domain blocked");
}
return { messageId: "mock-123" };
})
})
});
// Test error handling
it("handles blocked domains", async () => {
const testApp = r.resource("test.app")
.register([emailService, notifyUser])
.overrides([mockEmailService])
.build();
const { runTask, dispose } = await run(testApp);
try {
await expect(
runTask(notifyUser, "[email protected]")
).rejects.toThrow("Domain blocked");
} finally {
await dispose();
}
});
Pattern: Stateful Mock
Create mocks that maintain state across calls:const mockDatabase = override(database, {
init: async () => {
const users: User[] = [];
return {
insert: async (user: User) => {
users.push(user);
return user;
},
findAll: async () => users,
findById: async (id: string) =>
users.find(u => u.id === id),
clear: () => users.length = 0,
};
}
});
it("persists users in mock db", async () => {
const { runTask, getResourceValue, dispose } = await run(testApp);
try {
await runTask(createUser, { name: "Alice", email: "[email protected]" });
await runTask(createUser, { name: "Bob", email: "[email protected]" });
const db = await getResourceValue(database);
const users = await db.findAll();
expect(users).toHaveLength(2);
} finally {
await dispose();
}
});
Testing Middleware
Middleware doesn’t run when calling.run() directly. Use full runtime to test middleware:
import { r, run, globals } from "@bluelibs/runner";
const expensiveTask = r.task("app.expensiveTask")
.middleware([
globals.middleware.task.cache.with({ ttl: 60000 })
])
.run(async (input: number) => {
// Expensive computation
return input * 2;
})
.build();
// Unit test - NO middleware
it("computes correctly (no cache)", async () => {
const result = await expensiveTask.run(21, {});
expect(result).toBe(42);
// Cache middleware NOT executed
});
// Integration test - WITH middleware
it("caches expensive computation", async () => {
const app = r.resource("app")
.register([expensiveTask])
.build();
const { runTask, dispose } = await run(app);
try {
// First call - cache miss
const result1 = await runTask(expensiveTask, 21);
expect(result1).toBe(42);
// Second call - cache hit (faster)
const result2 = await runTask(expensiveTask, 21);
expect(result2).toBe(42);
} finally {
await dispose();
}
});
Mocking Events
Mock event emitters to verify emissions:const userCreated = r.event("app.events.userCreated")
.payloadSchema(z.object({ userId: z.string() }))
.build();
const createUser = r.task("app.createUser")
.dependencies({ database, userCreated })
.run(async (input: CreateUserInput, { database, userCreated }) => {
const user = await database.insert(input);
await userCreated({ userId: user.id });
return user;
})
.build();
// Unit test - mock event emitter
it("emits userCreated event", async () => {
const mockDatabase = {
insert: jest.fn().mockResolvedValue({ id: "123", name: "Alice" })
};
const mockUserCreated = jest.fn();
await createUser.run(
{ name: "Alice", email: "[email protected]" },
{ database: mockDatabase, userCreated: mockUserCreated }
);
expect(mockUserCreated).toHaveBeenCalledWith({ userId: "123" });
});
Testing Hooks
Test that hooks react to events correctly:const userCreated = r.event("app.events.userCreated")
.payloadSchema(z.object({ userId: z.string() }))
.build();
const sendWelcomeEmail = r.hook("app.hooks.sendWelcomeEmail")
.on(userCreated)
.dependencies({ emailService })
.run(async (event, { emailService }) => {
await emailService.send(
event.data.userId,
"Welcome to our platform!"
);
})
.build();
it("sends welcome email when user created", async () => {
const mockEmailService = override(emailService, {
init: async () => ({
send: jest.fn().mockResolvedValue(undefined)
})
});
const app = r.resource("app")
.register([userCreated, sendWelcomeEmail, emailService])
.overrides([mockEmailService])
.build();
const { emitEvent, getResourceValue, dispose } = await run(app);
try {
await emitEvent(userCreated, { userId: "user-123" });
const mailer = await getResourceValue(emailService);
expect(mailer.send).toHaveBeenCalledWith(
"user-123",
"Welcome to our platform!"
);
} finally {
await dispose();
}
});
Test Doubles Reference
Jest Mocks
const mockService = {
method: jest.fn().mockResolvedValue("result"),
};
// Assertions
expect(mockService.method).toHaveBeenCalled();
expect(mockService.method).toHaveBeenCalledWith("arg");
expect(mockService.method).toHaveBeenCalledTimes(2);
Spies
const realService = {
method: async () => "real result"
};
const spy = jest.spyOn(realService, "method");
// Use real service
await realService.method();
// Verify calls
expect(spy).toHaveBeenCalled();
Stubs
const stubService = {
method: async () => "stubbed result", // Always returns this
};
Fakes
class FakeDatabase {
private data = new Map();
async insert(item: any) {
this.data.set(item.id, item);
return item;
}
async findById(id: string) {
return this.data.get(id);
}
clear() {
this.data.clear();
}
}
const fakeDb = override(database, {
init: async () => new FakeDatabase()
});
Real-World Examples
From the Runner test suite:// From src/__tests__/run/run.overrides.test.ts
it("overrides resource with mock implementation", async () => {
const task = r.task("task")
.run(async () => "Task executed")
.build();
const overrideTask = override(task, {
run: async () => "Task overridden",
});
const app = r.resource("app")
.register([task])
.dependencies({ task })
.overrides([overrideTask])
.init(async (_, deps) => {
return await deps.task();
})
.build();
const { value, dispose } = await run(app);
try {
expect(value).toBe("Task overridden");
} finally {
await dispose();
}
});
// From examples/fastify-mikroorm/src/users/tasks/direct-run.test.ts
it("login sets cookie via context", async () => {
const testDb = override(database, {
init: async () => new InMemorySQLite()
});
const testRunner = await run(
r.resource("test")
.register([httpRoute, db, users])
.overrides([testDb])
.build()
);
try {
const { orm } = testRunner.getResourceValue(db);
await orm.getSchemaGenerator().createSchema();
// Test with async context
await fastifyContext.provide(
{ request: {} as any, reply: mockReply, userId: null },
async () => {
const result = await testRunner.runTask(loginUser, {
email: "[email protected]",
password: "password"
});
expect(result.token).toBeDefined();
}
);
} finally {
await testRunner.dispose();
}
});
Common Mistakes
❌ Registering Both Base and Override
// Wrong - duplicate ID error
const app = r.resource("app")
.register([database, mockDatabase]) // Both registered!
.build();
✅ Use Overrides Correctly
// Correct
const mockDb = override(database, { /* mock impl */ });
const app = r.resource("app")
.register([database]) // Register base
.overrides([mockDb]) // Override it
.build();
❌ Forgetting to Mock All Dependencies
// Wrong - missing logger dependency
await myTask.run(input, {
database: mockDb,
// logger is missing! Will throw
});
✅ Mock All Dependencies
// Correct
await myTask.run(input, {
database: mockDb,
logger: { info: jest.fn(), error: jest.fn() },
});
Next Steps
- Testing Overview - Understand the testing philosophy and strategies
- Test Resources - Learn about test runtime utilities
When to use which mocking strategy:
- Unit tests: Call
.run()with plain mock objects - Integration tests: Use
override()withrun() - E2E tests: Use real resources with test configurations