Skip to content

Repository files navigation

NodeBoot Integration Test Framework

A comprehensive, extensible testing framework for NodeBoot applications that provides a plugin-based architecture for dependency injection testing, service mocking, and lifecycle management.

Table of Contents

  1. Quick Start
  2. Architecture Overview
  3. Hook Types: Setup vs Return Hooks
  4. Available Hooks
  5. Best Practices
  6. Troubleshooting
  7. Migration Guide
  8. Hook Reference
  9. Extending

Quick Start

Installation

npm install @nodeboot/node-test
# or
pnpm add @nodeboot/node-test

Basic Usage

import{describe,test}from"node:test";importassertfrom"node:assert/strict";import{useNodeBoot}from"@nodeboot/node-test";import{MyApp}from"./MyApp";describe("My App Integration Tests",()=>{const{useHttp, useService, useConfig}=useNodeBoot(MyApp,({useConfig, useMock, useEnv, usePactum})=>{// Test configurationuseConfig({app: {port: 3001},database: {url: "sqlite::memory:"},});// Environment variablesuseEnv({NODE_ENV: "test"});// Enable Pactum for HTTP testingusePactum();// Mock a serviceuseMock(EmailService,{sendEmail: ()=>Promise.resolve(),});});test("should handle API requests",async()=>{const{get}=useHttp();constresponse=awaitget("/api/users");assert.equal(response.status,200);});test("should access services",()=>{constuserService=useService(UserService);assert.ok(userService,"UserService should be defined");});});

Advanced Usage

For comprehensive integration test examples using the NodeBoot Test Framework with Node.js test runner, refer to the Node.js Test Demo project.

Feel free to explore it, run it, and modify it to get a hands-on understanding of how to leverage the NodeBoot Test Framework effectively.

You can also check the demo projects:

Architecture Overview

The NodeBoot Test Framework follows a layered, plugin-based architecture designed for maximum extensibility and composability:

┌─────────────────────────────────────────────────────────┐
│ Test Runner Integration │
│ (node:test, Mocha, Vitest, etc.) │
├─────────────────────────────────────────────────────────┤
│ Custom Hook Libraries │
│ (MochaHooksLibrary, etc.) │
├─────────────────────────────────────────────────────────┤
│ Core Framework │
│ (NodeBootTestFramework, HookManager, HooksLibrary) │
├─────────────────────────────────────────────────────────┤
│ Hook System │
│ (Hook base class, lifecycle phases) │
├─────────────────────────────────────────────────────────┤
│ NodeBoot Application │
│ (IoC Container, Services, Config) │
└─────────────────────────────────────────────────────────┘

Key Design Principles

  1. Plugin Architecture: Everything is a hook that can be added, removed, or customized
  2. Lifecycle-Driven: Clear, predictable execution phases
  3. Priority-Based: Hooks execute in controlled order based on priority
  4. State Management: Hooks can store and share state across lifecycle phases
  5. Test Runner Agnostic: Core framework works with any test runner
  6. Composable: Hook libraries can extend and combine functionality

3. Hook System

The framework uses a priority-based hook system that executes in phases:

  • beforeStart: Setup before application starts
  • afterStart: Configuration after application starts
  • beforeTests: Setup before test suite runs
  • afterTests: Cleanup after test suite completes
  • beforeEachTest: Setup before each individual test
  • afterEachTest: Cleanup after each individual test

Hook Types: Setup vs Return Hooks

The NodeBoot Test Framework provides two distinct types of hooks that serve different purposes in your test lifecycle:

Setup Hooks (Configuration Phase)

Setup hooks are called during test configuration and are used to prepare your test environment before the application starts. These hooks configure how your application will run during tests.

Key Characteristics:

  • Execute during the setup callback function passed to useNodeBoot()
  • Run before the application starts
  • Used for configuration, mocking, and environment setup
  • Cannot access running application services or HTTP endpoints
  • Changes take effect when the application starts

Usage Pattern:

consthooks=useNodeBoot(MyApp,({useConfig, useMock, useEnv})=>{// These are Setup Hooks - they configure the test environmentuseConfig({database: {url: "sqlite::memory:"}});useMock(EmailService,{sendEmail: ()=>Promise.resolve()});useEnv({NODE_ENV: "test"});});

Common Setup Hooks:

  • useConfig() - Override application configuration
  • useMock() - Mock service implementations
  • useEnv() - Set environment variables
  • usePactum() - Enable HTTP testing tools
  • useCleanup() - Register cleanup functions
  • useAddress() - Get server address after startup

Return Hooks (Runtime Phase)

Return hooks are returned from useNodeBoot() and are used during test execution to interact with your running application. These hooks provide access to services, repositories, and HTTP clients.

Key Characteristics:

  • Available after useNodeBoot() returns
  • Execute during test runtime when called
  • Used for interacting with the running application
  • Can access services, make HTTP requests, and query data
  • Provide the actual testing capabilities

Usage Pattern:

const{useService, useHttp, useRepository}=useNodeBoot(MyApp,setupCallback);it("should work with services",()=>{// These are Return Hooks - they interact with the running appconstuserService=useService(UserService);const{get, post}=useHttp();constuserRepo=useRepository(UserRepository);});

Common Return Hooks:

  • useService() - Access IoC container services
  • useRepository() - Access data repositories
  • useHttp() - HTTP client for API testing
  • useSupertest() - Supertest instance for HTTP testing
  • useConfig() - Access current configuration (read-only)
  • useSpy() - Create spies on service methods

Execution Timeline

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Setup Phase │ │ Application │ │ Test Execution │
│ │ │ Startup │ │ │
│ Setup Hooks │───▶│ │───▶│ Return Hooks │
│ - useConfig() │ │ - Load config │ │ - useService() │
│ - useMock() │ │ - Start server │ │ - useHttp() │
│ - useEnv() │ │ - Initialize │ │ - useRepo() │
└─────────────────┘ └──────────────────┘ └─────────────────┘

Best Practices

  1. Use Setup Hooks for Configuration:

    // ✅ Good - Configure before app startsuseNodeBoot(AppUnderTest,({useConfig, useMock})=>{useConfig({port: 3001});useMock(EmailService,mockImpl);});
  2. Use Return Hooks for Testing:

    // ✅ Good - Test the running applicationconst{useService, useHttp}=useNodeBoot(AppUnderTest,setup);it("should work",()=>{constservice=useService(MyService);expect(service.doSomething()).toBeTruthy();});
  3. Don't Mix Hook Types:

    // ❌ Bad - Can't use return hooks in setupuseNodeBoot(AppUnderTest,({useConfig, useService})=>{// useService not available hereuseConfig({port: 3001});constservice=useService(MyService);// This will fail!});
  4. Understand Timing:

    // ✅ Good - Right timing for each hook typeconsthooks=useNodeBoot(AppUnderTest,({useConfig})=>{useConfig({database: {url: "test.db"}});// Setup: before app starts});it("should access database",()=>{constrepo=hooks.useRepository(UserRepo);// Runtime: after app started});

Available Hooks

Setup Hooks (Configuration Phase)

These hooks are called during the setup callback passed to useNodeBoot() and configure your test environment before the application starts.

useConfig(config: object)

Override application configuration for tests.

useConfig({app: {port: 3001},database: {url: "test.db"},redis: {host: "localhost",port: 6380},});

useEnv(variables: Record<string, string>)

Set environment variables for the test session.

useEnv({NODE_ENV: "test",API_KEY: "test-key",DEBUG: "true",});

useMock(ServiceClass, mockImplementation)

Mock service methods with automatic cleanup.

// Mock with plain function implementationsuseMock(EmailService,{sendEmail: ()=>Promise.resolve(),validateEmail: ()=>true,});// Mock with more complex implementationsuseMock(PaymentService,{processPayment: ()=>({success: true,transactionId: "test-123"}),});

useAddress(callback: (address: string) => void)

Access the server's listening address after startup.

useAddress(address=>{console.log("Server running at:",address);// Set up external test dependencies});

useAppContext(callback: (context: ApplicationContext) => void)

Access the application context for advanced setup.

useAppContext(context=>{expect(context.config).toBeDefined();expect(context.logger).toBeDefined();// Additional context validation or setup});

usePactum(baseUrl?: string)

Enable Pactum.js integration for HTTP testing.

usePactum();// Uses default server address// orusePactum("http://localhost:3001");// Custom base URL// Now you can use spec() from pactum directly in tests

useCleanup(hooks: { afterAll?: () => void, afterEach?: () => void })

Register cleanup functions that will be called automatically.

useCleanup({afterAll: ()=>{// Clean up test data, close connections, etc.},afterEach: ()=>{// Reset state between tests},});

Runtime Hooks (Test Execution Phase)

useService(ServiceClass)

Get service instances from the IoC container.

constuserService=useService(UserService);constresult=userService.findUser("123");

useRepository(RepositoryClass)

Get repository instances for data layer testing.

constuserRepo=useRepository(UserRepository);awaituserRepo.create({name: "Test User"});constusers=awaituserRepo.findAll();

useHttp(baseURL?: string)

Get HTTP client for API testing.

const{get, post, put,delete: del}=useHttp();// GET requestconstusers=awaitget("/api/users");// POST request with dataconstnewUser=awaitpost("/api/users",{name: "John Doe",email: "john@example.com",});// With headersconstresponse=awaitget("/api/protected",{headers: {Authorization: "Bearer token"},});// Custom base URLconstexternalApi=useHttp("https://api.external.com");constdata=awaitexternalApi.get("/data");

useSupertest()

Get Supertest instance for HTTP testing with built-in assertions.

constrequest=useSupertest();awaitrequest.get("/api/users").expect(200).expect("Content-Type",/json/).expect(res=>{expect(res.body).toHaveLength(1);});

useConfig()

Access the current configuration (read-only during test execution).

constconfig=useConfig();constport=config.getNumber("app.port");constdbUrl=config.getString("database.url");constisProduction=config.getBoolean("app.production",false);

Dual-Purpose Hooks

Some hooks can be used both during setup and test execution phases.

useAppContext() (Setup & Return)

Can be used as both a setup hook (with callback) and return hook (direct access).

// Setup usage - configure during setup phaseuseNodeBoot(AppUnderTest,({useAppContext})=>{useAppContext(context=>{// Configure based on application contextconsole.log("AppUnderTest started with config:",context.config);});});// Return usage - access during test executionconst{useAppContext}=useNodeBoot(AppUnderTest,setup);it("should access app context",()=>{useAppContext(context=>{expect(context.logger).toBeDefined();expect(context.config).toBeDefined();});});

Best Practices

1. Test Organization

import{describe,test}from"node:test";import{useNodeBoot}from"@nodeboot/node-test";describe("User Management",()=>{consthooks=useNodeBoot(AppUnderTest,commonSetup);describe("Authentication",()=>{// Auth-specific tests});describe("Profile Management",()=>{// Profile-specific tests});});

2. Mock Strategy

// Mock external dependencies, keep internal services realuseMock(EmailService,{sendEmail: ()=>Promise.resolve()});// ExternaluseMock(PaymentGateway,{charge: ()=>Promise.resolve()});// External// Don't mock UserService, OrderService, etc. (internal business logic)

3. Configuration Management

// Use environment-specific configsuseNodeBoot(AppUnderTest,({useConfig})=>{useConfig({app: {port: 20000,},database: {url: process.env.TEST_DB_URL||"sqlite::memory:"},redis: {host: "localhost",port: 6380},// Test Redis instanceexternal: {apiKey: "test-key",baseUrl: "http://localhost:8080",// Mock server},});});

4. Data Management

const{useRepository}=useNodeBoot(AppUnderTest);// Clean slate for each testbeforeEach(async()=>{constrepository=useRepository(UserRepository);awaitrepository.find({});});

5. Async Testing

5. Async Testing

import{test}from"node:test";importassertfrom"node:assert/strict";test("should handle async operations",async()=>{constservice=useService(AsyncService);// Use proper async/awaitconstresult=awaitservice.processAsync("data");assert.ok(result,"Result should be defined");// Verify async side effectsconstspy=useSpy(EmailService,"sendEmail");assert.equal(spy.callCount,1,"sendEmail should have been called");});

Troubleshooting

Common Issues

  1. Service Not Found Error

    Error: The class MyService is not decorated with @Service
    

    Ensure your service classes are properly decorated with @Service().

  2. IoC Container Not Found

    Error: IOC Container is required for useService hook to work
    

    Make sure your app is properly initialized with dependency injection.

  3. Port Conflicts Use random ports in tests:

    useConfig({app: {port: 0}});// Random available port
  4. Memory Leaks in Tests Ensure proper cleanup:

    useCleanup({afterAll: ()=>{// Close connections, clear caches, etc.},});

Debugging Tips

  1. Enable Debug Logging

    useEnv({DEBUG: "nodeboot:*"});
  2. Inspect Hook Execution The framework logs hook execution order and timing.

  3. Verify Mock Calls

    constspy=useSpy(Service,"method");console.log("Mock calls:",spy.calls);console.log("Call count:",spy.callCount);

Migration Guide

From Manual Setup

// Before (manual setup)beforeAll(async()=>{app=newMyApp();server=awaitapp.start();});afterAll(async()=>{awaitserver.close();});// After (NodeBoot Test Framework)const{useHttp}=useNodeBoot(MyApp);

Hook Reference

HookCategoryDescriptionDocs
useAddressSetupAccess server listening address after startupAddressHook
useAppContextSetup/TestAccess application context for advanced setup or during testsAppContextHook
useConfigSetup/TestOverride and read configuration for testsConfigHook
useEnvSetupSet environment variables for the test sessionEnvHook
useMockSetup/TestMock service methods with cleanup/restoreMockHook
useCleanupSetupRegister cleanup functions for automatic executionLifecycleHook
usePactumSetupEnable Pactum.js integration for HTTP testingPactumHook
useHttpTestHTTP client for calling app endpointsHttpClientHook
useServiceTestAccess services from IoC containerServiceHook
useRepositoryTestAccess repositories for persistence testsRepositoryHook
useSupertestTestSupertest agent for HTTP assertionsSupertestHook
useSpyTest (node-test)Create spies on service methodsSpyHook
useTimerTest (node-test)Control fake timers and track execution timeTimerHook
useMetricsTestRecord metrics and timers; retrieve summariesMetricsHook
usePerformanceBudgetSetup/TestEnforce per-label performance budgetsPerformanceBudgetHook
useFileSystemSandboxSetup/TestPer-test real filesystem sandboxFileSystemSandboxHook
useMemoryFileSystemSetup/TestIn-memory filesystem replacement (memfs)MemoryFileSystemHook
useLogCaptureSetup/TestCapture logs for assertion and diagnosticsLogCaptureHook
useLogMatchSetup/TestDeclarative log pattern expectations/forbidsLogMatchHook
useLoggerHookTestAccess the shared Winston logger in testsLoggerHook
useLifecycleSetupUnified lifecycle management (before/after)LifecycleHook
useMongoContainerSetup/TestReal MongoDB via DockerMongoContainerHook
useMongoMemoryServerSetup/TestIn-memory single MongoDB instanceMongoMemoryServerHook
useMongoMemoryReplSetSetup/TestIn-memory MongoDB replica set (transactions/change streams)MongoMemoryReplSetHook
useGenericContainerSetup/TestDeclarative Docker containers via TestcontainersGenericContainerHook
useGenericContainerRawSetup/TestRaw factory-based Testcontainers controlGenericContainerRawHook
useToxiproxySetup/TestNetwork toxicity simulationToxiproxyHook
useSnapshotStateSetup/TestDetect unintended shared state mutationsSnapshotStateHook
useResourceLeakDetectorSetup/TestDetect lingering resources/handles across testsResourceLeakDetectorHook
useHttpClientTestHTTP client (alias where applicable)HttpClientHook

Hook Scope Legend

  • Setup: Hooks called during the setup callback passed to useNodeBoot(). These configure the test environment before the application starts.
  • Test: Hooks returned from useNodeBoot() and used during test execution to interact with the running application.

Usage Pattern

// Setup hooks are used in the configuration callbackconsthooks=useNodeBoot(MyApp,({useConfig, useMock, useEnv})=>{useConfig({database: {url: "test.db"}});// Setup HookuseMock(EmailService,{sendEmail: ()=>Promise.resolve()});// Setup HookuseEnv({NODE_ENV: "test"});// Setup Hook});// Return/Test hooks are used during test executionconst{useService, useHttp, useSpy}=hooks;test("should work",()=>{constservice=useService(UserService);// Return Hookconst{get}=useHttp();// Return Hookconstspy=useSpy(EmailService,"sendEmail");// Return Hook});

Extending the Framework

License

MIT License - see LICENSE file for details.

Releases

Packages

Contributors

Languages