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.

referencescommonbest-practices.md

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

E2E Testing Best Practices

Test Design Best Practices

1. Test One Behavior Per Test

// ❌ BAD - Multiple behaviors in one test
it('should create, update, and delete user', async () => {
  const createRes = await request(httpServer).post('/users').send(data);
  const updateRes = await request(httpServer).patch(`/users/${id}`).send(update);
  await request(httpServer).delete(`/users/${id}`).expect(204);
});

// ✅ GOOD - Separate tests for each behavior
it('should create user', async () => { /* ... */ });
it('should update user', async () => { /* ... */ });
it('should delete user', async () => { /* ... */ });

2. Use Descriptive Test Names

// ❌ BAD - Vague names
it('should work', async () => { /* ... */ });
it('should handle error', async () => { /* ... */ });

// ✅ GOOD - Descriptive names with behavior and context
it('should return 201 when creating user with valid data', async () => { /* ... */ });
it('should return 400 when email format is invalid', async () => { /* ... */ });
it('should return 404 when user not found', async () => { /* ... */ });

3. Assert Specific Values, Not Just Types

// ❌ BAD - Only checking existence/type
expect(response.body.data).toBeDefined();
expect(typeof response.body.data.id).toBe('string');

// ✅ GOOD - Assert specific expected values
expect(response.body).toEqual({
  code: ErrorCode.SUCCESS,
  message: 'User created successfully',
  data: {
    id: expect.any(String),
    email: 'test@example.com',
    name: 'Test User',
    createdAt: expect.any(String),
  },
});

4. No Conditional Assertions

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

// ✅ GOOD - Separate deterministic tests
it('should return user on success', async () => {
  await userModel.create({ _id: userId, email: 'test@example.com' });
  const response = await request(httpServer).get(`/users/${userId}`).expect(200);
  expect(response.body.data.email).toBe('test@example.com');
});

it('should return 404 when not found', async () => {
  await request(httpServer).get('/users/nonexistent').expect(404);
});

Test Data Best Practices

1. Use Factories for Test Data

// test/factories/user.factory.ts
export const createTestUser = (overrides?: Partial<User>): User => ({
  id: `user-${Date.now()}`,
  email: `test-${Date.now()}@example.com`,
  name: 'Test User',
  status: 'active',
  ...overrides,
});

// Usage in tests
const user = createTestUser({ name: 'Custom Name' });

2. Seed Minimal Data

// ❌ BAD - Seeding too much data
await userRepository.save([
  { email: 'user1@test.com', name: 'User 1', /* 10+ fields */ },
  { email: 'user2@test.com', name: 'User 2', /* 10+ fields */ },
  // ... 50 more users
]);

// ✅ GOOD - Seed only what's needed for the test
const user = await userRepository.save({
  email: 'test@example.com',
  name: 'Test User',
});

3. Generate Unique Identifiers

// ❌ BAD - Hardcoded IDs (cause conflicts)
const userId = 'user-123';

// ✅ GOOD - Unique IDs per test
const userId = `user-${Date.now()}-${Math.random().toString(36).slice(2)}`;

Cleanup Best Practices

1. Clean in Both beforeEach AND afterEach

beforeEach(async () => {
  await new Promise(r => setTimeout(r, 1000)); // Wait for in-flight
  await userModel.deleteMany({});
  await orderModel.deleteMany({});
});

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

2. Respect Foreign Key Constraints

// Clean in correct order (children first)
beforeEach(async () => {
  await orderItemRepository.delete({});
  await orderRepository.delete({});
  await userRepository.delete({});
});

// Or use CASCADE
await dataSource.query('SET session_replication_role = replica');
await dataSource.query('TRUNCATE users, orders CASCADE');
await dataSource.query('SET session_replication_role = DEFAULT');

Async Testing Best Practices

1. Smart Polling Over Fixed Waits

// ❌ BAD - Fixed wait (always slow)
await new Promise(r => setTimeout(r, 8000));
const result = await repository.findOne({ id });

// ✅ GOOD - Poll until condition met
async function waitFor<T>(
  fn: () => Promise<T | null>,
  timeout = 10000,
  interval = 100
): Promise<T> {
  const startTime = Date.now();
  while (Date.now() - startTime < timeout) {
    const result = await fn();
    if (result) return result;
    await new Promise(r => setTimeout(r, interval));
  }
  throw new Error('Timeout waiting for condition');
}

const result = await waitFor(() => repository.findOne({ id }));

2. Use Appropriate Timeouts

// Per-test timeout for async operations
jest.setTimeout(25000);

// Long timeout for setup
beforeAll(async () => {
  // ... setup
}, 90000);

// Cleanup timeout
afterAll(async () => {
  // ... cleanup
}, 30000);

Error Handling Best Practices

1. Test Error Responses Explicitly

it('should return validation error for invalid email', async () => {
  // GIVEN: Invalid email format
  const invalidData = { email: 'not-an-email', name: 'Test' };

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

  // THEN: Validation error returned
  expect(response.body).toMatchObject({
    code: ErrorCode.VALIDATION_ERROR,
    errors: expect.arrayContaining([
      expect.objectContaining({ field: 'email' }),
    ]),
  });
});

2. Verify Side Effects Don't Occur on Error

it('should not create user when validation fails', async () => {
  // GIVEN: Invalid data
  const invalidData = { email: 'invalid' };

  // WHEN: Attempting to create
  await request(httpServer)
    .post('/users')
    .send(invalidData)
    .expect(400);

  // THEN: No user created
  const count = await userRepository.count();
  expect(count).toBe(0);
});

Performance Best Practices

1. Reuse App Instance Across Tests in Same File

// ✅ GOOD - Single app instance per describe block
describe('User API E2E', () => {
  let app: INestApplication;

  beforeAll(async () => {
    // Create app ONCE
    app = await createTestApp();
  });

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

  // All tests use same app instance
});

2. Use Batch Operations for Setup

// ❌ BAD - Individual inserts
for (const user of users) {
  await userRepository.save(user);
}

// ✅ GOOD - Batch insert
await userRepository.save(users);

3. Parallel Container Startup

// ✅ GOOD - Start containers in parallel
beforeAll(async () => {
  const [kafka, mongo, redis] = await Promise.all([
    new KafkaContainer().start(),
    new MongoDBContainer().start(),
    new RedisContainer().start(),
  ]);
  // Setup time: ~6-8s vs ~15-20s sequential
}, 60000);

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.