All skills
asyrafhussin avatar

/testing-best-practices

@575c801

Unit testing, integration testing, and test-driven development principles. Use when writing tests, reviewing test code, improving test coverage, or setting up testing strategy. Triggers on "write tests", "review tests", "testing best practices", or "TDD".

Use this Skill: https://skilld.dev/gh/asyrafhussin/agent-skills/testing-best-practices

This session only. Nothing lands on disk.

rulesiso-test-doubles.md

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

Test Doubles for Isolation

Impact: CRITICAL (unit test speed and reliability)

Use test doubles (mocks, stubs, spies, fakes) to isolate the unit under test from its dependencies. Types include mocks (record interactions), stubs (return predetermined responses), spies (wrap real objects), fakes (simplified implementations), and dummies (unused placeholders).

Incorrect

// ❌ Bad: Tests tightly coupled to real implementations
describe('OrderService', () => {
  test('sends order confirmation email', async () => {
    // Using real email service — slow, unreliable, sends real emails!
    const emailService = new EmailService();
    const orderService = new OrderService(emailService);

    await orderService.placeOrder({
      customerId: 'cust-123',
      items: [{ id: 1, quantity: 2 }]
    });

    // No way to verify email was sent without checking real inbox
  });

  test('saves order to database', async () => {
    // Using real database — slow, stateful, needs setup
    const database = new PostgresDatabase();
    const orderService = new OrderService(new EmailService(), database);

    const order = await orderService.placeOrder({
      customerId: 'cust-123',
      items: [{ id: 1, quantity: 2 }]
    });

    const saved = await database.query('SELECT * FROM orders WHERE id = $1', [order.id]);
    expect(saved.rows[0]).toBeDefined();
  });

  test('handles payment processing', async () => {
    // Using real payment gateway — charges real money!
    const paymentGateway = new StripeGateway(process.env.STRIPE_KEY);
    const orderService = new OrderService(
      new EmailService(),
      new PostgresDatabase(),
      paymentGateway
    );

    await orderService.placeOrder({ /* ... */ });
  });
});

Problems:

  • Real email service sends actual emails during tests
  • Real database makes tests slow and stateful
  • Real payment gateway charges actual money
  • No way to simulate error scenarios reliably
  • Tests are flaky due to network and external service dependencies

Correct

// ✅ Good: Using test doubles for isolation
describe('OrderService', () => {
  let orderService: OrderService;
  let emailService: jest.Mocked<EmailService>;
  let orderRepository: jest.Mocked<OrderRepository>;
  let paymentGateway: jest.Mocked<PaymentGateway>;
  let inventoryService: jest.Mocked<InventoryService>;

  beforeEach(() => {
    emailService = {
      send: jest.fn().mockResolvedValue({ messageId: 'msg-123' })
    };

    orderRepository = {
      save: jest.fn().mockImplementation(order =>
        Promise.resolve({ ...order, id: 'order-123' })
      ),
      findById: jest.fn()
    };

    paymentGateway = {
      charge: jest.fn().mockResolvedValue({
        transactionId: 'txn-456',
        status: 'success'
      })
    };

    inventoryService = {
      reserve: jest.fn().mockResolvedValue(true),
      release: jest.fn().mockResolvedValue(true)
    };

    orderService = new OrderService(
      emailService,
      orderRepository,
      paymentGateway,
      inventoryService
    );
  });

  describe('placeOrder', () => {
    const validOrder = {
      customerId: 'cust-123',
      customerEmail: 'customer@example.com',
      items: [{ productId: 'prod-1', quantity: 2, price: 29.99 }]
    };

    test('saves order to repository', async () => {
      await orderService.placeOrder(validOrder);

      expect(orderRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          customerId: 'cust-123',
          items: validOrder.items
        })
      );
    });

    test('sends confirmation email after successful order', async () => {
      await orderService.placeOrder(validOrder);

      expect(emailService.send).toHaveBeenCalledWith({
        to: 'customer@example.com',
        template: 'order-confirmation',
        data: expect.objectContaining({
          orderId: 'order-123'
        })
      });
    });

    test('charges payment gateway with correct amount', async () => {
      await orderService.placeOrder(validOrder);

      expect(paymentGateway.charge).toHaveBeenCalledWith({
        customerId: 'cust-123',
        amount: 59.98, // 2 * 29.99
        currency: 'USD'
      });
    });

    test('reserves inventory before processing payment', async () => {
      await orderService.placeOrder(validOrder);

      const reserveCall = inventoryService.reserve.mock.invocationCallOrder[0];
      const chargeCall = paymentGateway.charge.mock.invocationCallOrder[0];
      expect(reserveCall).toBeLessThan(chargeCall);
    });

    test('releases inventory when payment fails', async () => {
      paymentGateway.charge.mockRejectedValue(new PaymentError('Card declined'));

      await expect(orderService.placeOrder(validOrder)).rejects.toThrow('Card declined');

      expect(inventoryService.release).toHaveBeenCalled();
    });

    test('does not send email when payment fails', async () => {
      paymentGateway.charge.mockRejectedValue(new PaymentError('Card declined'));

      await expect(orderService.placeOrder(validOrder)).rejects.toThrow();

      expect(emailService.send).not.toHaveBeenCalled();
    });
  });
});

// Using a Fake for more complex scenarios
class FakeOrderRepository implements OrderRepository {
  private orders: Map<string, Order> = new Map();
  private idCounter = 0;

  async save(order: Omit<Order, 'id'>): Promise<Order> {
    const id = `order-${++this.idCounter}`;
    const saved = { ...order, id };
    this.orders.set(id, saved);
    return saved;
  }

  async findById(id: string): Promise<Order | null> {
    return this.orders.get(id) || null;
  }

  getAll(): Order[] {
    return Array.from(this.orders.values());
  }
}

Benefits:

  • No network calls, database queries, or I/O — tests run fast
  • Only the unit's logic is tested, not its dependencies
  • Any scenario including errors and edge cases can be simulated
  • Assertions verify exactly how dependencies were called
  • No real emails sent or real payments charged
  • No flakiness from external services

Reference: Martin Fowler — Mocks Aren't Stubs

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive guidelines and best practices for unit testing, integration testing, and Test-Driven Development (TDD). It consists entirely of documentation and illustrative code examples using industry-standard tools like Jest and Vitest. No security risks or malicious patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    29 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 575c801. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated 7 months ago
metadata
{
  "author": "agent-skills",
  "version": "2.0.0"
}

README badge

README badge for asyrafhussin/agent-skills/testing-best-practices