@nodeboot/jest
v1.4.1
Published
Test framework for Node_Boot projects using Jest
Downloads
93
Readme
NodeBoot Jest Integration
Jest-specific integration for the NodeBoot Test Framework, providing seamless integration with Jest test runner and additional Jest-specific testing utilities.
Overview
The @nodeboot/jest package extends the core NodeBoot Test Framework with Jest-specific features:
- Automatic Jest Lifecycle Integration: Seamless integration with Jest's beforeAll, afterAll, beforeEach, afterEach hooks
- Jest Spies: Built-in Jest spy functionality for service methods
- Timer Control: Mock and control JavaScript timers using Jest's timer mocks
- Enhanced Type Safety: Full TypeScript support with Jest types
Installation
npm install @nodeboot/jest @nodeboot/test
# or
pnpm add @nodeboot/jest @nodeboot/testBasic Usage
Simple Test Setup
import {useNodeBoot} from "@nodeboot/jest";
import {MyApp} from "./MyApp";
describe("My App Tests", () => {
const {useHttp, useService} = useNodeBoot(MyApp);
it("should start the application", async () => {
const {get} = useHttp();
const response = await get("/health");
expect(response.status).toBe(200);
});
});With Configuration
import {useNodeBoot} from "@nodeboot/jest";
import {MyApp, EmailService} from "./MyApp";
describe("My App with Config", () => {
const {useHttp, useService, useSpy} = useNodeBoot(MyApp, ({useConfig, useMock, useEnv}) => {
// Test-specific configuration
useConfig({
app: {port: 3001},
database: {url: "sqlite::memory:"},
});
// Mock external services
useMock(EmailService, {
sendEmail: jest.fn(() => Promise.resolve({messageId: "test-123"})),
});
// Set environment variables
useEnv({NODE_ENV: "test"});
});
it("should send email and track calls", async () => {
const emailSpy = useSpy(EmailService, "sendEmail");
const {post} = useHttp();
await post("/api/send-notification", {
email: "[email protected]",
message: "Hello World",
});
expect(emailSpy).toHaveBeenCalledWith({
to: "[email protected]",
subject: expect.any(String),
body: "Hello World",
});
});
});Jest-Specific Hooks
useSpy(ServiceClass, methodName)
Create Jest spies on service methods to verify calls and behavior.
describe("Service Interaction Tests", () => {
const {useService, useSpy} = useNodeBoot(MyApp);
it("should track service method calls", () => {
const userService = useService(UserService);
const findUserSpy = useSpy(UserService, "findUser");
// Call the service method
const user = userService.findUser("123");
// Verify the spy was called
expect(findUserSpy).toHaveBeenCalledWith("123");
expect(findUserSpy).toHaveBeenCalledTimes(1);
expect(findUserSpy).toReturnWith(user);
});
it("should spy on async methods", async () => {
const userService = useService(UserService);
const createUserSpy = useSpy(UserService, "createUser");
const userData = {name: "John", email: "[email protected]"};
const result = await userService.createUser(userData);
expect(createUserSpy).toHaveBeenCalledWith(userData);
expect(createUserSpy).toHaveResolvedWith(result);
});
it("should spy on mocked services", () => {
// First mock the service
const {useMock} = useNodeBoot(MyApp, ({useMock}) => {
useMock(EmailService, {
sendEmail: jest.fn(() => Promise.resolve({messageId: "mock-123"})),
});
});
// Then spy on the mocked method
const emailSpy = useSpy(EmailService, "sendEmail");
const emailService = useService(EmailService);
emailService.sendEmail({to: "[email protected]"});
expect(emailSpy).toHaveBeenCalled();
});
});useTimer()
Control JavaScript timers in your tests using Jest's timer mocks.
describe("Timer Control Tests", () => {
const {useTimer} = useNodeBoot(MyApp);
it("should advance timers manually", () => {
const {control} = useTimer();
let callbackExecuted = false;
setTimeout(() => {
callbackExecuted = true;
}, 1000);
expect(callbackExecuted).toBe(false);
// Advance time by 1 second
control().advanceTimeBy(1000);
expect(callbackExecuted).toBe(true);
});
it("should fast-forward all timers", () => {
const {control} = useTimer();
const callback1 = jest.fn();
const callback2 = jest.fn();
setTimeout(callback1, 100);
setTimeout(callback2, 500);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
// Run all pending timers
control().runAllTimers();
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should advance timers step by step", () => {
const {control} = useTimer();
const callback = jest.fn();
setInterval(callback, 100);
// Run only the next timer
control().runOnlyPendingTimers();
expect(callback).toHaveBeenCalledTimes(1);
// Advance by specific time
control().advanceTimeBy(250);
expect(callback).toHaveBeenCalledTimes(3); // 1 + 2 more calls
});
it("should work with async operations", async () => {
const {control, useService} = useTimer();
const delayedService = useService(DelayedService);
const promise = delayedService.processWithDelay(1000);
// Advance time to complete the delay
control().advanceTimeBy(1000);
const result = await promise;
expect(result).toBeDefined();
});
});Advanced Jest Integration
Custom Jest Matchers
Use Jest's custom matchers with NodeBoot services:
// Custom matcher for testing service results
expect.extend({
toBeValidUser(received) {
const pass = received && received.id && received.email;
return {
message: () => `expected ${received} to be a valid user`,
pass,
};
},
});
describe("User Service Tests", () => {
const {useService} = useNodeBoot(MyApp);
it("should return valid user", () => {
const userService = useService(UserService);
const user = userService.findUser("123");
expect(user).toBeValidUser();
});
});Parameterized Tests
Use Jest's test.each with NodeBoot:
describe("API Endpoint Tests", () => {
const {useHttp} = useNodeBoot(MyApp);
test.each([
["/api/users", 200],
["/api/products", 200],
["/api/orders", 200],
["/api/nonexistent", 404],
])("GET %s should return status %i", async (endpoint, expectedStatus) => {
const {get} = useHttp();
const response = await get(endpoint);
expect(response.status).toBe(expectedStatus);
});
});Snapshot Testing
Use Jest snapshots with API responses:
describe("API Response Snapshots", () => {
const {useHttp} = useNodeBoot(MyApp, ({useConfig}) => {
useConfig({app: {version: "1.0.0-test"}});
});
it("should match API info snapshot", async () => {
const {get} = useHttp();
const response = await get("/api/info");
expect(response.data).toMatchSnapshot();
});
it("should match user list structure", async () => {
const {get} = useHttp();
const response = await get("/api/users");
expect(response.data).toMatchSnapshot({
users: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
createdAt: expect.any(String),
}),
]),
});
});
});Error Testing
Test error scenarios with Jest:
describe("Error Handling Tests", () => {
const {useService, useHttp} = useNodeBoot(MyApp, ({useMock}) => {
useMock(DatabaseService, {
findUser: jest.fn(() => {
throw new Error("Database connection failed");
}),
});
});
it("should handle service errors", () => {
const userService = useService(UserService);
expect(() => {
userService.getUser("123");
}).toThrow("Database connection failed");
});
it("should return error responses", async () => {
const {get} = useHttp();
const response = await get("/api/users/123");
expect(response.status).toBe(500);
expect(response.data.error).toContain("Database connection failed");
});
});Testing Patterns
Setup and Teardown
describe("User Management", () => {
const {useService, useRepository} = useNodeBoot(MyApp);
let userRepo: UserRepository;
let testUsers: User[];
beforeAll(() => {
userRepo = useRepository(UserRepository);
});
beforeEach(async () => {
// Clear and seed test data
await userRepo.deleteAll();
testUsers = await userRepo.createMany([
{name: "John", email: "[email protected]"},
{name: "Jane", email: "[email protected]"},
]);
});
it("should find existing users", async () => {
const userService = useService(UserService);
const users = await userService.findAll();
expect(users).toHaveLength(2);
expect(users[0].name).toBe("John");
});
afterEach(async () => {
// Optional: additional cleanup
await userRepo.deleteAll();
});
});Mock Strategies
describe("Payment Processing", () => {
describe("with successful payment gateway", () => {
const {useService, useSpy} = useNodeBoot(MyApp, ({useMock}) => {
useMock(PaymentGateway, {
charge: jest.fn(() =>
Promise.resolve({
transactionId: "txn_123",
status: "success",
}),
),
});
});
it("should process payment successfully", async () => {
const paymentService = useService(PaymentService);
const chargeSpy = useSpy(PaymentGateway, "charge");
const result = await paymentService.processPayment({
amount: 100,
currency: "USD",
});
expect(result.success).toBe(true);
expect(chargeSpy).toHaveBeenCalledWith({
amount: 100,
currency: "USD",
});
});
});
describe("with failing payment gateway", () => {
const {useService} = useNodeBoot(MyApp, ({useMock}) => {
useMock(PaymentGateway, {
charge: jest.fn(() => Promise.reject(new Error("Payment failed"))),
});
});
it("should handle payment failures", async () => {
const paymentService = useService(PaymentService);
await expect(paymentService.processPayment({amount: 100, currency: "USD"})).rejects.toThrow(
"Payment failed",
);
});
});
});Integration with Pactum
import {spec} from "pactum";
describe("API Contract Tests", () => {
const {useSpy} = useNodeBoot(MyApp, ({usePactum, useMock}) => {
usePactum();
useMock(EmailService, {
sendEmail: jest.fn(() => Promise.resolve({messageId: "test-123"})),
});
});
it("should create user and send welcome email", async () => {
const emailSpy = useSpy(EmailService, "sendEmail");
const response = await spec()
.post("/api/users")
.withJson({
name: "John Doe",
email: "[email protected]",
})
.expectStatus(201)
.expectJsonLike({
id: /^\w+$/,
name: "John Doe",
email: "[email protected]",
createdAt: /^\d{4}-\d{2}-\d{2}T/,
})
.returns("res.body");
expect(emailSpy).toHaveBeenCalledWith({
to: "[email protected]",
subject: "Welcome!",
template: "welcome",
});
expect(response.id).toBeDefined();
});
});Configuration
Jest Setup File
Create a Jest setup file for global configuration:
// jest.setup.js
import {jest} from "@jest/globals";
// Global test timeout
jest.setTimeout(30000);
// Mock console methods if needed
global.console = {
...console,
// Suppress console.log in tests
log: jest.fn(),
// Keep error and warn for debugging
error: console.error,
warn: console.warn,
};
// Global test utilities
global.testUtils = {
createMockUser: () => ({
id: "test-user-id",
name: "Test User",
email: "[email protected]",
}),
};Jest Configuration
// jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
testMatch: ["**/__tests__/**/*.test.ts", "**/?(*.)+(spec|test).ts"],
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/**/*.test.ts"],
coverageReporters: ["text", "lcov", "html"],
verbose: true,
forceExit: true,
detectOpenHandles: true,
};Best Practices for Jest Integration
1. Test Isolation
// Ensure each test is isolated
describe("Isolated Tests", () => {
// Each describe block gets its own app instance
const {useService} = useNodeBoot(MyApp);
it("test 1", () => {
// This test won't affect others
});
});2. Mock Management
// Use beforeEach for consistent mock setup
describe("Service Tests", () => {
const {useService, useSpy} = useNodeBoot(MyApp);
let emailSpy: jest.SpyInstance;
beforeEach(() => {
emailSpy = useSpy(EmailService, "sendEmail");
});
afterEach(() => {
// Spies are automatically restored by the framework
expect(emailSpy).toHaveBeenCalledTimes(expect.any(Number));
});
});3. Async Testing
// Always use async/await for async operations
it("should handle async operations", async () => {
const service = useService(AsyncService);
// Wait for async operations to complete
const result = await service.performAsyncOperation();
expect(result).toBeDefined();
// Wait for side effects
await new Promise(resolve => setTimeout(resolve, 100));
});4. Error Testing
// Test both success and failure scenarios
describe("Error Scenarios", () => {
it("should handle errors gracefully", async () => {
const {useHttp} = useNodeBoot(MyApp, ({useMock}) => {
useMock(ServiceClass, {
method: jest.fn(() => {
throw new Error("Test error");
}),
});
});
const {get} = useHttp();
const response = await get("/api/endpoint");
expect(response.status).toBe(500);
});
});Migration from Other Test Frameworks
From Manual Jest Setup
// Before (manual Jest setup)
describe("Manual Setup", () => {
let app: Application;
let server: any;
beforeAll(async () => {
app = new MyApp();
server = await app.start();
});
afterAll(async () => {
await server.close();
});
it("should work", async () => {
// Manual HTTP requests
const response = await fetch("http://localhost:3000/api/test");
expect(response.status).toBe(200);
});
});
// After (NodeBoot Jest)
describe("NodeBoot Setup", () => {
const {useHttp} = useNodeBoot(MyApp);
it("should work", async () => {
const {get} = useHttp();
const response = await get("/api/test");
expect(response.status).toBe(200);
});
});Troubleshooting Jest Integration
Common Jest Issues
Tests hanging or not exiting
// Ensure proper cleanup const {useCleanup} = useNodeBoot(MyApp, ({useCleanup}) => { useCleanup({ afterAll: () => { // Close all connections }, }); });Timer issues
// Use real timers for integration tests beforeAll(() => { jest.useRealTimers(); });Memory leaks
// Monitor for open handles afterAll(() => { // Ensure all resources are closed });
License
MIT License - see LICENSE file for details.
