ts-testing
Guides TypeScript testing with Vitest and TDD. Use when adding TypeScript test coverage, mocking async behavior, or writing property and type-level tests.
Files3
View on GitHubSKILL.md.agents-plugin/skills/ts-testing/SKILL.md
GitHubTypeScript Testing Patterns
Comprehensive TypeScript testing patterns for writing reliable, maintainable tests using Vitest as the primary runner, following TDD methodology. Vitest pairs with Vite (same team, shared config) so the transform pipeline used in development is the same one used in tests — no duplicated build configuration.
When to Use
- Writing new TypeScript functions, classes, or modules
- Adding test coverage to existing TypeScript code
- Creating benchmarks for performance-sensitive code
- Implementing property-based tests for input validation
- Writing type-level tests for library APIs
- Following TDD workflow in a TypeScript project
How It Works
- Identify target code — find the function, class, or module to test.
- Write a test — use
describe/it/expectin a colocated*.test.tsfile (ortests/dir for integration tests). - Mock external dependencies — prefer dependency injection; fall back to
vi.mock()for modules you can't control. - Run tests (RED) — confirm the test fails for the expected reason.
- Implement (GREEN) — write the minimum code needed to pass.
- Refactor — clean up while keeping tests green.
- Check coverage —
vitest --coverage, target 80%+.
Critical Rules
- Vitest for unit and integration tests; Playwright only for real-browser E2E.
- Prefer dependency injection over
vi.mock()when you control the seams. - Use
vi.useFakeTimers()instead of realsetTimeout. - Clear mocks between tests:
beforeEach(() => vi.clearAllMocks()). - Strict typing in tests — same
tsconfigstrict settings as source; noany. - Type-level tests (
expectTypeOf) for libraries with public type guarantees.
References
Paths are relative to this SKILL.md's directory. Read only the file that matches the task. If a file cannot be read, say so.
references/vitest-core.md Read when: TDD workflow; unit tests; assertions; error and rejection testing; integration tests; async tests and fake timers (
vi.useFakeTimers).references/advanced-testing.md Read when: parameterized tests with
test.each; property-based testing with fast-check; mocking (vi.mock,vi.fn,vi.spyOn) and dependency injection; type-level testing withexpectTypeOf; benchmarks; CI integration; alternatives to Vitest.
Test Coverage
Running Coverage
# Vitest's v8 provider is built in — no extra deps
vitest run --coverage
// vitest.config.ts — coverage config
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
exclude: ['**/*.d.ts', '**/*.config.ts', 'tests/**', 'dist/**', 'src/**/*.bench.ts'],
},
},
});
Coverage Targets
| Code type | Target |
|---|---|
| Critical business logic | 100% |
| Public library API | 90%+ |
| General application code | 80%+ |
| Generated / binding code | Exclude |
Testing Commands
# Watch mode (default when running vitest interactively)
vitest
# Run once and exit (use in CI)
vitest run
# Run files/tests matching a pattern
vitest user # files matching "user"
vitest -t "valid email" # test names matching "valid email"
# Coverage
vitest run --coverage
# Change the reporter
vitest run --reporter=verbose
vitest run --reporter=dot
# Benchmark mode
vitest bench
# Browser-based UI explorer
vitest --ui
# Type-check tests alongside runtime tests
vitest --typecheck
# Force single-thread (useful for debugging shared state)
vitest --poolOptions.threads.singleThread=true
# Run only changed files
vitest --changed
Best Practices
DO:
- Write tests FIRST (TDD)
- Colocate unit tests with source (
foo.ts+foo.test.ts) - Keep integration tests in
tests/with asetupFilesentry - Test behavior, not implementation detail
- Use
describeto group related tests;it/testfor individual cases - Prefer
toEqualfor objects and arrays,toBefor primitives - Use
test.eachto eliminate duplication across input sets - Inject dependencies so tests don't need
vi.mock() - Reset mocks between tests (
beforeEach(() => vi.clearAllMocks())) - Run tests under the same
tsconfigstrict settings as source
DON'T:
- Use
anyin test code — strict-mode rules still apply - Use real
setTimeoutin tests — usevi.useFakeTimers() - Reach for
vi.mock()when dependency injection would work - Test third-party libraries (trust their tests; test your integration with them)
- Ignore flaky tests — fix them or quarantine with
it.skipand a tracking comment - Share mutable state between tests — each test must be independent