Skip to content

Advanced Testing

This guide covers advanced testing strategies, patterns, and tools for building robust STX applications with comprehensive test coverage.

Testing Architecture

Test Organization

tests/
├── unit/
│   ├── components/
│   ├── utils/
│   └── services/
├── integration/
│   ├── api/
│   ├── workflows/
│   └── features/
├── e2e/
│   ├── specs/
│   ├── fixtures/
│   └── support/
├── performance/
├── accessibility/
└── visual/

Test Configuration

typescript
// test.config.ts
export default {
  testDir: './tests',
  timeout: 30000,
  retries: 2,
  
  projects: [
    {
      name: 'unit',
      testMatch: 'tests/unit/**/*.test.ts',
      use: { 
        environment: 'happy-dom',
        setupFiles: ['./tests/setup/unit.ts']
      }
    },
    {
      name: 'integration',
      testMatch: 'tests/integration/**/*.test.ts',
      use: {
        environment: 'node',
        setupFiles: ['./tests/setup/integration.ts']
      }
    },
    {
      name: 'e2e',
      testMatch: 'tests/e2e/**/*.test.ts',
      use: {
        browser: 'chromium',
        setupFiles: ['./tests/setup/e2e.ts']
      }
    }
  ]
}

Component Testing Patterns

Testing Hooks

typescript
// tests/utils/component-testing.ts
import { mount } from '@stx/testing'

export function createComponentTester<T>(component: T) {
  return {
    mount: (props = {}, options = {}) => {
      return mount(component, {
        props,
        global: {
          stubs: ['router-link', 'router-view'],
          mocks: {
            $auth: { user: null, isAuthenticated: false },
            $router: { push: vi.fn() }
          }
        },
        ...options
      })
    },
    
    mountWithAuth: (user: User, props = {}) => {
      return mount(component, {
        props,
        global: {
          mocks: {
            $auth: { user, isAuthenticated: true }
          }
        }
      })
    },
    
    mountWithStore: (storeState: any, props = {}) => {
      const store = createTestStore(storeState)
      return mount(component, {
        props,
        global: {
          plugins: [store]
        }
      })
    }
  }
}

Async Component Testing

typescript
import { test, expect } from 'bun:test'
import { mount, flushPromises } from '@stx/testing'
import AsyncDataLoader from './AsyncDataLoader.stx'

test('handles async data loading', async () => {
  const mockApi = {
    fetchData: vi.fn().mockResolvedValue({ data: 'test data' })
  }
  
  const wrapper = mount(AsyncDataLoader, {
    global: {
      provide: { api: mockApi }
    }
  })
  
  // Initially shows loading state
  expect(wrapper.find('.loading').exists()).toBe(true)
  
  // Wait for async operations to complete
  await flushPromises()
  
  // Shows loaded data
  expect(wrapper.find('.loading').exists()).toBe(false)
  expect(wrapper.text()).toContain('test data')
})

Error Boundary Testing

typescript
test('error boundary catches component errors', async () => {
  const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
  
  const FailingComponent = {
    mounted() {
      throw new Error('Component error')
    },
    template: '<div>Should not render</div>'
  }
  
  const wrapper = mount(ErrorBoundary, {
    slots: {
      default: FailingComponent
    }
  })
  
  await wrapper.vm.$nextTick()
  
  expect(wrapper.find('.error-message').exists()).toBe(true)
  expect(wrapper.find('.error-message').text()).toContain('Something went wrong')
  
  consoleError.mockRestore()
})

State Testing

Store Testing

typescript
// tests/store/user.test.ts
import { test, expect, beforeEach } from 'bun:test'
import { createTestStore } from '@stx/testing'
import { userModule } from '@/store/modules/user'

describe('User Store', () => {
  let store: any
  
  beforeEach(() => {
    store = createTestStore({
      modules: { user: userModule }
    })
  })
  
  test('login action updates state correctly', async () => {
    const credentials = { email: 'test@example.com', password: 'password' }
    
    await store.dispatch('user/login', credentials)
    
    expect(store.state.user.isAuthenticated).toBe(true)
    expect(store.state.user.user.email).toBe('test@example.com')
  })
  
  test('logout action clears user state', async () => {
    // Setup authenticated state
    store.commit('user/setUser', { email: 'test@example.com' })
    store.commit('user/setAuthenticated', true)
    
    await store.dispatch('user/logout')
    
    expect(store.state.user.isAuthenticated).toBe(false)
    expect(store.state.user.user).toBeNull()
  })
})

Reactive State Testing

typescript
test('reactive state updates trigger re-renders', async () => {
  const { state, increment } = useCounter()
  
  const wrapper = mount({
    setup() {
      return { state, increment }
    },
    template: '<div>{{ state.count }}</div>'
  })
  
  expect(wrapper.text()).toBe('0')
  
  increment()
  await wrapper.vm.$nextTick()
  
  expect(wrapper.text()).toBe('1')
})

API Testing

Mock API Responses

typescript
// tests/utils/api-mocks.ts
export function createApiMock() {
  return {
    get: vi.fn(),
    post: vi.fn(),
    put: vi.fn(),
    delete: vi.fn(),
    
    // Helper methods for common scenarios
    mockSuccess: (data: any) => {
      return Promise.resolve({ data, status: 200 })
    },
    
    mockError: (message: string, status = 500) => {
      return Promise.reject({ message, status })
    },
    
    mockPaginated: (items: any[], page = 1, perPage = 10) => {
      const start = (page - 1) * perPage
      const end = start + perPage
      
      return Promise.resolve({
        data: items.slice(start, end),
        pagination: {
          current: page,
          total: Math.ceil(items.length / perPage),
          perPage
        }
      })
    }
  }
}

HTTP Client Testing

typescript
import { test, expect } from 'bun:test'
import { createApiMock } from './utils/api-mocks'
import { UserService } from '@/services/UserService'

test('UserService handles API errors gracefully', async () => {
  const apiMock = createApiMock()
  apiMock.get.mockRejectedValue({ message: 'Network error', status: 500 })
  
  const userService = new UserService(apiMock)
  
  await expect(userService.getUsers()).rejects.toThrow('Network error')
  expect(apiMock.get).toHaveBeenCalledWith('/api/users')
})

Performance Testing

Component Performance

typescript
// tests/performance/component-performance.test.ts
import { test, expect } from 'bun:test'
import { benchmark, mount } from '@stx/testing'
import LargeList from '@/components/LargeList.stx'

test('large list renders within performance budget', async () => {
  const items = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    value: Math.random()
  }))
  
  const result = await benchmark('large list rendering', () => {
    return mount(LargeList, {
      props: { items }
    })
  }, {
    iterations: 10,
    warmup: 2
  })
  
  // Should render 1000 items in under 100ms
  expect(result.average).toBeLessThan(100)
})

Memory Leak Testing

typescript
test('component cleanup prevents memory leaks', async () => {
  const getMemoryUsage = () => (performance as any).memory?.usedJSHeapSize || 0
  
  const initialMemory = getMemoryUsage()
  
  // Create and destroy many components
  for (let i = 0; i < 100; i++) {
    const wrapper = mount(TestComponent)
    wrapper.unmount()
  }
  
  // Force garbage collection if available
  if (global.gc) {
    global.gc()
  }
  
  const finalMemory = getMemoryUsage()
  const memoryGrowth = finalMemory - initialMemory
  
  // Memory growth should be minimal (less than 1MB)
  expect(memoryGrowth).toBeLessThan(1024 * 1024)
})

Accessibility Testing

Automated A11y Testing

typescript
// tests/a11y/accessibility.test.ts
import { test, expect } from 'bun:test'
import { mount } from '@stx/testing'
import { axe, toHaveNoViolations } from 'jest-axe'

expect.extend(toHaveNoViolations)

test('Button component has no accessibility violations', async () => {
  const wrapper = mount(Button, {
    slots: { default: 'Click me' },
    props: { variant: 'primary' }
  })
  
  const results = await axe(wrapper.element)
  expect(results).toHaveNoViolations()
})

test('Form has proper ARIA labels', async () => {
  const wrapper = mount(LoginForm)
  
  const emailInput = wrapper.find('input[type="email"]')
  const passwordInput = wrapper.find('input[type="password"]')
  
  expect(emailInput.attributes('aria-label')).toBe('Email address')
  expect(passwordInput.attributes('aria-label')).toBe('Password')
  expect(emailInput.attributes('aria-required')).toBe('true')
})

Keyboard Navigation Testing

typescript
test('modal can be navigated with keyboard', async () => {
  const wrapper = mount(Modal, {
    props: { isOpen: true }
  })
  
  const modal = wrapper.find('[role="dialog"]')
  const closeButton = wrapper.find('[aria-label="Close"]')
  
  // Modal should be focusable
  expect(modal.attributes('tabindex')).toBe('-1')
  
  // Escape key should close modal
  await modal.trigger('keydown', { key: 'Escape' })
  expect(wrapper.emitted('close')).toBeTruthy()
  
  // Tab navigation should work
  closeButton.element.focus()
  await closeButton.trigger('keydown', { key: 'Tab' })
  
  // Focus should be trapped within modal
  expect(document.activeElement).not.toBe(document.body)
})

Visual Regression Testing

Screenshot Testing

typescript
// tests/visual/visual-regression.test.ts
import { test, expect } from '@playwright/test'

test('button variants match visual designs', async ({ page }) => {
  await page.goto('/component-library/button')
  
  // Test different button variants
  const variants = ['primary', 'secondary', 'danger']
  
  for (const variant of variants) {
    const button = page.locator(`[data-variant="${variant}"]`)
    await expect(button).toHaveScreenshot(`button-${variant}.png`)
  }
})

test('responsive layout renders correctly', async ({ page }) => {
  await page.goto('/dashboard')
  
  // Test desktop layout
  await page.setViewportSize({ width: 1920, height: 1080 })
  await expect(page).toHaveScreenshot('dashboard-desktop.png')
  
  // Test tablet layout
  await page.setViewportSize({ width: 768, height: 1024 })
  await expect(page).toHaveScreenshot('dashboard-tablet.png')
  
  // Test mobile layout
  await page.setViewportSize({ width: 375, height: 667 })
  await expect(page).toHaveScreenshot('dashboard-mobile.png')
})

Test Data Management

Factories and Fixtures

typescript
// tests/factories/user.ts
import { faker } from '@faker-js/faker'

export function createUser(overrides: Partial<User> = {}): User {
  return {
    id: faker.number.int(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
    avatar: faker.image.avatar(),
    role: faker.helpers.arrayElement(['admin', 'user', 'moderator']),
    createdAt: faker.date.past().toISOString(),
    updatedAt: faker.date.recent().toISOString(),
    ...overrides
  }
}

export function createUsers(count: number): User[] {
  return Array.from({ length: count }, () => createUser())
}

// Usage
test('user list displays multiple users', () => {
  const users = createUsers(5)
  const wrapper = mount(UserList, { props: { users } })
  
  expect(wrapper.findAll('.user-item')).toHaveLength(5)
})

Database Seeding

typescript
// tests/setup/database.ts
export async function seedTestDatabase() {
  const db = getTestDatabase()
  
  // Clear existing data
  await db.table('users').del()
  await db.table('posts').del()
  
  // Insert test data
  const users = await db.table('users').insert([
    createUser({ email: 'admin@example.com', role: 'admin' }),
    createUser({ email: 'user@example.com', role: 'user' })
  ])
  
  const posts = await db.table('posts').insert([
    { title: 'Test Post 1', userId: users[0].id },
    { title: 'Test Post 2', userId: users[1].id }
  ])
  
  return { users, posts }
}

Continuous Integration

GitHub Actions

yaml
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        test-type: [unit, integration, e2e]
    
    steps:
      - uses: actions/checkout@v3
      - uses: oven-sh/setup-bun@v1
      
      - name: Install dependencies
        run: bun install
      
      - name: Run tests
        run: bun test:${{ matrix.test-type }}
        
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        if: matrix.test-type == 'unit'
        with:
          file: ./coverage/lcov.info

Test Reporting

typescript
// tests/setup/reporting.ts
import { afterAll } from 'bun:test'

afterAll(() => {
  // Generate test report
  const report = {
    timestamp: new Date().toISOString(),
    tests: global.__TEST_RESULTS__,
    coverage: global.__COVERAGE__
  }
  
  // Save report
  require('fs').writeFileSync(
    './test-reports/results.json',
    JSON.stringify(report, null, 2)
  )
})

Released under the MIT License.