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.

referencescommonexamples.md

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

E2E Test Examples by Category

Quick Navigation


REST API Examples

Create Resource (POST)

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

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

  // THEN: User is created with correct data
  expect(response.body).toMatchObject({
    code: 'SUCCESS',
    message: 'User created successfully',
    data: {
      id: expect.any(String),
      email: 'john.doe@example.com',
      name: 'John Doe',
      createdAt: expect.any(String),
    },
  });

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

Read Resource (GET)

it('should return user by ID', async () => {
  // GIVEN: Existing user in database
  const user = await userRepository.save({
    email: 'existing@example.com',
    name: 'Existing User',
    status: 'active',
  });

  // WHEN: Fetching user by ID
  const response = await request(httpServer)
    .get(`/api/v1/users/${user.id}`)
    .expect(200);

  // THEN: User data is returned
  expect(response.body).toMatchObject({
    code: 'SUCCESS',
    data: {
      id: user.id,
      email: 'existing@example.com',
      name: 'Existing User',
      status: 'active',
    },
  });
});

Update Resource (PATCH)

it('should update user profile', async () => {
  // GIVEN: Existing user
  const user = await userRepository.save({
    email: 'user@example.com',
    name: 'Original Name',
  });

  // WHEN: Updating user name
  const response = await request(httpServer)
    .patch(`/api/v1/users/${user.id}`)
    .send({ name: 'Updated Name' })
    .expect(200);

  // THEN: Updated data returned
  expect(response.body.data.name).toBe('Updated Name');

  // THEN: Database reflects change
  const updatedUser = await userRepository.findOne({ where: { id: user.id } });
  expect(updatedUser.name).toBe('Updated Name');
});

Delete Resource (DELETE)

it('should delete user and return 204', async () => {
  // GIVEN: Existing user
  const user = await userRepository.save({
    email: 'delete-me@example.com',
    name: 'Delete Me',
  });

  // WHEN: Deleting user
  await request(httpServer)
    .delete(`/api/v1/users/${user.id}`)
    .expect(204);

  // THEN: User no longer exists
  const deletedUser = await userRepository.findOne({ where: { id: user.id } });
  expect(deletedUser).toBeNull();
});

List with Pagination

it('should return paginated list of users', async () => {
  // GIVEN: Multiple users exist
  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' },
    { email: 'user4@test.com', name: 'User 4', status: 'active' },
    { email: 'user5@test.com', name: 'User 5', status: 'active' },
  ]);

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

  // THEN: Paginated results returned
  expect(response.body.data).toHaveLength(2);
  expect(response.body.meta).toMatchObject({
    page: 1,
    limit: 2,
    total: 4,
    totalPages: 2,
  });
});

Kafka Examples

Publish and Consume Event

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

  // GIVEN: Order event
  const orderEvent = {
    orderId: `order-${Date.now()}`,
    productId: product.id,
    quantity: 5,
    timestamp: new Date().toISOString(),
  };

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

  // THEN: Inventory is updated (using smart polling)
  const updatedProduct = await waitFor(
    () => productRepository.findOne({
      where: { id: product.id, stock: 95 },
    }),
    20000
  );

  expect(updatedProduct).toBeDefined();
  expect(updatedProduct.stock).toBe(95);
});

Event Produces Output to Topic

it('should process input event and produce output event', async () => {
  // GIVEN: Input event
  const inputEvent = {
    id: `event-${Date.now()}`,
    type: 'USER_CREATED',
    payload: { userId: 'user-123', email: 'test@example.com' },
  };

  // WHEN: Publishing to input topic
  await kafkaHelper.publishEvent('domain-events', inputEvent, inputEvent.id);

  // THEN: Output event received on notification topic
  const outputMessages = await kafkaHelper.waitForMessages(
    'notifications',
    1,
    20000
  );

  expect(outputMessages[0].value).toMatchObject({
    eventId: inputEvent.id,
    notificationType: 'WELCOME_EMAIL',
    recipient: 'test@example.com',
  });
});

Batch Processing

it('should process batch of 10 events within timeout', async () => {
  // GIVEN: Batch of events
  const events = Array.from({ length: 10 }, (_, i) => ({
    event: { id: `batch-event-${i}`, data: `data-${i}` },
    key: `batch-event-${i}`,
  }));

  // WHEN: Publishing batch
  await kafkaHelper.publishBatch('input-topic', events);

  // THEN: All events processed
  const outputMessages = await kafkaHelper.waitForMessages(
    'output-topic',
    10,
    30000
  );

  expect(outputMessages.length).toBe(10);
  expect(outputMessages.every(m => m.value.processed === true)).toBe(true);
});

PostgreSQL Examples

Transaction with Rollback

it('should rollback order creation on payment failure', async () => {
  // GIVEN: User with balance
  const user = await userRepository.save({
    email: 'buyer@example.com',
    balance: 100,
  });

  // GIVEN: Product costs more than balance
  const product = await productRepository.save({
    name: 'Expensive Item',
    price: 150,
  });

  // WHEN: Attempting to create order
  const response = await request(httpServer)
    .post('/api/v1/orders')
    .send({
      userId: user.id,
      items: [{ productId: product.id, quantity: 1 }],
    })
    .expect(400);

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

  // THEN: No order created
  const orders = await orderRepository.find({ where: { userId: user.id } });
  expect(orders).toHaveLength(0);

  // THEN: User balance unchanged
  const refreshedUser = await userRepository.findOne({ where: { id: user.id } });
  expect(refreshedUser.balance).toBe(100);
});

Complex Query with Joins

it('should return orders with user and product details', async () => {
  // GIVEN: User, product, and order
  const user = await userRepository.save({ email: 'buyer@test.com', name: 'Buyer' });
  const product = await productRepository.save({ name: 'Widget', price: 29.99 });
  const order = await orderRepository.save({
    userId: user.id,
    status: 'completed',
    items: [{ productId: product.id, quantity: 2, price: 29.99 }],
  });

  // WHEN: Fetching order with relations
  const response = await request(httpServer)
    .get(`/api/v1/orders/${order.id}`)
    .query({ include: 'user,items.product' })
    .expect(200);

  // THEN: Full details returned
  expect(response.body.data).toMatchObject({
    id: order.id,
    status: 'completed',
    user: { id: user.id, email: 'buyer@test.com' },
    items: [{
      productId: product.id,
      quantity: 2,
      product: { name: 'Widget', price: 29.99 },
    }],
    total: 59.98,
  });
});

MongoDB Examples

Nested Document Update

it('should update nested document field', async () => {
  // GIVEN: Product with specifications
  const product = await productModel.create({
    name: 'Laptop',
    specs: {
      ram: '8GB',
      storage: '256GB',
      display: '13 inch',
    },
  });

  // WHEN: Updating nested field
  const response = await request(httpServer)
    .patch(`/api/v1/products/${product._id}`)
    .send({ 'specs.ram': '16GB' })
    .expect(200);

  // THEN: Only specified field updated
  const updated = await productModel.findById(product._id).lean();
  expect(updated.specs).toMatchObject({
    ram: '16GB',
    storage: '256GB',
    display: '13 inch',
  });
});

Text Search

it('should search products by text', async () => {
  // GIVEN: Products with searchable content
  await productModel.insertMany([
    { name: 'Apple MacBook Pro 16', category: 'laptop' },
    { name: 'Samsung Galaxy S24', category: 'phone' },
    { name: 'Apple iPhone 15', category: 'phone' },
    { name: 'Dell XPS 15', category: 'laptop' },
  ]);

  // WHEN: Searching for "Apple"
  const response = await request(httpServer)
    .get('/api/v1/products/search')
    .query({ q: 'Apple' })
    .expect(200);

  // THEN: Only Apple products returned
  expect(response.body.data).toHaveLength(2);
  expect(response.body.data.every(p => p.name.includes('Apple'))).toBe(true);
});

Aggregation

it('should return order statistics by status', async () => {
  // GIVEN: Orders with different statuses
  await orderModel.insertMany([
    { status: 'pending', total: 100 },
    { status: 'pending', total: 150 },
    { status: 'completed', total: 200 },
    { status: 'completed', total: 250 },
    { status: 'completed', total: 300 },
    { status: 'cancelled', total: 50 },
  ]);

  // WHEN: Fetching order statistics
  const response = await request(httpServer)
    .get('/api/v1/orders/statistics')
    .expect(200);

  // THEN: Aggregated statistics returned
  expect(response.body.data).toMatchObject({
    pending: { count: 2, total: 250 },
    completed: { count: 3, total: 750 },
    cancelled: { count: 1, total: 50 },
  });
});

Redis Examples

Cache Hit and Miss

it('should cache user profile on first fetch', async () => {
  // GIVEN: User in database (not in cache)
  const user = await userRepository.save({
    email: 'cache-test@example.com',
    name: 'Cache Test User',
  });

  // WHEN: Fetching user first time
  const response1 = await request(httpServer)
    .get(`/api/v1/users/${user.id}`)
    .expect(200);

  // THEN: User cached
  await redisHelper.assertKeyExists(`user:profile:${user.id}`);

  // WHEN: Fetching user second time (from cache)
  const response2 = await request(httpServer)
    .get(`/api/v1/users/${user.id}`)
    .expect(200);

  // THEN: Same data returned
  expect(response2.body.data).toEqual(response1.body.data);
});

it('should invalidate cache on update', async () => {
  // GIVEN: Cached user profile
  const user = await userRepository.save({ email: 'test@example.com', name: 'Original' });
  await redisHelper.setTestData(`user:profile:${user.id}`, { ...user });

  // WHEN: Updating user
  await request(httpServer)
    .patch(`/api/v1/users/${user.id}`)
    .send({ name: 'Updated' })
    .expect(200);

  // THEN: Cache invalidated
  await redisHelper.assertKeyNotExists(`user:profile:${user.id}`);
});

Rate Limiting

it('should block requests exceeding rate limit', async () => {
  // GIVEN: Near rate limit (9 of 10 requests used)
  await redis.set('ratelimit:api:127.0.0.1', '9');
  await redis.expire('ratelimit:api:127.0.0.1', 60);

  // WHEN: Making 10th request (should succeed)
  await request(httpServer).get('/api/v1/resource').expect(200);

  // WHEN: Making 11th request (should be blocked)
  const response = await request(httpServer)
    .get('/api/v1/resource')
    .expect(429);

  // THEN: Rate limit error
  expect(response.body.code).toBe('RATE_LIMIT_EXCEEDED');
  expect(response.headers['retry-after']).toBeDefined();
});

Authentication Examples

Login Success

it('should return JWT token on successful login', async () => {
  // GIVEN: Registered user
  await userRepository.save({
    email: 'user@example.com',
    password: await bcrypt.hash('password123', 10),
    status: 'active',
  });

  // WHEN: Logging in
  const response = await request(httpServer)
    .post('/api/v1/auth/login')
    .send({ email: 'user@example.com', password: 'password123' })
    .expect(200);

  // THEN: Token returned
  expect(response.body.data).toMatchObject({
    accessToken: expect.any(String),
    refreshToken: expect.any(String),
    expiresIn: 3600,
  });

  // THEN: Token is valid JWT
  const decoded = jwt.verify(response.body.data.accessToken, process.env.JWT_SECRET);
  expect(decoded).toMatchObject({
    userId: expect.any(String),
    email: 'user@example.com',
  });
});

Protected Endpoint

it('should return user profile with valid token', async () => {
  // GIVEN: Authenticated user
  const user = await userRepository.save({ email: 'me@example.com', name: 'Me' });
  const token = authHelper.generateToken({ userId: user.id, email: user.email });

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

  // THEN: User profile returned
  expect(response.body.data.email).toBe('me@example.com');
});

it('should return 401 for expired token', async () => {
  // GIVEN: Expired token
  const expiredToken = authHelper.generateToken({ userId: 'user-1' }, { expiresIn: '-1h' });

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

  // THEN: Unauthorized error
  expect(response.body.code).toBe('TOKEN_EXPIRED');
});

Error Handling Examples

Validation Error

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

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

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

External Service Failure

it('should handle payment gateway timeout gracefully', async () => {
  // GIVEN: Payment gateway times out
  mockPaymentGateway.onPost('/charge').timeout();

  // WHEN: Processing payment
  const response = await request(httpServer)
    .post('/api/v1/payments')
    .send({ orderId: 'order-123', amount: 99.99 })
    .expect(503);

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

Edge Case Examples

Empty Result Set

it('should return empty array when no users match filter', async () => {
  // GIVEN: Users exist but none match filter
  await userRepository.save([
    { email: 'user1@test.com', status: 'inactive' },
    { email: 'user2@test.com', status: 'inactive' },
  ]);

  // WHEN: Filtering for active users
  const response = await request(httpServer)
    .get('/api/v1/users')
    .query({ status: 'active' })
    .expect(200);

  // THEN: Empty array with zero total
  expect(response.body.data).toEqual([]);
  expect(response.body.meta.total).toBe(0);
});

Concurrent Updates

it('should handle concurrent balance updates correctly', async () => {
  // GIVEN: User with balance
  const user = await userRepository.save({ email: 'test@example.com', balance: 100 });

  // WHEN: Two concurrent deductions
  const [response1, response2] = await Promise.all([
    request(httpServer).post(`/api/v1/users/${user.id}/deduct`).send({ amount: 60 }),
    request(httpServer).post(`/api/v1/users/${user.id}/deduct`).send({ amount: 60 }),
  ]);

  // THEN: One succeeds, one fails (optimistic locking)
  const statuses = [response1.status, response2.status].sort();
  expect(statuses).toEqual([200, 409]); // One success, one conflict

  // THEN: Balance reflects only one deduction
  const refreshedUser = await userRepository.findOne({ where: { id: user.id } });
  expect(refreshedUser.balance).toBe(40);
});

Large Payload

it('should handle batch import of 1000 records', async () => {
  // GIVEN: Large batch of records
  const records = Array.from({ length: 1000 }, (_, i) => ({
    email: `user${i}@test.com`,
    name: `User ${i}`,
  }));

  // WHEN: Importing batch
  const response = await request(httpServer)
    .post('/api/v1/users/import')
    .send({ users: records })
    .timeout(60000)
    .expect(201);

  // THEN: All records imported
  expect(response.body.data.imported).toBe(1000);
  expect(response.body.data.failed).toBe(0);

  // THEN: Records exist in database
  const count = await userRepository.count();
  expect(count).toBe(1000);
}, 90000);

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.