All skills
bmad-labs avatar

/typescript-e2e-testing

@3bcc7a6
by bmad-labsbmad-labs/skills15 stars
4

E2E and integration testing for TypeScript/NestJS projects using Jest, supertest, and real infrastructure via Docker (Kafka, PostgreSQL, MongoDB, Redis) with the Given-When-Then pattern. Use whenever the user is working on `.e2e-spec.ts` files or anything under `test/e2e/`, or asks to set up, write, review, run, debug, or optimize E2E or integration tests — including flaky tests, docker-compose for tests, Kafka/Redpanda consumers, test isolation, or GWT compliance.

Use this Skill: https://skilld.dev/gh/bmad-labs/skills/typescript-e2e-testing

This session only. Nothing lands on disk.

referencescommonnestjs-setup.md

≈2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

NestJS E2E Test Setup

Project Structure

project-root/
├── src/
├── test/
│   ├── e2e/                          # E2E test files
│   │   ├── feature-name.e2e-spec.ts  # Feature-specific tests
│   │   ├── setup.ts                  # Global setup/teardown
│   │   └── helpers/                  # Test utilities
│   │       ├── test-app.helper.ts    # App bootstrapping
│   │       ├── database.helper.ts    # DB utilities
│   │       └── kafka.helper.ts       # Kafka utilities
│   └── jest-e2e.config.ts            # E2E Jest config
├── docker-compose.e2e.yml            # E2E infrastructure
├── .env.e2e                          # E2E environment
└── package.json

Jest Configuration

jest-e2e.config.ts

import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>'],
  testMatch: ['**/*.e2e-spec.ts'],
  moduleFileExtensions: ['js', 'json', 'ts'],
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  // ESM compatibility - map .js imports to .ts files
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  testTimeout: 25000,
  setupFilesAfterEnv: ['<rootDir>/setup.ts'],

  // CRITICAL: Run tests serially for database/messaging isolation
  maxWorkers: 1,

  // CRITICAL: Clean mock state between test files
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true,

  // Prevent hanging connections
  forceExit: true,
  detectOpenHandles: true,
};

export default config;

package.json Scripts

{
  "scripts": {
    "test:e2e": "jest --config test/jest-e2e.config.ts --runInBand",
    "test:e2e:watch": "jest --config test/jest-e2e.config.ts --runInBand --watch",
    "test:e2e:debug": "node --inspect-brk node_modules/.bin/jest --config test/jest-e2e.config.ts --runInBand",
    "e2e:infra:up": "docker-compose -f docker-compose.e2e.yml up -d",
    "e2e:infra:down": "docker-compose -f docker-compose.e2e.yml down -v",
    "e2e:logs": "tail -f logs/e2e-test.log"
  }
}

Running Tests (Context Efficient)

ALWAYS redirect output to temp files with unique session ID:

# Initialize session (once at start)
export E2E_SESSION=$(date +%s)-$$

# Run all E2E tests (no console output)
npm run test:e2e > /tmp/e2e-${E2E_SESSION}-output.log 2>&1 && tail -50 /tmp/e2e-${E2E_SESSION}-output.log

# Run specific test (no console output)
npm run test:e2e -- -t "test name" > /tmp/e2e-${E2E_SESSION}-output.log 2>&1 && tail -50 /tmp/e2e-${E2E_SESSION}-output.log

# Get failure details
grep -B 2 -A 15 "FAIL\|✕" /tmp/e2e-${E2E_SESSION}-output.log

# Debug mode (requires console for interactive debugging)
npm run test:e2e:debug

# Cleanup when done
rm -f /tmp/e2e-${E2E_SESSION}-*.log

Test Setup Pattern

Global Setup (setup.ts)

import * as fs from 'fs';

// Clean log file before test run
beforeAll(() => {
  const logFile = './logs/e2e-test.log';
  if (fs.existsSync(logFile)) {
    fs.unlinkSync(logFile);
  }
  // Ensure logs directory exists
  if (!fs.existsSync('./logs')) {
    fs.mkdirSync('./logs', { recursive: true });
  }
});

Test App Bootstrap Helper

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HttpAdapterHost, Reflector } from '@nestjs/core';
import { ClsService } from 'nestjs-cls';
import { Transport } from '@nestjs/microservices';

export interface TestSetup {
  app: INestApplication;
  httpServer: any;
  moduleFixture: TestingModule;
}

export async function createTestApp(
  modules: any[],
  options?: { kafka?: boolean }
): Promise<TestSetup> {
  const moduleFixture = await Test.createTestingModule({
    imports: [
      ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env.e2e' }),
      ...modules,
    ],
  }).compile();

  const app = moduleFixture.createNestApplication();
  const { httpAdapter } = app.get(HttpAdapterHost);
  const reflector = app.get(Reflector);
  const cls = app.get(ClsService);
  const configService = app.get(ConfigService);

  // ✅ CRITICAL: Use custom logger (same as main.ts)
  app.useLogger(app.get(CustomLoggerService));

  // ✅ CRITICAL: Global interceptors (same as main.ts)
  app.useGlobalInterceptors(new HttpRequestLoggingInterceptor(cls, reflector));

  // ✅ CRITICAL: Global exception filters (same as main.ts)
  app.useGlobalFilters(new UnknownExceptionsFilter(httpAdapter));
  app.useGlobalFilters(new DefaultValidateExceptionFilter(httpAdapter));
  app.useGlobalFilters(new DefaultInternalExceptionFilter(httpAdapter));
  app.useGlobalFilters(new HttpExceptionFilter(httpAdapter));

  // ✅ CRITICAL: ValidationPipe BEFORE connectMicroservice
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    transform: true,
    forbidNonWhitelisted: true,
  }));

  if (options?.kafka) {
    const uniqueGroupId = `${configService.get('KAFKA_GROUP_ID')}-e2e-${Date.now()}`;

    app.connectMicroservice({
      transport: Transport.KAFKA,
      options: {
        client: {
          clientId: configService.get('KAFKA_CLIENT_ID'),
          brokers: [configService.get('KAFKA_BROKER')],
        },
        consumer: {
          groupId: uniqueGroupId,
          allowAutoTopicCreation: true,
        },
        subscribe: { fromBeginning: false },
      },
    }, { inheritAppConfig: true });

    await app.startAllMicroservices();
  }

  await app.init();

  return {
    app,
    httpServer: app.getHttpServer(),
    moduleFixture,
  };
}

Complete Test Suite Example

describe('Feature E2E', () => {
  let app: INestApplication;
  let httpServer: any;
  let userRepository: Repository<User>;

  jest.setTimeout(25000);

  beforeAll(async () => {
    const setup = await createTestApp([AppModule], { kafka: true });
    app = setup.app;
    httpServer = setup.httpServer;
    userRepository = setup.moduleFixture.get(getRepositoryToken(User));

    // Wait for Kafka consumer to be ready
    await new Promise(r => setTimeout(r, 5000));
  }, 90000);

  afterAll(async () => {
    await app?.close();
  }, 30000);

  beforeEach(async () => {
    await new Promise(r => setTimeout(r, 1000));
    await userRepository.clear();
  });

  afterEach(async () => {
    await userRepository.clear();
  });

  it('should create user', async () => {
    // GIVEN: Valid user data
    const createUserDto = { email: 'test@example.com', name: 'Test' };

    // WHEN: Creating user
    const response = await request(httpServer)
      .post('/users')
      .send(createUserDto)
      .expect(201);

    // THEN: User created
    expect(response.body.data.email).toBe('test@example.com');

    // THEN: Persisted in database
    const savedUser = await userRepository.findOne({
      where: { email: 'test@example.com' },
    });
    expect(savedUser).toBeDefined();
  });
});

Common Issues and Solutions

Issue: Tests hang after completion

// Solution 1: Force exit in jest config
forceExit: true,
detectOpenHandles: true,

// Solution 2: Proper cleanup in afterAll
afterAll(async () => {
  await app?.close();
}, 30000);

Issue: Port conflicts between test runs

// Solution: Use random ports
const app = await NestFactory.create(AppModule);
await app.listen(0); // Random available port

Issue: Database state pollution

// Solution: Clean before AND after each test
beforeEach(async () => {
  await connection.synchronize(true); // PostgreSQL
  await collection.deleteMany({});    // MongoDB
});

afterEach(async () => {
  await collection.deleteMany({});
});

Issue: Kafka consumer lag

// Solution: Use unique consumer groups and skip old messages
const uniqueGroupId = `test-group-${Date.now()}-${Math.random()}`;
subscribe: { fromBeginning: false }

Issue: Tests fail intermittently (flaky)

// Solution: Ensure sequential execution
// In jest config:
maxWorkers: 1
// In npm script:
"test:e2e": "jest --runInBand"

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides comprehensive workflows and reference materials for E2E testing in TypeScript/NestJS projects. It uses standard development tools like Jest, Docker, and shell commands. No malicious patterns or security risks were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    30/103 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 3bcc7a6. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 5 months ago
  • TypeScript
  • jest
  • nestjs
  • e2e-testing
  • supertest
  • docker
  • kafka
  • postgres
  • mongodb
  • redis
  • integration-testing
  • gwt-pattern

README badge

README badge for bmad-labs/skills

Writes E2E and integration tests for NestJS projects using Jest and supertest against real Docker infrastructure (Kafka, PostgreSQL, MongoDB, Redis) following the Given-When-Then pattern. Covers test setup, writing, review, debugging, and optimization workflows with technology-specific helpers and isolation strategies for each service.

Generated from the current SKILL.md.

Does this skill work with NestJS only?
The skill is built for NestJS projects using Jest and supertest, but the patterns and workflows apply to any TypeScript backend with HTTP endpoints. Technology-specific helpers exist for Kafka, PostgreSQL, MongoDB, and Redis.
Do I need Docker running to use this skill?
Yes. The skill assumes real infrastructure via Docker (Kafka, PostgreSQL, MongoDB, Redis). Tests execute against actual services, not mocks.
What if my E2E tests are flaky or failing?
The Debugging E2E Test workflow provides a systematic protocol: fix one test at a time using isolated test runs, verify with 3-5 consecutive runs before moving to the next failure, then run the full suite only once all tests pass individually.
Does this skill cover GraphQL or gRPC testing?
Yes. The API testing reference includes examples for REST, GraphQL, and gRPC, plus external API mocking with MSW and Nock.
What is the Given-When-Then pattern and is it mandatory?
GWT is the mandatory test structure: Given describes setup state, When describes the action under test, Then describes expected outcomes. All E2E tests in this skill must follow this pattern.

Generated from the current SKILL.md. These answers refresh after source changes.