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.

referencesapimocking.md

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

External API Mocking

MSW (Mock Service Worker)

Setup

import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const mockServer = setupServer();

beforeAll(() => mockServer.listen({ onUnhandledRequest: 'error' }));
afterEach(() => mockServer.resetHandlers());
afterAll(() => mockServer.close());

Basic Mocking

// Mock GET request
mockServer.use(
  http.get('https://api.example.com/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Test User',
      email: 'test@example.com',
    });
  })
);

// Mock POST request
mockServer.use(
  http.post('https://api.example.com/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { id: 'new-id', ...body },
      { status: 201 }
    );
  })
);

Error Responses

// Mock 404 error
mockServer.use(
  http.get('https://api.example.com/users/:id', () => {
    return new HttpResponse(null, { status: 404 });
  })
);

// Mock 500 error
mockServer.use(
  http.post('https://api.example.com/users', () => {
    return HttpResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  })
);

// Mock timeout
mockServer.use(
  http.get('https://api.example.com/slow', async () => {
    await new Promise(r => setTimeout(r, 30000));
    return HttpResponse.json({});
  })
);

Conditional Responses

let callCount = 0;

mockServer.use(
  http.post('https://api.example.com/flaky', () => {
    callCount++;
    if (callCount < 3) {
      return new HttpResponse(null, { status: 503 });
    }
    return HttpResponse.json({ success: true });
  })
);

Nock

Setup

import * as nock from 'nock';

beforeEach(() => {
  nock.cleanAll();
});

afterAll(() => {
  nock.restore();
});

Basic Mocking

// Mock GET request
nock('https://api.example.com')
  .get('/users/123')
  .reply(200, {
    id: '123',
    name: 'Test User',
  });

// Mock POST request
nock('https://api.example.com')
  .post('/users', { email: 'test@example.com', name: 'Test' })
  .reply(201, { id: 'new-id', email: 'test@example.com', name: 'Test' });

Multiple Responses

// First 2 calls fail, 3rd succeeds
nock('https://api.example.com')
  .get('/data')
  .times(2)
  .reply(503)
  .get('/data')
  .reply(200, { value: 'success' });

Query Parameters

nock('https://api.example.com')
  .get('/search')
  .query({ q: 'test', page: 1 })
  .reply(200, { results: [] });

Headers

nock('https://api.example.com', {
  reqheaders: {
    'Authorization': 'Bearer token123',
  },
})
.get('/protected')
.reply(200, { data: 'secret' });

Complete Test Examples

Payment Gateway Integration

describe('Payment Integration E2E', () => {
  const mockServer = setupServer();

  beforeAll(() => mockServer.listen({ onUnhandledRequest: 'error' }));
  afterEach(() => mockServer.resetHandlers());
  afterAll(() => mockServer.close());

  it('should process successful payment', async () => {
    // GIVEN: Payment gateway returns success
    mockServer.use(
      http.post('https://api.stripe.com/v1/charges', () => {
        return HttpResponse.json({
          id: 'ch_123',
          status: 'succeeded',
          amount: 9999,
          currency: 'usd',
        });
      })
    );

    // WHEN: Processing payment
    const response = await request(httpServer)
      .post('/api/v1/checkout')
      .send({
        orderId: 'order-123',
        paymentMethod: 'pm_card_visa',
      })
      .expect(200);

    // THEN: Payment processed
    expect(response.body.data).toMatchObject({
      status: 'completed',
      chargeId: 'ch_123',
    });
  });

  it('should handle card declined', async () => {
    // GIVEN: Card is declined
    mockServer.use(
      http.post('https://api.stripe.com/v1/charges', () => {
        return HttpResponse.json(
          {
            error: {
              type: 'card_error',
              code: 'card_declined',
              message: 'Your card was declined.',
            },
          },
          { status: 402 }
        );
      })
    );

    // WHEN: Processing payment
    const response = await request(httpServer)
      .post('/api/v1/checkout')
      .send({
        orderId: 'order-123',
        paymentMethod: 'pm_card_declined',
      })
      .expect(402);

    // THEN: Error returned
    expect(response.body.code).toBe('CARD_DECLINED');
  });

  it('should retry on transient failure', async () => {
    let attempts = 0;

    // GIVEN: First 2 attempts fail, 3rd succeeds
    mockServer.use(
      http.post('https://api.stripe.com/v1/charges', () => {
        attempts++;
        if (attempts < 3) {
          return new HttpResponse(null, { status: 503 });
        }
        return HttpResponse.json({
          id: 'ch_123',
          status: 'succeeded',
        });
      })
    );

    // WHEN: Processing payment
    const response = await request(httpServer)
      .post('/api/v1/checkout')
      .send({ orderId: 'order-123', paymentMethod: 'pm_card_visa' })
      .expect(200);

    // THEN: Payment eventually succeeds
    expect(response.body.data.status).toBe('completed');
    expect(attempts).toBe(3);
  });
});

Exchange Rate API

describe('Currency Conversion E2E', () => {
  beforeEach(() => {
    nock.cleanAll();
  });

  it('should convert currency using exchange rate', async () => {
    // GIVEN: Exchange rate API returns rate
    nock('https://api.exchangerate.io')
      .get('/v1/rates')
      .query({ base: 'USD', symbols: 'EUR' })
      .reply(200, {
        rates: { EUR: 0.85 },
        timestamp: Date.now(),
      });

    // WHEN: Converting currency
    const response = await request(httpServer)
      .get('/api/v1/convert')
      .query({ from: 'USD', to: 'EUR', amount: 100 })
      .expect(200);

    // THEN: Converted correctly
    expect(response.body.data).toMatchObject({
      from: 'USD',
      to: 'EUR',
      amount: 100,
      result: 85,
      rate: 0.85,
    });
  });

  it('should use cached rate when API unavailable', async () => {
    // GIVEN: Rate is cached
    await redisHelper.setTestData('exchange:USD:EUR', { rate: 0.84 }, 3600);

    // GIVEN: API is unavailable
    nock('https://api.exchangerate.io')
      .get('/v1/rates')
      .query(true)
      .reply(503);

    // WHEN: Converting currency
    const response = await request(httpServer)
      .get('/api/v1/convert')
      .query({ from: 'USD', to: 'EUR', amount: 100 })
      .expect(200);

    // THEN: Uses cached rate
    expect(response.body.data.result).toBe(84);
  });
});

Email Service

describe('Email Service E2E', () => {
  const mockServer = setupServer();

  beforeAll(() => mockServer.listen());
  afterEach(() => mockServer.resetHandlers());
  afterAll(() => mockServer.close());

  it('should send welcome email on registration', async () => {
    const sentEmails: any[] = [];

    // GIVEN: Email service accepts requests
    mockServer.use(
      http.post('https://api.sendgrid.com/v3/mail/send', async ({ request }) => {
        const body = await request.json();
        sentEmails.push(body);
        return new HttpResponse(null, { status: 202 });
      })
    );

    // WHEN: User registers
    await request(httpServer)
      .post('/api/v1/auth/register')
      .send({
        email: 'newuser@example.com',
        name: 'New User',
        password: 'SecurePass123!',
      })
      .expect(201);

    // THEN: Welcome email sent
    expect(sentEmails).toHaveLength(1);
    expect(sentEmails[0]).toMatchObject({
      personalizations: [
        { to: [{ email: 'newuser@example.com' }] },
      ],
      template_id: 'welcome-email-template',
    });
  });
});

Best Practices

1. Always Clean Up

afterEach(() => {
  mockServer.resetHandlers();  // MSW
  nock.cleanAll();             // Nock
});

afterAll(() => {
  mockServer.close();  // MSW
  nock.restore();      // Nock
});

2. Use Error Mode for Unhandled Requests

// MSW: Throw error on unhandled requests
mockServer.listen({ onUnhandledRequest: 'error' });

// Nock: Disable real HTTP
nock.disableNetConnect();
nock.enableNetConnect('localhost');  // Allow localhost

3. Capture Requests for Verification

const capturedRequests: any[] = [];

mockServer.use(
  http.post('https://api.example.com/webhook', async ({ request }) => {
    const body = await request.json();
    capturedRequests.push({
      url: request.url,
      body,
      headers: Object.fromEntries(request.headers),
    });
    return new HttpResponse(null, { status: 200 });
  })
);

// Later in test
expect(capturedRequests).toHaveLength(1);
expect(capturedRequests[0].body).toMatchObject({ event: 'order.created' });

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.