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.

referencesmongodbexamples.md

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

MongoDB E2E Test Examples

Basic CRUD Operations

Create with Validation

it('should create product with category reference', async () => {
  // GIVEN: Existing category
  const category = await categoryModel.create({ name: 'Electronics' });

  // WHEN: Creating product
  const response = await request(httpServer)
    .post('/products')
    .send({
      name: 'Laptop',
      categoryId: category._id.toString(),
      price: 999.99,
    })
    .expect(201);

  // THEN: Product created with reference
  await helper.assertDocumentExists(
    productModel,
    { name: 'Laptop' },
    {
      categoryId: category._id,
      price: 999.99,
    }
  );
});

Read with Population

it('should return order with populated user and items', async () => {
  // GIVEN: User, products, and order
  const user = await userModel.create({ email: 'buyer@test.com', name: 'Buyer' });
  const products = await productModel.insertMany([
    { name: 'Widget', price: 29.99 },
    { name: 'Gadget', price: 49.99 },
  ]);
  const order = await orderModel.create({
    userId: user._id,
    items: [
      { productId: products[0]._id, quantity: 2 },
      { productId: products[1]._id, quantity: 1 },
    ],
    status: 'completed',
  });

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

  // THEN: Order with populated relations
  expect(response.body.data).toMatchObject({
    status: 'completed',
    user: { email: 'buyer@test.com' },
    items: expect.arrayContaining([
      expect.objectContaining({
        quantity: 2,
        product: { name: 'Widget' },
      }),
    ]),
  });
});

Nested Document Updates

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

  // WHEN: Updating nested field
  const response = await request(httpServer)
    .patch(`/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.ram).toBe('16GB');
  expect(updated.specs.storage).toBe('256GB');  // Unchanged
  expect(updated.specs.processor).toBe('Intel i5');  // Unchanged
});

Text Search

it('should search products by text', async () => {
  // GIVEN: Products with searchable content
  await helper.seedDocuments(productModel, [
    { name: 'Apple MacBook Pro', description: 'Powerful laptop' },
    { name: 'Samsung Galaxy', description: 'Android smartphone' },
    { name: 'Apple iPhone', description: 'iOS smartphone' },
  ]);

  // WHEN: Searching for "Apple"
  const response = await request(httpServer)
    .get('/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 Pipeline

it('should return sales statistics by category', async () => {
  // GIVEN: Orders with products in categories
  const electronics = await categoryModel.create({ name: 'Electronics' });
  const clothing = await categoryModel.create({ name: 'Clothing' });

  await productModel.insertMany([
    { name: 'Phone', categoryId: electronics._id, price: 500 },
    { name: 'Laptop', categoryId: electronics._id, price: 1000 },
    { name: 'Shirt', categoryId: clothing._id, price: 50 },
  ]);

  await orderItemModel.insertMany([
    { productId: 'phone-id', quantity: 2, price: 500 },
    { productId: 'laptop-id', quantity: 1, price: 1000 },
    { productId: 'shirt-id', quantity: 5, price: 50 },
  ]);

  // WHEN: Fetching aggregated stats
  const response = await request(httpServer)
    .get('/api/v1/reports/sales-by-category')
    .expect(200);

  // THEN: Aggregated statistics
  expect(response.body.data).toMatchObject({
    Electronics: { totalSales: 2000, itemCount: 3 },
    Clothing: { totalSales: 250, itemCount: 5 },
  });
});

Pagination with Filtering

it('should return filtered and paginated users', async () => {
  // GIVEN: Users with different statuses
  await helper.seedDocuments(userModel, [
    { email: 'active1@test.com', name: 'Active 1', status: 'active' },
    { email: 'active2@test.com', name: 'Active 2', status: 'active' },
    { email: 'active3@test.com', name: 'Active 3', status: 'active' },
    { email: 'inactive1@test.com', name: 'Inactive 1', status: 'inactive' },
    { email: 'pending1@test.com', name: 'Pending 1', status: 'pending' },
  ]);

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

  // THEN: Correct pagination
  expect(response.body.data).toHaveLength(2);
  expect(response.body.data[0].name).toBe('Active 1');
  expect(response.body.data[1].name).toBe('Active 2');
  expect(response.body.meta).toMatchObject({
    page: 1,
    limit: 2,
    total: 3,
    totalPages: 2,
  });
});

Unique Constraint Validation

it('should return 409 for duplicate email', async () => {
  // GIVEN: Existing user
  await userModel.create({ email: 'existing@example.com', name: 'Existing' });

  // WHEN: Creating user with same email
  const response = await request(httpServer)
    .post('/api/v1/users')
    .send({ email: 'existing@example.com', name: 'New User' })
    .expect(409);

  // THEN: Conflict error
  expect(response.body).toMatchObject({
    code: 'EMAIL_ALREADY_EXISTS',
    message: expect.stringContaining('email'),
  });
});

Soft Delete

it('should soft delete user and exclude from queries', async () => {
  // GIVEN: Active user
  const user = await userModel.create({
    email: 'tobedeleted@test.com',
    name: 'To Be Deleted',
    deletedAt: null,
  });

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

  // THEN: User has deletedAt set
  const deletedUser = await userModel.findById(user._id).lean();
  expect(deletedUser.deletedAt).not.toBeNull();

  // THEN: User not returned in list
  const response = await request(httpServer)
    .get('/api/v1/users')
    .expect(200);
  expect(response.body.data).toHaveLength(0);
});

Embedded Array Operations

it('should add item to embedded array', async () => {
  // GIVEN: Order with items
  const order = await orderModel.create({
    userId: 'user-123',
    items: [{ productId: 'prod-1', quantity: 1 }],
    status: 'pending',
  });

  // WHEN: Adding new item
  const response = await request(httpServer)
    .post(`/api/v1/orders/${order._id}/items`)
    .send({ productId: 'prod-2', quantity: 3 })
    .expect(200);

  // THEN: Item added to array
  const updated = await orderModel.findById(order._id).lean();
  expect(updated.items).toHaveLength(2);
  expect(updated.items[1]).toMatchObject({
    productId: 'prod-2',
    quantity: 3,
  });
});

it('should update item in embedded array', async () => {
  // GIVEN: Order with items
  const order = await orderModel.create({
    userId: 'user-123',
    items: [
      { productId: 'prod-1', quantity: 1 },
      { productId: 'prod-2', quantity: 2 },
    ],
  });

  // WHEN: Updating item quantity
  const response = await request(httpServer)
    .patch(`/api/v1/orders/${order._id}/items/prod-1`)
    .send({ quantity: 5 })
    .expect(200);

  // THEN: Only specified item updated
  const updated = await orderModel.findById(order._id).lean();
  expect(updated.items[0].quantity).toBe(5);
  expect(updated.items[1].quantity).toBe(2);  // Unchanged
});

Transaction Tests

describe('MongoDB Transactions', () => {
  let session: ClientSession;

  beforeEach(async () => {
    session = await connection.startSession();
  });

  afterEach(async () => {
    session.endSession();
  });

  it('should transfer balance between users atomically', async () => {
    // GIVEN: Two users with balances
    const sender = await userModel.create({ email: 'sender@test.com', balance: 100 });
    const receiver = await userModel.create({ email: 'receiver@test.com', balance: 50 });

    // WHEN: Transferring balance
    const response = await request(httpServer)
      .post('/api/v1/transfers')
      .send({
        fromUserId: sender._id.toString(),
        toUserId: receiver._id.toString(),
        amount: 30,
      })
      .expect(200);

    // THEN: Balances updated atomically
    const updatedSender = await userModel.findById(sender._id).lean();
    const updatedReceiver = await userModel.findById(receiver._id).lean();
    expect(updatedSender.balance).toBe(70);
    expect(updatedReceiver.balance).toBe(80);
  });

  it('should rollback on insufficient balance', async () => {
    // GIVEN: User with low balance
    const sender = await userModel.create({ email: 'sender@test.com', balance: 20 });
    const receiver = await userModel.create({ email: 'receiver@test.com', balance: 50 });

    // WHEN: Attempting transfer exceeding balance
    const response = await request(httpServer)
      .post('/api/v1/transfers')
      .send({
        fromUserId: sender._id.toString(),
        toUserId: receiver._id.toString(),
        amount: 100,
      })
      .expect(400);

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

    // THEN: Balances unchanged
    const updatedSender = await userModel.findById(sender._id).lean();
    const updatedReceiver = await userModel.findById(receiver._id).lean();
    expect(updatedSender.balance).toBe(20);
    expect(updatedReceiver.balance).toBe(50);
  });
});

Common Issues Handling

ObjectId Comparison

it('should compare ObjectIds correctly', async () => {
  const category = await categoryModel.create({ name: 'Test' });
  const product = await productModel.create({
    name: 'Product',
    categoryId: category._id,
  });

  const found = await productModel.findById(product._id).lean();

  // CORRECT: Use toString()
  expect(found.categoryId.toString()).toBe(category._id.toString());
});

Virtual Fields with Lean

it('should include virtuals in lean query', async () => {
  const user = await userModel.create({
    firstName: 'John',
    lastName: 'Doe',
  });

  // Use lean with virtuals option
  const found = await userModel.findById(user._id).lean({ virtuals: true });

  expect(found.fullName).toBe('John Doe');
});

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.