Testing Standards

Overview

Testing patterns and conventions using Vitest. Tests live alongside source files with the .test.ts extension. These rules cover file organization, mocking strategies, and coverage expectations.

Rules

File Structure and Naming

Place test files next to the source they test. Use .test.ts as the extension. Name describe blocks after the module or function under test, and write test cases as "should + expected behavior."

ElementConventionExample
Test file*.test.tsconfig.test.ts
Describe blockFeature/function namedescribe('loadConfig', ...)
Test caseShould + expected behaviorit('should return resolved config', ...)

Correct

src/
├── lib/
│   ├── config.ts
│   └── config.test.ts
├── runtime/
│   ├── executor.ts
│   └── executor.test.ts
└── __tests__/
    └── integration.test.ts

Write Clear Test Cases

Each test should have a single assertion focus. Use async/await for asynchronous code and toMatchObject for partial matching.

Correct

import { describe, it, expect } from 'vitest'
import { resolveScriptPath } from './resolver'

describe('resolveScriptPath', () => {
  it('should resolve path relative to workspace root', () => {
    const result = resolveScriptPath('build', '/project')
    expect(result).toBe('/project/scripts/build.ts')
  })

  it('should return undefined for missing scripts', () => {
    expect(resolveScriptPath('missing', '/project')).toBeUndefined()
  })
})

// Async tests
it('should load config from parent directories', async () => {
  const config = await loadConfig('/project/packages/core')
  expect(config).toMatchObject({
    name: expect.any(String),
  })
})

Mock External Dependencies

Use vi.mock for module-level mocks and vi.fn for individual functions. Replace real I/O (file system, network) with deterministic mocks.

Correct

import { vi, describe, it, expect } from 'vitest'

// Mock a module
vi.mock('node:fs/promises', () => ({
  readFile: vi.fn().mockResolvedValue('{ "name": "test" }'),
  stat: vi.fn().mockResolvedValue({ isFile: () => true }),
}))

// Mock individual functions
const mockCallback = vi.fn()
mockCallback.mockReturnValue('result')
mockCallback.mockResolvedValue('async result')

// Assert calls
expect(mockCallback).toHaveBeenCalledWith('arg')
expect(mockCallback).toHaveBeenCalledTimes(1)

Organize Tests by Feature

Group related tests with nested describe blocks. Use beforeEach to reset mocks and shared state before each test.

Correct

import { beforeEach, describe, it, vi } from 'vitest'

describe('ScriptRunner', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  describe('execute', () => {
    it('should run the script command', () => {})
    it('should pass environment variables', () => {})
  })

  describe('resolve', () => {
    it('should find scripts in workspace root', () => {})
    it('should return error for missing scripts', () => {})
  })
})

Meet Coverage Requirements

Target the following minimum coverage levels by area.

AreaMinimum Coverage
Critical path (config)100%
Business logic80%
Utilities70%

Test Edge Cases and Error Handling

Test pure functions exhaustively, including boundary values and error paths.

Correct

describe('parseTimeout', () => {
  it('should parse valid number', () => {
    expect(parseTimeout('5000')).toBe(5000)
  })

  it('should return default for NaN', () => {
    expect(parseTimeout('abc')).toBe(30000)
  })

  it('should clamp negative values to zero', () => {
    expect(parseTimeout('-1')).toBe(0)
  })
})

it('should return error result for missing config', async () => {
  vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'))

  const [error] = await loadConfig('/missing')
  expect(error).toMatchObject({ message: expect.stringContaining('ENOENT') })
})

Avoid Testing Anti-patterns

Don'tDo Instead
Test implementation detailsTest behavior/outcomes
Large test filesSplit by feature
Shared mutable stateReset in beforeEach
Skip tests without reasonDelete or fix
Test framework codeTrust dependencies

Resources

References

  • Functions -- Pure functions and testability
  • State -- Immutable state patterns