.do
Test

Test

Test your Business-as-Code with comprehensive unit, integration, and end-to-end testing strategies

Test your Business-as-Code with comprehensive unit, integration, and end-to-end testing strategies to ensure reliability and correctness.

Overview

The Test phase ensures your Business-as-Code works correctly, reliably, and at scale. Using modern testing frameworks and patterns, you validate business logic, workflows, integrations, and user experiences before deployment.

This phase emphasizes:

  • Unit Testing: Test individual components in isolation
  • Integration Testing: Test component interactions
  • End-to-End Testing: Test complete user workflows
  • Contract Testing: Verify API contracts
  • Performance Testing: Validate scalability and speed

Testing Primitives

Unit Tests

Test individual business logic components:

import { describe, it, expect, beforeEach } from 'vitest'
import { db } from '@dotdo/platform'

describe('Order Processing', () => {
  beforeEach(async () => {
    await db.reset() // Reset test database
  })

  it('calculates order total correctly', async () => {
    const order = await db.create($.Order, {
      items: [
        { product: 'Widget', price: 10, quantity: 2 },
        { product: 'Gadget', price: 15, quantity: 1 },
      ],
    })

    const total = calculateOrderTotal(order)
    expect(total).toBe(35) // (10 * 2) + (15 * 1)
  })

  it('applies discount correctly', async () => {
    const order = await db.create($.Order, {
      items: [{ product: 'Widget', price: 100, quantity: 1 }],
      discount: { type: 'percentage', value: 20 },
    })

    const total = calculateOrderTotal(order)
    expect(total).toBe(80) // 100 - 20%
  })

  it('handles tax calculation', async () => {
    const order = await db.create($.Order, {
      items: [{ product: 'Widget', price: 100, quantity: 1 }],
      taxRate: 0.08,
    })

    const total = calculateOrderTotal(order)
    expect(total).toBe(108) // 100 + 8% tax
  })
})

Integration Tests

Test component interactions and workflows:

describe('Checkout Workflow', () => {
  it('completes full checkout process', async () => {
    // 1. Create user
    const user = await db.create($.User, {
      email: '[email protected]',
      name: 'Test User',
    })

    // 2. Add items to cart
    const cart = await db.create($.Cart, {
      userId: user.id,
      items: [{ productId: 'prod_123', quantity: 2 }],
    })

    // 3. Process checkout
    const order = await checkout({
      userId: user.id,
      cartId: cart.id,
      paymentMethod: 'test_card',
    })

    // 4. Verify order created
    expect(order.status).toBe('pending_payment')

    // 5. Verify inventory reserved
    const product = await db.get($.Product, 'prod_123')
    expect(product.reserved).toBe(2)

    // 6. Verify email sent
    const emails = await getTestEmails()
    expect(emails).toHaveLength(1)
    expect(emails[0].to).toBe(user.email)
    expect(emails[0].template).toBe('order-confirmation')
  })

  it('handles payment failure gracefully', async () => {
    const user = await createTestUser()
    const cart = await createTestCart(user.id)

    // Use failing payment method
    const result = await checkout({
      userId: user.id,
      cartId: cart.id,
      paymentMethod: 'fail_card',
    })

    // Verify order not created
    expect(result.success).toBe(false)
    expect(result.error).toBe('payment_failed')

    // Verify inventory not reserved
    const product = await db.get($.Product, cart.items[0].productId)
    expect(product.reserved).toBe(0)

    // Verify failure email sent
    const emails = await getTestEmails()
    expect(emails[0].template).toBe('payment-failed')
  })
})

End-to-End Tests

Test complete user workflows:

import { test, expect } from '@playwright/test'

test.describe('User Signup Flow', () => {
  test('new user can signup and activate account', async ({ page }) => {
    // 1. Visit signup page
    await page.goto('/signup')

    // 2. Fill signup form
    await page.fill('[name="email"]', '[email protected]')
    await page.fill('[name="password"]', 'SecurePassword123!')
    await page.fill('[name="name"]', 'New User')

    // 3. Submit form
    await page.click('button[type="submit"]')

    // 4. Verify redirect to onboarding
    await expect(page).toHaveURL('/onboarding')

    // 5. Verify welcome message
    await expect(page.locator('h1')).toContainText('Welcome, New User')

    // 6. Verify account created in database
    const user = await db.findOne($.User, {
      where: { email: '[email protected]' },
    })
    expect(user).toBeDefined()
    expect(user.status).toBe('active')

    // 7. Verify welcome email sent
    const email = await getLatestEmail('[email protected]')
    expect(email.subject).toContain('Welcome')
  })

  test('user can complete onboarding', async ({ page }) => {
    const user = await createTestUser({ onboarded: false })
    await loginAs(page, user)

    // Navigate through onboarding steps
    await page.goto('/onboarding')

    // Step 1: Profile
    await page.fill('[name="company"]', 'Acme Corp')
    await page.click('button:has-text("Next")')

    // Step 2: Preferences
    await page.check('[name="emailNotifications"]')
    await page.click('button:has-text("Next")')

    // Step 3: Complete
    await page.click('button:has-text("Get Started")')

    // Verify redirect to dashboard
    await expect(page).toHaveURL('/dashboard')

    // Verify onboarding completed
    const updated = await db.get($.User, user.id)
    expect(updated.onboardingCompleted).toBe(true)
  })
})

Contract Testing

Verify API contracts between services:

import { pactWith } from 'jest-pact'

pactWith({ consumer: 'Frontend', provider: 'API' }, (interaction) => {
  interaction('get user profile', () => ({
    state: 'user exists',
    uponReceiving: 'a request for user profile',
    withRequest: {
      method: 'GET',
      path: '/api/users/123',
      headers: {
        Authorization: 'Bearer token',
      },
    },
    willRespondWith: {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
      },
      body: {
        id: '123',
        email: '[email protected]',
        name: 'Test User',
        plan: 'pro',
      },
    },
  }))

  test('fetches user profile', async () => {
    const response = await fetch('/api/users/123', {
      headers: { Authorization: 'Bearer token' },
    })

    const user = await response.json()
    expect(user.email).toBe('[email protected]')
  })
})

Performance Testing

Validate system performance:

import { test } from 'vitest'
import autocannon from 'autocannon'

describe('API Performance', () => {
  test('handles 1000 requests/second', async () => {
    const result = await autocannon({
      url: 'http://localhost:3000/api/products',
      connections: 100,
      duration: 30, // 30 seconds
      pipelining: 10,
    })

    // Verify performance metrics
    expect(result.requests.average).toBeGreaterThan(1000)
    expect(result.latency.p95).toBeLessThan(100) // 95th percentile < 100ms
    expect(result.errors).toBe(0)
  })

  test('database query performance', async () => {
    const iterations = 1000
    const start = Date.now()

    for (let i = 0; i < iterations; i++) {
      await db.list($.Product, { limit: 10 })
    }

    const duration = Date.now() - start
    const avgLatency = duration / iterations

    expect(avgLatency).toBeLessThan(10) // Average < 10ms per query
  })
})

Testing Patterns

Test Data Builders

Create reusable test data:

// Test data builders
export const TestBuilders = {
  user: (overrides = {}) => ({
    email: `test-${Date.now()}@example.com`,
    name: 'Test User',
    status: 'active',
    ...overrides,
  }),

  order: (overrides = {}) => ({
    userId: 'user_123',
    items: [{ productId: 'prod_123', quantity: 1, price: 10 }],
    status: 'pending',
    total: 10,
    ...overrides,
  }),

  product: (overrides = {}) => ({
    name: 'Test Product',
    price: 99,
    category: 'Test',
    inventory: 100,
    ...overrides,
  }),
}

// Usage
const user = await db.create($.User, TestBuilders.user({ plan: 'pro' }))
const order = await db.create($.Order, TestBuilders.order({ userId: user.id }))

Test Fixtures

Manage complex test setups:

export async function setupEcommerceFixture() {
  // Create products
  const products = await Promise.all([
    db.create($.Product, { name: 'Widget', price: 10, inventory: 100 }),
    db.create($.Product, { name: 'Gadget', price: 20, inventory: 50 }),
  ])

  // Create users
  const users = await Promise.all([
    db.create($.User, { email: '[email protected]', plan: 'free' }),
    db.create($.User, { email: '[email protected]', plan: 'pro' }),
  ])

  // Create orders
  const orders = await Promise.all([
    db.create($.Order, {
      userId: users[0].id,
      items: [{ productId: products[0].id, quantity: 2 }],
    }),
  ])

  return { products, users, orders }
}

// Usage
describe('Ecommerce Features', () => {
  let fixture

  beforeEach(async () => {
    fixture = await setupEcommerceFixture()
  })

  it('processes refund', async () => {
    const refund = await processRefund(fixture.orders[0].id)
    expect(refund.status).toBe('completed')
  })
})

Best Practices

Do's

  1. Test behavior, not implementation - Test what code does, not how
  2. Write tests first - TDD ensures testable code
  3. Keep tests fast - Fast tests run frequently
  4. Test edge cases - Don't just test happy paths
  5. Use factories - Reusable test data builders
  6. Mock external services - Isolate system under test
  7. Test at appropriate level - Unit for logic, E2E for workflows

Don'ts

  1. Don't test implementation details - Tests become brittle
  2. Don't skip tests - Untested code breaks
  3. Don't test third-party code - Trust libraries
  4. Don't use production data - Use test fixtures
  5. Don't ignore flaky tests - Fix or remove them
  6. Don't over-mock - Integration tests need real interactions

CLI Tools

# Run all tests
do test

# Run specific test file
do test src/orders.test.ts

# Run tests in watch mode
do test:watch

# Run integration tests
do test:integration

# Run E2E tests
do test:e2e

# Generate coverage report
do test:coverage

# Run performance tests
do test:performance

Next Steps


Testing Tip: The best tests are fast, focused, and fail for one reason. Write tests that document behavior.