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.

referencescommontest-case-creation-guide.md

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

E2E Test Case Creation Guide

GWT Pattern (Given-When-Then)

CRITICAL: ALL E2E tests MUST follow GWT pattern with explicit comments.

it('should [expected behavior]', async () => {
  // GIVEN: [preconditions and context]
  // Setup test data, initial state, dependencies

  // WHEN: [single action being tested]
  // ONE primary action only

  // THEN: [expected outcomes]
  // Verify response, database state, side effects
});

GWT Rules

Rule Description
Comments Required Use // GIVEN:, // WHEN:, // THEN: (uppercase with colon)
GIVEN All test data setup MUST happen before WHEN
WHEN Contains ONLY ONE primary action
THEN Verify ALL relevant outcomes
One Test = One Behavior Split multiple scenarios into separate tests

Test Case Templates

REST API Success

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

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

  // THEN: User is created with correct data
  expect(response.body).toMatchObject({
    code: ErrorCode.SUCCESS,
    data: {
      email: 'test@example.com',
      name: 'Test User',
    },
  });
  expect(response.body.data.id).toBeDefined();

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

REST API Validation Error

it('should return 400 for invalid email format', async () => {
  // GIVEN: User data with invalid email
  const invalidUserDto = {
    email: 'not-an-email',
    name: 'Test User',
    password: 'SecurePass123!',
  };

  // WHEN: Attempting to create user with invalid email
  const response = await request(httpServer)
    .post('/users')
    .send(invalidUserDto)
    .expect(400);

  // THEN: Validation error returned
  expect(response.body).toMatchObject({
    code: ErrorCode.VALIDATION_ERROR,
    message: expect.stringContaining('email'),
  });
});

REST API Not Found

it('should return 404 when user not found', async () => {
  // GIVEN: No user exists with this ID
  const nonExistentId = 'user-does-not-exist';

  // WHEN: Fetching non-existent user
  const response = await request(httpServer)
    .get(`/users/${nonExistentId}`)
    .expect(404);

  // THEN: Not found error returned
  expect(response.body).toMatchObject({
    code: ErrorCode.NOT_FOUND,
    message: 'User not found',
  });
});

Database Query with Pagination

it('should return paginated list of active users', async () => {
  // GIVEN: Multiple users in database
  await userRepository.save([
    { email: 'user1@test.com', name: 'User 1', status: 'active' },
    { email: 'user2@test.com', name: 'User 2', status: 'active' },
    { email: 'user3@test.com', name: 'User 3', status: 'inactive' },
  ]);

  // WHEN: Fetching active users with pagination
  const response = await request(httpServer)
    .get('/users')
    .query({ status: 'active', page: 1, limit: 10 })
    .expect(200);

  // THEN: Only active users returned with pagination metadata
  expect(response.body.data).toHaveLength(2);
  expect(response.body.data.every(u => u.status === 'active')).toBe(true);
  expect(response.body.meta).toMatchObject({
    page: 1,
    limit: 10,
    total: 2,
  });
});

Kafka Event Processing

it('should process order created event and update inventory', async () => {
  // GIVEN: Product with available stock
  const product = await productRepository.save({
    id: 'prod-123',
    name: 'Test Product',
    stock: 100,
  });

  // GIVEN: Order created event
  const orderEvent = {
    orderId: 'order-456',
    productId: 'prod-123',
    quantity: 5,
  };

  // WHEN: Publishing order created event
  await kafkaHelper.publishEvent('order.created', orderEvent);

  // THEN: Wait for async processing (using smart polling)
  const updatedProduct = await waitFor(
    () => productRepository.findOne({ where: { id: 'prod-123', stock: 95 } }),
    20000
  );

  // THEN: Inventory is updated
  expect(updatedProduct).toBeDefined();
  expect(updatedProduct.stock).toBe(95);
});

Kafka Event with Output Topic

it('should process message and produce to output topic', async () => {
  // GIVEN: Input event
  const inputEvent = { id: 'test-1', data: 'test-data' };

  // WHEN: Publishing to input topic
  await kafkaHelper.publishEvent(inputTopic, inputEvent, inputEvent.id);

  // THEN: Output message received
  const outputMessages = await kafkaHelper.waitForMessages(outputTopic, 1, 20000);

  expect(outputMessages.length).toBe(1);
  expect(outputMessages[0].value).toMatchObject({
    id: 'test-1',
    processed: true,
  });
});

External API Mock

it('should handle payment gateway timeout gracefully', async () => {
  // GIVEN: Payment request data
  const paymentDto = {
    orderId: 'order-123',
    amount: 99.99,
    currency: 'USD',
  };

  // GIVEN: Payment gateway configured to timeout
  mockPaymentGateway.onPost('/charge').timeout();

  // WHEN: Processing payment
  const response = await request(httpServer)
    .post('/payments')
    .send(paymentDto)
    .expect(503);

  // THEN: Graceful error response
  expect(response.body).toMatchObject({
    code: ErrorCode.PAYMENT_SERVICE_UNAVAILABLE,
    message: 'Payment service is temporarily unavailable',
  });

  // THEN: Order status updated to payment_failed
  const order = await orderRepository.findOne({
    where: { id: 'order-123' },
  });
  expect(order.paymentStatus).toBe('failed');
});

Authentication Flow

it('should return 401 for expired JWT token', async () => {
  // GIVEN: User with expired token
  const expiredToken = generateJwt({ userId: 'user-123' }, { expiresIn: '-1h' });

  // WHEN: Accessing protected endpoint with expired token
  const response = await request(httpServer)
    .get('/users/me')
    .set('Authorization', `Bearer ${expiredToken}`)
    .expect(401);

  // THEN: Unauthorized error returned
  expect(response.body).toMatchObject({
    code: ErrorCode.TOKEN_EXPIRED,
    message: 'Token has expired',
  });
});

Test Naming Conventions

Format

should [action/outcome] when [condition]

Examples by Category

Success Scenarios:

'should create user and return 201'
'should return user by id'
'should update user profile'
'should delete user and cascade orders'

Error Scenarios:

'should return 400 for invalid email format'
'should return 404 when user not found'
'should return 401 for expired token'
'should return 409 for duplicate email'

Business Logic:

'should calculate discount for premium users'
'should apply tax based on shipping address'
'should send notification after order completion'

Edge Cases:

'should handle empty result set gracefully'
'should process batch of 1000 items within timeout'
'should recover from transient database error'

Anti-Patterns to Avoid

Multiple WHEN Actions

// ❌ WRONG: Multiple actions in one test
it('should create and update user', async () => {
  // WHEN: Creating user
  const createResponse = await request(httpServer).post('/users').send(userData);

  // WHEN: Updating user (WRONG - second action)
  const updateResponse = await request(httpServer)
    .patch(`/users/${createResponse.body.data.id}`)
    .send({ name: 'Updated' });
});

// ✅ CORRECT: Split into separate tests
it('should create user', async () => { /* ... */ });
it('should update existing user', async () => { /* ... */ });

Conditional Assertions

// ❌ WRONG: Conditional logic in assertions
if (response.status === 200) {
  expect(response.body.data).toBeDefined();
} else {
  expect(response.body.error).toBeDefined();
}

// ✅ CORRECT: Separate deterministic tests
it('should return users on success', async () => { /* ... */ });
it('should return error on failure', async () => { /* ... */ });

Missing Specific Assertions

// ❌ WRONG: Only checking existence
expect(response.body.data).toBeDefined();

// ✅ CORRECT: Assert specific values
expect(response.body).toMatchObject({
  code: ErrorCode.SUCCESS,
  data: {
    id: expect.any(String),
    email: 'test@example.com',
  },
});

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.