
Accelint Ts Testing
- 312 installs
- 21 repo stars
- Updated August 4, 2026
- gohypergiant/agent-skills
Accelint TS Testing is an agent skill that guides TypeScript test development and related infrastructure setup for developers who need faster, consistent test coverage during application builds.
About
Accelint TS Testing is an agent skill from gohypergiant/agent-skills aimed at TypeScript testing workflows during development and infrastructure management. The skill helps agents scaffold, refine, and organize TypeScript tests so teams maintain coverage while building features. Developers reach for Accelint TS Testing when a TypeScript codebase needs test patterns, suite structure, or infra guidance rather than one-off assertions. Source metadata is sparse, so treat Accelint TS Testing as a TypeScript-focused testing accelerator within agent-driven build workflows.
- accelint-ts-testing
- Development
Accelint Ts Testing by the numbers
- 312 all-time installs (skills.sh)
- Ranked #1,279 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-ts-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 312 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 4, 2026 |
| Repository | gohypergiant/agent-skills ↗ |
How do you accelerate TypeScript test setup in a repo?
For development and infrastructure management.
Who is it for?
TypeScript developers who want agent-guided test scaffolding and infrastructure patterns while building features.
Skip if: Teams needing language-agnostic QA strategy, production monitoring, or non-TypeScript test frameworks exclusively.
When should I use this skill?
A TypeScript project needs new tests, better suite organization, or testing infrastructure guidance during active development.
What you get
TypeScript test files, suite structure, and testing infrastructure configuration
- TypeScript test files
- Testing infrastructure guidance
Files
Vitest Best Practices
Comprehensive patterns for writing maintainable, effective vitest tests. Focused on expert-level guidance for test organization, clarity, and performance.
NEVER Do When Writing Vitest Tests
- NEVER write tests for files with no behavior - Constants files (just
export const X = value), type definition files, GLSL uniform declarations, and pure data files contain no logic to test. Testingexpect(MY_CONSTANT).toBe(42)verifies nothing: if the value changes, the test changes with it, providing zero protection. These "tests" waste CI time and create maintenance burden when values change. Test behavior (functions, logic, transformations), not data declarations. If a file exports only types, constants, or data structures with no functions or logic, skip testing it entirely. - NEVER skip global mock cleanup configuration - Manual cleanup appears safe but creates "action at a distance" failures: a mock in test file A leaks into test file B running 3 files later, causing non-deterministic failures that only appear when tests run in specific orders. These Heisenbugs waste hours in CI debugging. Configure
clearMocks: true,mockReset: true,restoreMocks: trueinvitest.config.tsonce to eliminate this entire class of order-dependent failure. - NEVER nest describe blocks more than 2 levels deep - Deep nesting creates cognitive overhead and excessive indentation. Put context in test names instead:
it('should add item to empty cart')vsdescribe('when cart is empty', () => describe('addItem', ...)). - NEVER write test descriptions that don't read as sentences - Test descriptions must complete the sentence "it ..." in lowercase. Write
it('should add item to cart')notit('Add item to cart')orit('It should add item to cart'). The description reads as a sentence when prefixed with "it": "it should add item to cart". Capitalized starts, non-sentence formats likeit('addToCart test'), or redundant "It should" break readability and test output consistency. Example-based tests useit('should...')while property-based tests useit('property: ...')format. - NEVER test library internals that the library already tests - Testing
expect(array.map(fn)).toEqual(expected)wastes time verifying that Array.prototype.map works correctly. The JavaScript/TypeScript standard library and established third-party libraries are already well-tested. Focus tests on your business logic, not on proving that lodash, React, or the language itself works. If you find yourself testing "does this library function do what it claims?", you're testing the wrong layer. Test how your code uses libraries, not whether libraries work. - NEVER export internal functions just to test them - Tests should verify behavior through the public API, not reach into implementation details. Exporting private helpers, internal utilities, or implementation functions solely to enable testing is a code smell that indicates either: (1) the public API is insufficient for testing the behavior, or (2) the tests are verifying implementation details instead of behavior. If internal logic is complex enough to warrant dedicated testing, extract it into a separate module with its own public API and test file. Private functions get tested indirectly through the public functions that call them.
- NEVER mock your own pure functions - Mocking internal code makes tests brittle and less valuable. Mock only external dependencies (APIs, databases, third-party libraries). Prefer fakes > stubs > spies > mocks.
- NEVER use loose assertions like `toBeTruthy()` or `toBeDefined()` - These assertions pass for multiple distinct values you never intended:
toBeTruthy()passes for1,"false",[], and{}- all semantically different. When refactoring changesgetUser()from returning{id: 1}to returning1, your test still passes but your production code breaks. Loose assertions create false confidence that evaporates in production.toBeTypeOf()is NOT a loose assertion. - NEVER test implementation details instead of behavior - Tests that verify "function X was called 3 times" create false failures: you optimize code to call X once via memoization, all tests fail, yet the user experience is identical (and faster). These tests actively punish performance improvements and refactoring. Test what users observe (outputs given inputs), not how your code achieves it internally.
- NEVER share mutable state between tests - Tests that depend on execution order or previous test state create flaky, unreliable suites. Each test must be fully independent with fresh setup.
- NEVER use `any` or skip type checking in test files - When implementation signatures change, tests with
as anysilently pass while calling functions with wrong arguments. You ship broken code that TypeScript could have caught. Tests are executable documentation:user as anycommunicates nothing, butcreateTestUser(Partial<User>)shows exactly what properties matter for this test case. - NEVER mark test files as complete without running TypeScript type checking - Test files are typically excluded from
tsconfig.jsoncompilation paths, so runningtscat the project root won't catch type errors in tests. Type errors in tests cause runtime failures, incorrect test behavior, and false confidence from tests that don't test what they claim. Before marking any test file as "done", you MUST runtsc --noEmitdirectly against the test file using the project's package manager (npm/pnpm/bun/yarn). For monorepos,cdinto the specific package directory first, then run type checking. Fix all type errors before proceeding - never useas anyor@ts-ignoreto bypass errors. - NEVER assume TypeScript types prevent runtime errors - TS types are compile-time only and vanish at runtime. Testing only "type-valid" inputs creates a false sense of security. In production, functions receive invalid data from JSON APIs without validation,
JSON.parse()results, external libraries, user input, and database records. A function typed asprocess(data: ValidData)can still receivenull,undefined, or malformed objects at runtime. Test defensive programming scenarios: passnullto non-nullable parameters,undefinedto required fields, malformed objects to typed parameters. These "type-invalid" tests catch real bugs that TypeScript cannot prevent. - NEVER write weak properties when stronger ones exist - Property-based tests that only verify "no exception thrown" or "returns a value" provide minimal coverage. When testing encode/decode pairs, verify roundtrip equality (
decode(encode(x)) === x), not just that decode succeeds. When testing normalization, verify idempotence (normalize(normalize(x)) === normalize(x)), not just that it returns a string. Weak properties give false confidence: they pass but don't actually validate correctness.
Before Writing Tests, Ask
Apply these expert thinking patterns before implementing tests:
Should This File Be Tested?
- Does this file contain behavior to test? Files that only declare constants, types, or data structures without logic don't need tests. Constants files (
export const X = 42), type definition files (type User = {...}), GLSL uniform declarations, configuration objects, and pure data files have no behavior to verify. If the file contains no functions, no logic, no transformations - skip testing it. Test behavior, not data.
Test Isolation and Setup
- Where should cleanup logic live? Think in layers: configuration eliminates entire error classes (mock cleanup in vitest.config.ts), setup files handle project-wide concerns (custom matchers, global mocks), beforeEach handles test-specific state. Each test doing its own mock cleanup is like each function doing its own null checks - it works but misses the point. Push concerns to the highest appropriate layer.
- Does this test depend on previous tests or shared state? Test suites are parallel universes - each test should work identically whether it runs first, last, or alone. State dependency creates "quantum tests" that pass or fail based on execution order. If a test needs data from another test, they're actually one test split artificially.
What to Test
- Am I testing behavior or implementation? Test what users experience (inputs → outputs), not how code achieves it (which functions were called). Implementation tests break during safe refactoring.
- What's the simplest dependency I can use? Real implementation > fake > stub > spy > mock. Each step down this hierarchy adds brittleness. Mock only when using real code is impractical (external APIs, slow operations).
Test Clarity
- Can someone understand this test in 5 seconds? Follow AAA pattern (Arrange, Act, Assert) with clear boundaries. If setup is complex, extract to helper functions with descriptive names.
- Are there multiple variations of the same behavior? Use
it.each()for parameterized tests instead of copying test structure. One assertion per concept keeps tests focused.
Performance and Maintenance
- Will this test still be valuable in 6 months? Avoid testing framework internals or trivial operations. Focus on business logic, edge cases, and error handling that actually prevent bugs.
- Is this test fast enough to run on every save? Avoid expensive operations in tests. Use fakes for databases, mock timers for delays, stub external calls. Tests should complete in milliseconds.
What This Skill Covers
Expert guidance on vitest testing patterns:
1. Organization - File placement, naming, describe block structure 2. AAA Pattern - Arrange, Act, Assert for instant clarity 3. Parameterized Tests - Using it.each() to reduce duplication 4. Error Handling - Testing exceptions, edge cases, fault injection 5. Assertions - Strict assertions to catch unintended values 6. Test Doubles - Fakes, stubs, mocks, spies hierarchy and when to use each 7. Async Testing - Promises, async/await, timers, concurrent tests 8. Performance - Fast tests through efficient setup and global config 9. Vitest Features - Coverage, watch mode, setup files, config discovery 10. Snapshot Testing - When snapshots help vs hurt maintainability 11. Property-Based Testing - Using fast-check for stronger coverage with generated inputs
How to Use
This skill uses a progressive disclosure structure to minimize context usage:
1. Start with the Overview (AGENTS.md)
Read AGENTS.md for a concise overview of all rules with one-line summaries and the workflow for discovering existing test configuration.
2. Check for Existing Test Configuration
Before writing tests:
- First check
vitest.config.tsfor global mock cleanup settings (clearMocks,mockReset,restoreMocks) - Then search for setup files (
test/setup.ts,vitest.setup.ts, etc.) and analyze their configuration - See the workflow in AGENTS.md
3. Load Specific Rules as Needed
Use these explicit triggers to know when to load each reference file:
MANDATORY Loading (load entire file):
- Writing async tests with promises/timers → async-testing.md
- Working with mocks, stubs, spies, or fakes → test-doubles.md
- Auditing/reviewing existing test files → property-based-testing.md (to identify PBT opportunities)
Load When You See These Patterns:
- Nested describe blocks >2 levels deep → organization.md
- Test files not co-located with implementation → organization.md
- Tests without clear Arrange/Act/Assert structure → aaa-pattern.md
- Duplicate test code with slight variations → parameterized-tests.md
- Missing error case tests or inadequate edge case coverage → error-handling.md
- Loose assertions like `toBeTruthy()` or `toBeDefined()` → assertions.md
- Tests running slow (>100ms per test) → performance.md
- Need coverage, watch mode, or vitest-specific features → vitest-features.md
- Considering or reviewing snapshot tests → snapshot-testing.md
- Encode/decode pairs, validators, normalizers, or pure functions → property-based-testing.md
- Code with invariants, mathematical properties, or data transformations → property-based-testing.md
- Existing fast-check or property-based tests → property-based-testing.md
Do NOT Load Unless Specifically Needed:
- Do NOT load performance.md if tests are fast (<50ms)
- Do NOT load snapshot-testing.md unless snapshots are mentioned
- Do NOT load vitest-features.md for basic test writing
4. Apply the Pattern
Each reference file contains:
- ❌ Incorrect examples showing the anti-pattern
- ✅ Correct examples showing the optimal implementation
- Explanations of why the pattern matters
5. Use the Report Template
When this skill is invoked for test code review, use the standardized report format:
Template: `assets/output-report-template.md`
The report format provides:
- Executive Summary with test quality impact assessment
- Severity levels (Critical, High, Medium, Low) for prioritization
- Impact analysis (test reliability, maintainability, performance, clarity)
- Categorization (Test Organization, Assertions, Test Doubles, Async Testing, Performance)
- Pattern references linking to detailed guidance in references/
- Summary table for tracking all issues
When to use the report template:
- Skill invoked directly via
/accelint-ts-testing <path> - User asks to "review test code" or "audit tests" across file(s), invoking skill implicitly
When NOT to use the report template:
- User asks to "write a test for this function" (direct implementation)
- User asks "what's wrong with this test?" (answer the question)
- User requests specific test fixes (apply fixes directly without formal report)
IMPORTANT: When auditing tests, ALWAYS check for property-based testing opportunities
- Load property-based-testing.md during every audit
- Follow the "Workflow: Test Code Review/Audit" in AGENTS.md
- Check for high-value PBT patterns: encode/decode pairs, normalizers, validators, pure functions, sorting functions
- Include PBT opportunities in the audit report even if no other issues are found
Quick Example
See quick-start.md for a complete before/after example showing how this skill transforms unclear tests into clear, maintainable ones.
Vitest Best Practices
Note:
This document is mainly for agents and LLMs to follow when maintaining, generating, or refactoring vitest tests. Humans may also find it useful, but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Expert-level vitest testing guidance designed for AI agents and LLMs. Each rule includes one-line summaries here, with links to detailed examples in the references/ folder. Load reference files only when you need detailed implementation guidance for a specific rule.
Token efficiency principle: This guide focuses on expert-level insights and non-obvious patterns. It assumes understanding of basic vitest concepts (describe, it, expect, vi) and focuses on decisions experts make: when to mock vs use real code, how to structure tests for maintainability, and performance optimization patterns.
---
How to Use This Guide
1. Start here: Scan the rule summaries to identify relevant patterns 2. Check for existing setup files: Before writing tests, look for setup files that configure global mocks and utilities 3. Load references as needed: Click through to detailed examples only when implementing 4. Progressive loading: Each reference file is self-contained with examples
This structure minimizes context usage while providing complete implementation guidance when needed.
Workflow: Before Writing Tests
0. Verify the file contains testable behavior Before writing any tests, check if the file actually needs testing:
- Does it contain functions or logic? → Test it
- Does it only export constants, types, or data? → Skip testing it
Files without behavior (constants files, type definitions, GLSL uniform declarations, pure data files) don't need tests. Testing expect(CONSTANT).toBe(value) provides no value and wastes CI time.
1. Check vitest.config.ts for global configuration Verify mock cleanup is configured globally:
// Look for these settings:
clearMocks: true // Mock cleanup configured?
mockReset: true // Mock reset configured?
restoreMocks: true // Mock restore configured?If not present, recommend adding them. This eliminates the entire class of mock cleanup errors.
2. Discover existing test setup files Check common locations for test setup configuration:
test/setup.{ts,js}ortesting/setup.{ts,js}vitest.setup.{ts,js}orsrc/test/setup.{ts,js}- Check
vitest.config.tsfor configuredsetupFilesandglobalSetup
3. Analyze setup file contents When found, identify:
- Global mocks (fetch, timers, etc.)
- Custom matchers (e.g.,
@testing-library/jest-dom) - Test utilities and helpers
- Environment configuration
4. Only add per-test cleanup for non-mock resources If global config handles mocks, DO NOT add manual mock cleanup:
- ❌ Don't add
vi.clearAllMocks()(handled by config) - ✅ Do clean up listeners, connections, custom state
Principle: Configuration over repetition Mock cleanup is a safety concern. Configure it once globally to make forgetting impossible. Manual cleanup in every test violates DRY and creates maintenance burden.
See vitest-features.md and performance.md for detailed examples.
Workflow: Before Marking Test Files Complete
CRITICAL: This workflow is MANDATORY. Never skip type checking test files.
Before marking any test file as "complete" or "done", verify type correctness:
Why this matters: Test files are typically excluded from tsconfig.json compilation (not in include paths), so running tsc at the project root won't catch type errors in tests. Type errors in tests can cause:
- Runtime failures that should have been caught at compile time
- Incorrect test behavior due to type mismatches
- False confidence from tests that don't actually test what they claim
Verification steps:
1. Navigate to the package directory (CRITICAL for monorepos): For monorepos or multi-package projects, you MUST cd into the specific package directory before running type checking. TypeScript needs to run from where the tsconfig.json and node_modules are located for that package.
# Example for monorepo:
cd packages/my-package
# Then run tsc from here2. Check test file directly with TypeScript: Use the project's package manager to run TypeScript:
# Detect which package manager to use:
# - npm: npm exec tsc -- --noEmit path/to/test.test.ts
# - pnpm: pnpm exec tsc --noEmit path/to/test.test.ts
# - bun: bunx tsc --noEmit path/to/test.test.ts
# - yarn: yarn exec tsc --noEmit path/to/test.test.tsTo detect the package manager, check for:
bun.lockborbun.lock→ usebunxpnpm-lock.yaml→ usepnpm execyarn.lock→ useyarn execpackage-lock.json→ usenpm exec
2. Look for common type issues:
- Mock types not matching actual implementation types
- Test data with missing or incorrect properties
- Assertion types that don't match expected values
- Missing type parameters on generic functions
- Incorrect use of type guards or type assertions
3. Fix all type errors before marking complete (NON-NEGOTIABLE) This step is MANDATORY, not optional. Type errors in tests are as critical as type errors in production code.
- Do NOT use
as anyor@ts-ignoreto bypass type checking - Update test data to match actual types
- Fix mock return types to match implementation
- Add proper type annotations where TypeScript cannot infer
- If you encounter type errors, STOP and fix them - do not proceed with "I'll fix types later"
Example type errors to catch:
// ❌ Type error: property 'email' is missing
const user = createUser({ name: 'Alice' })
// ✅ Correct: all required properties provided
const user = createUser({ name: 'Alice', email: 'alice@example.com' })
// ❌ Type error: vi.fn() returns unknown, not User
const mockGetUser = vi.fn().mockReturnValue({ id: 1 })
// ✅ Correct: explicitly type the mock
const mockGetUser = vi.fn<() => User>().mockReturnValue({ id: 1, name: 'Alice', email: 'test@example.com' })Principle: Type-safe tests prevent silent failures Type errors in tests are as critical as type errors in production code. Catch them before marking work complete.
CRITICAL REMINDER: This workflow is NOT optional. Running tsc --noEmit against test files is a REQUIRED step before marking test work as complete. If you skip this step, you risk shipping broken tests that provide false confidence.
Workflow: Test Code Review/Audit
When reviewing existing test code (skill invoked with file path or user asks to "review tests" or "audit tests"), follow this systematic approach:
1. Load property-based-testing.md for pattern detection Always load property-based-testing.md during test audits to check for PBT opportunities.
2. Identify anti-patterns and violations Check for violations of rules in sections 1.1-1.10 below.
3. Check for property-based testing opportunities For each test file, analyze the code under test and identify high-value PBT patterns:
ALWAYS check for these patterns:
- Encode/decode pairs: Functions like
encode()/decode(),serialize()/deserialize(),toJSON()/fromJSON()→ Suggest roundtrip property - Normalization functions:
normalize(),sanitize(),format()→ Suggest idempotence property - Validator + normalizer pairs:
isValid()+normalize()→ Suggest "isValid(normalize(x)) always true" - Pure transformation functions: No side effects, deterministic → Multiple properties may apply
- Sorting/ordering functions:
sort(),compare()→ Suggest ordering + idempotence properties - Data structure operations: Custom collections with invariants → Suggest invariant properties
When identifying PBT opportunities:
- Check if fast-check is installed (
package.jsondevDependencies) - If installed: Recommend PBT improvements directly
- If NOT installed: Suggest PBT as an option with user approval required
4. Generate report using template Use assets/output-report-template.md and include PBT opportunities in a dedicated section.
Example PBT opportunity detection:
// Code under test:
function get<T>(obj: Record<string, T>, path: string): T | undefined
// Example-based test found:
it('gets nested value', () => {
expect(get({ a: { b: 1 } }, 'a.b')).toBe(1)
})
// PBT opportunity identified:
// ✅ EXCELLENT CANDIDATE for property-based testing
// Pattern: Pure function with clear invariants
// Properties to test:
// 1. get(obj, path) returns undefined for non-existent paths
// 2. get(obj, path) preserves type (type preservation)
// 3. get(obj, path) never throws on valid inputsPrinciple: Proactive improvement suggestions Don't wait for users to ask "would any benefit from PBT?" — proactively identify and suggest PBT opportunities as part of every test audit.
---
1. General
1.1 Organization
Place test files next to implementation; one test file per module. View detailed examples
1.2 AAA Pattern
Structure tests as Arrange, Act, Assert for clarity. View detailed examples
1.3 Parameterized Tests
Use it.each for variations; one behavior per test. View detailed examples
1.4 Error Handling
Test negative cases, fault injection, and recovery thoroughly. View detailed examples
1.5 Assertions
Use strict assertions (toEqual, toStrictEqual) over loose ones. View detailed examples
1.6 Test Doubles
Prefer fakes > stubs > spies/mocks; avoid over-mocking. View detailed examples
1.7 Async Testing
Test promises, async/await, and timers correctly. View detailed examples
1.8 Performance
Keep tests fast through efficient setup and avoiding expensive operations. View detailed examples
1.9 Vitest Features
Use coverage, watch mode, benchmarking, and other vitest-specific features. View detailed examples
1.10 Snapshot Testing
Use snapshots for appropriate cases; avoid common pitfalls. View detailed examples
1.11 Property-Based Testing
Use fast-check for stronger coverage with generated inputs; test encode/decode pairs, validators, normalizers, and invariants. View detailed examples
╭─────────────────────╮ │ accelint-ts-testing │ ╰─────────────────────╯
<!-- Make sure to display this warning block to the user --> ┌─────────────────────────────────────────────────────┐ │ ⚠️ WARNING: This skill does it's best to process │ │ the context needed to suggest correct unit tests │ │ but it can make mistakes. Please make sure to read │ │ the suggested unit tests to make sure that they are │ │ correct. │ └─────────────────────────────────────────────────────┘
Report: [Target Name]
<!-- INSTRUCTIONS FOR COMPLETING THIS TEMPLATE:
1. Replace [Target Name] with the specific file/module being audited (e.g., "UserService Tests", "cart.test.ts")
2. EXECUTIVE SUMMARY: Provide a high-level overview
- Summarize what was audited and the scope
- Count issues by severity and category
- Include Impact Assessment explaining test reliability risks and maintainability concerns
3. PHASE 1 - ISSUE GROUPING RULES:
- Group issues when they share the SAME root cause AND same fix pattern
- Example: Multiple instances of loose toBeTruthy() assertions → group together
- Example: Different async testing violations → separate issues
- Use subsections (4-8) for grouped issues, individual numbers (1, 2, 3) for unique issues
4. PHASE 1 - EACH ISSUE/GROUP MUST INCLUDE:
- Location (file:line or file:line-range)
- Current code with ❌ marker
- Clear explanation of the issue
- Severity (Critical, High, Medium, Low)
- Category (Test Organization, AAA Pattern, Assertions, Test Doubles, Async Testing, Performance, Snapshot Testing, Code Quality)
- Impact (false confidence, test flakiness, maintainability concerns)
- Pattern Reference (which references/*.md file)
- Recommended Fix with ✅ marker
5. SEVERITY LEVELS:
- Critical: Tests that actively lie — give false confidence that code is correct when it isn't
Examples: loose toBeTruthy() on return values with multiple valid types, shared mutable state causing order-dependent failures, missing global mock cleanup leaking across files
- High: Tests that punish safe refactoring or hide real bugs
Examples: testing implementation details (spy call counts on internal functions), mocking your own pure functions, using any types that let wrong argument types pass silently
- Medium: Tests that are hard to maintain or understand
Examples: describe nesting >2 levels deep, duplicate test structures that should be parameterized, unclear AAA boundaries, no error case coverage
- Low: Minor clarity and style improvements
Examples: could use it.each() for small variations, test name could be more descriptive, minor naming issues
6. CATEGORIES:
- Test Organization: File placement, describe nesting depth, test naming, co-location with implementation
- AAA Pattern: Missing or unclear Arrange/Act/Assert boundaries, multiple concepts per test
- Assertions: Loose assertions (toBeTruthy, toBeDefined), wrong assertion for the type, multiple unrelated assertions per test
- Test Doubles: Wrong level of hierarchy (using mocks when fakes/stubs suffice), mocking own pure functions, over-mocking
- Async Testing: Missing await, incorrect async patterns, timer handling, concurrent test issues
- Performance: Tests running >100ms, expensive setup in tests, missing global mock config (clearMocks/mockReset/restoreMocks)
- Snapshot Testing: Overuse of snapshots, unstable snapshots, snapshots masking real assertions
- Code Quality: Using
anyin tests, unclear naming, missing type coverage, poor test isolation
7. IMPACT FIELD SHOULD DESCRIBE:
- False confidence: Does this test pass for values you never intended?
- Test reliability: Could this test fail non-deterministically (flakiness, order-dependence)?
- Refactor safety: Will this test break when you safely refactor internals without changing behavior?
- Test clarity: Can someone understand what this test verifies in 5 seconds?
- CI performance: Is this slowing the test suite and feedback loop?
- Production safety: What bugs could ship because this test doesn't catch them?
8. PHASE 2: Generate summary table from Phase 1 findings
- Include all issues with their numbers
- Keep it concise - one row per issue/group
See references/ for pattern guidance on each category. -->
Executive Summary
Completed systematic audit of [file/module path] following accelint-ts-testing standards. Identified [N] test quality issues across [N] severity levels. [Brief description of what this module does and why reliable tests matter here].
Key Findings:
- [N] Critical issues (false confidence, shared state, mock leakage across test files)
- [N] High severity issues (implementation testing, over-mocking, type safety gaps)
- [N] Medium severity issues (hard to maintain, missing parameterization, unclear structure)
- [N] Low severity issues (minor clarity and naming improvements)
Impact Assessment: [Explain the overall test quality and reliability concerns. Consider:]
- Are there tests that always pass but don't catch real bugs (false confidence)?
- Are there flaky tests that fail non-deterministically due to shared state or mock leakage?
- Do implementation tests block safe refactoring of internals?
- Are async tests properly awaited to avoid race conditions?
- Does the test suite run fast enough to support TDD workflows?
---
Phase 1: Identified Issues
1. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet showing the issue]Issue:
- [Point 1 explaining the problem]
- [Point 2 with specifics about the violation]
- [Point 3 quantifying the impact if possible]
Severity: [Critical|High|Medium|Low] Category: [Test Organization|AAA Pattern|Assertions|Test Doubles|Async Testing|Performance|Snapshot Testing|Code Quality] Impact:
- False confidence: [Does this test pass for values you never intended?]
- Reliability: [Could this fail non-deterministically or due to test order?]
- Refactor safety: [Will this break when safely refactoring internals?]
- Production safety: [What bugs could ship undetected?]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet showing the fix]---
2. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet]Issue:
- [Explanation]
Severity: [Critical|High|Medium|Low] Category: [Test Organization|AAA Pattern|Assertions|Test Doubles|Async Testing|Performance|Snapshot Testing|Code Quality] Impact:
- False confidence: [Does this test pass for values you never intended?]
- Reliability: [Could this fail non-deterministically or due to test order?]
- Refactor safety: [Will this break when safely refactoring internals?]
- Production safety: [What bugs could ship undetected?]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet]---
3-N. [Grouped Issues] - [Shared Issue Type] ([N] instances)
<!-- Use this format when multiple issues share the same root cause and fix pattern -->
Locations:
[file:line]- [function/context][file:line]- [function/context][file:line]- [function/context]
Example from [specific location]:
// ❌ Current: [Brief description of problem]
[representative code snippet]Issue:
- [Shared root cause explanation]
- [Why this pattern is problematic]
- [Impact across all instances]
Severity: [Critical|High|Medium|Low] Category: [Test Organization|AAA Pattern|Assertions|Test Doubles|Async Testing|Performance|Snapshot Testing|Code Quality] Impact:
- False confidence: [Does this test pass for values you never intended, across all instances?]
- Reliability: [Could these fail non-deterministically or due to test order?]
- Refactor safety: [Will these break when safely refactoring internals?]
- Production safety: [What bugs could ship undetected?]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[fixed code snippet]Same pattern applies to all [N] instances:
// [Location/function 2]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]
// [Location/function 3]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]---
Property-Based Testing Opportunities
<!-- ALWAYS include this section when performing test audits. Check for high-value PBT patterns. -->
[Function/Pattern] - [PBT Pattern Type]
Location: [file:line] (implementation under test)
Current Test Approach:
// Example-based test
[current test code snippet]Why Property-Based Testing Would Help:
- [Reason 1: broader coverage, edge case discovery, etc.]
- [Reason 2: specific property that example tests can't verify]
- [Reason 3: quantify improvement - "100 generated inputs vs 3 examples"]
Pattern Identified: [Encode/Decode | Normalization | Validator | Pure Function | Sorting | Data Structure | Other]
Recommended Properties to Test: 1. [Property name]: [property formula]
- Verifies: [what this property guarantees]
2. [Property name]: [property formula]
- Verifies: [what this property guarantees]
Implementation Sketch:
// ✅ Property-based test with fast-check
import fc from 'fast-check'
it('[property description]', () => {
fc.assert(
fc.property(
[arbitrary],
(input) => {
// Property verification
expect([assertion]).toBe([expected])
}
)
)
})Prerequisites:
- [ ] fast-check installed? [Yes/No - if No, requires
npm install -D fast-check]
---
Summary: Identified [N] high-value opportunities for property-based testing. These patterns would benefit from broader input coverage and stronger guarantees than example-based tests alone provide.
---
Phase 2: Categorized Issues
| # | Location | Issue | Category | Severity |
|---|---|---|---|---|
| 1 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 2 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 3 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 4-N | [multiple] | [Brief issue description] | [Category] | [Severity] |
Total Issues: [N] By Severity: Critical ([N]), High ([N]), Medium ([N]), Low ([N]) By Category: [Category1] ([N]), [Category2] ([N]), [Category3] ([N])
Vitest Best Practices
Expert patterns for writing maintainable, effective vitest tests. This skill provides comprehensive guidance on test organization, assertions, mocking, async testing, and performance optimization.
For complete guidance, see [SKILL.md](SKILL.md)
Quick Start
Installation
npm install -D vitest @vitest/coverage-v8Basic Configuration
Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
clearMocks: true // Clear call history before each test
mockReset: true // Reset implementation before each test
restoreMocks: true // Restore original implementation before each test
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
},
});Your First Test
// math.test.ts
import { describe, it, expect } from 'vitest';
import { add } from './math';
describe('add', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5);
});
});Common Commands
vitest # Watch mode
vitest run # Run once
vitest --coverage # With coverage
vitest --ui # Visual UIPackage.json Integration
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"test:typecheck": "vitest typecheck"
}
}What You'll Learn
This skill covers expert-level guidance on:
- Test organization and AAA pattern
- Strict assertions to catch bugs
- Test doubles hierarchy (fakes > stubs > mocks)
- Async testing and timer mocking
- Performance optimization and global configuration
- Vitest-specific features and setup file discovery
See [SKILL.md](SKILL.md) for complete patterns and examples.
Learn More
- Vitest Documentation
- Testing Library
- AGENTS.md - Quick reference for all rules
Version Compatibility
- Vitest: v1.0.0+
- Node.js: v18.0.0+
- TypeScript: v5.0.0+
1.2 AAA Pattern
Structure tests as Arrange, Act, Assert for maximum clarity and readability.
What is AAA?
- Arrange: Set up the test data, dependencies, and preconditions
- Act: Execute the code under test
- Assert: Verify the expected outcome
This pattern makes tests instantly understandable by separating setup, execution, and verification.
Basic AAA Structure
❌ Incorrect: mixed arrange/act/assert
it('should return the default value for an unknown property', () => {
const defaultColor: Color = [128, 128, 128, 155];
const colorLookup = lookup(colorTable, defaultVal(defaultColor));
const actual = colorLookup('UNKNOWN');
expect(actual).toEqual(defaultColor);
// Another test mixed in!
const result2 = colorLookup('ANOTHER');
expect(result2).toEqual(defaultColor);
});✅ Correct: clear AAA structure with comments
it('should return the default value for an unknown property', () => {
// Arrange
const defaultColor: Color = [128, 128, 128, 155];
const colorLookup = lookup(colorTable, defaultVal(defaultColor));
// Act
const actual = colorLookup('UNKNOWN');
// Assert
expect(actual).toEqual(defaultColor);
});Why? Without clear separation and with multiple behaviors tested together, it's hard to understand what's being tested and why a test fails.
Blank Lines for Separation
Use blank lines between AAA sections even without comments for simple tests.
❌ Incorrect: no visual separation
it('should calculate total price with tax', () => {
const cart = new ShoppingCart();
cart.addItem({ name: 'Widget', price: 100 });
const total = cart.calculateTotal(0.08);
expect(total).toEqual(108);
});✅ Correct: visual separation with blank lines
it('should calculate total price with tax', () => {
const cart = new ShoppingCart();
cart.addItem({ name: 'Widget', price: 100 });
const total = cart.calculateTotal(0.08);
expect(total).toEqual(108);
});Why? Without visual separation, it's harder to quickly identify where setup ends and the actual test logic begins.
Multiple Assertions for Same Behavior
Multiple assertions are OK when they verify different aspects of the same behavior.
❌ Incorrect: testing multiple unrelated behaviors
it('should handle user operations', () => {
// Testing creation
const user = createUser({ email: 'test@example.com' });
expect(user.email).toEqual('test@example.com');
// Testing update (different behavior!)
updateUser(user.id, { name: 'Updated' });
expect(user.name).toEqual('Updated');
// Testing deletion (different behavior!)
deleteUser(user.id);
expect(getUser(user.id)).toBeNull();
});✅ Correct: multiple assertions for one behavior
it('should create user with all required fields', () => {
// Arrange
const userData = { email: 'test@example.com', name: 'Test User' };
// Act
const user = createUser(userData);
// Assert
expect(user.email).toEqual('test@example.com');
expect(user.name).toEqual('Test User');
expect(user.id).toBeDefined();
expect(user.createdAt).toBeInstanceOf(Date);
});Why? Each test should verify one behavior. Split this into three separate tests: creation, update, and deletion.
Avoid Logic in Tests
Keep the AAA sections simple - avoid conditional logic, loops, or complex calculations.
❌ Incorrect: complex logic in test
it('should filter active users', () => {
const users = generateUsers(100); // Hidden complexity
const userService = new UserService(users);
const activeUsers = userService.getActiveUsers();
// Complex verification logic
let count = 0;
for (const user of users) {
if (user.active) {
expect(activeUsers).toContain(user);
count++;
}
}
expect(activeUsers).toHaveLength(count);
});✅ Correct: straightforward test logic
it('should filter active users', () => {
// Arrange
const users = [
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true },
];
const userService = new UserService(users);
// Act
const activeUsers = userService.getActiveUsers();
// Assert
expect(activeUsers).toHaveLength(2);
expect(activeUsers[0].name).toEqual('Alice');
expect(activeUsers[1].name).toEqual('Charlie');
});Why? If the test has bugs, you won't know if the test or the code is wrong. Keep tests simple and obvious.
Complex Arrange Sections
For complex setup, extract to helper functions or factories.
❌ Incorrect: complex setup in test
it('should apply discount to orders over $50', () => {
// Arrange - too much going on!
const order = new Order();
const items = [];
for (let i = 0; i < 10; i++) {
const item = {
id: i,
name: `Item ${i}`,
price: 10 * i,
category: i % 2 === 0 ? 'even' : 'odd',
taxable: i > 5,
};
items.push(item);
order.addItem(item);
}
const discountService = new DiscountService();
// Act
const discountedTotal = discountService.applyDiscount(order);
// Assert
expect(discountedTotal).toBeLessThan(order.total);
});✅ Correct: extracted setup helper
function createTestOrder(items: number = 3): Order {
const order = new Order();
for (let i = 0; i < items; i++) {
order.addItem({ id: i, name: `Item ${i}`, price: 10 * i });
}
return order;
}
it('should apply discount to orders over $50', () => {
// Arrange
const order = createTestOrder(10);
const discountService = new DiscountService();
// Act
const discountedTotal = discountService.applyDiscount(order);
// Assert
expect(discountedTotal).toBeLessThan(order.total);
expect(discountService.discountApplied).toEqual(0.1);
});Why? Complex setup obscures the test's purpose. Extract to helper functions or test-utils.
Extracting Duplicated Setup Code
When the same setup code appears in multiple tests, extract it into a generic setup function.
❌ Incorrect: duplicated setup across tests
describe('callNextSecond', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should execute callback at next clock second', () => {
// Arrange
const callback = vi.fn();
const now = 1234567890;
const SECOND = 1000;
vi.setSystemTime(now);
const expectedDelay = SECOND - (now % SECOND);
// Act
callNextSecond(callback);
// Assert
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(expectedDelay);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should set timeout with correct delay', () => {
// Arrange
const callback = vi.fn();
const now = 1500; // 1500 % 1000 = 500
const SECOND = 1000;
vi.setSystemTime(now);
const expectedDelay = SECOND - (now % SECOND); // 500
// Act
callNextSecond(callback);
// Assert
vi.advanceTimersByTime(expectedDelay - 1);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(callback).toHaveBeenCalledTimes(1);
});
});✅ Correct: extracted generic setup function
describe('callNextSecond', () => {
const SECOND = 1000;
/**
* Sets up test environment for timer tests
* @returns Test fixtures with callback, expected delay, and utility to advance timers
*/
function setupTimerTest(now: number) {
const callback = vi.fn();
vi.setSystemTime(now);
const expectedDelay = SECOND - (now % SECOND);
return {
callback,
expectedDelay,
advanceToNextSecond: () => vi.advanceTimersByTime(expectedDelay),
};
}
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should execute callback at next clock second', () => {
// Arrange
const { callback, advanceToNextSecond } = setupTimerTest(1234567890);
// Act
callNextSecond(callback);
// Assert
expect(callback).not.toHaveBeenCalled();
advanceToNextSecond();
expect(callback).toHaveBeenCalledTimes(1);
});
it('should set timeout with correct delay', () => {
// Arrange
const { callback, expectedDelay } = setupTimerTest(1500);
// Act
callNextSecond(callback);
// Assert
vi.advanceTimersByTime(expectedDelay - 1);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(callback).toHaveBeenCalledTimes(1);
});
});Why? Duplicated setup code makes tests harder to maintain and increases the chance of inconsistencies. Extract common setup into a function that returns test fixtures. This also allows you to add useful utilities (like advanceToNextSecond()) that make tests more readable.
Async/Await with AAA
AAA works perfectly with async tests - just add await in the Act section.
❌ Incorrect: mixing setup with async calls
it('should fetch user from API', async () => {
const userId = 'user-123';
const apiClient = new ApiClient();
const user = await apiClient.getUser(userId); // Act hidden in middle
const profile = await apiClient.getProfile(userId); // More acts!
expect(user.id).toEqual(userId);
expect(profile.userId).toEqual(userId);
});✅ Correct: async AAA pattern
it('should fetch user from API', async () => {
// Arrange
const userId = 'user-123';
const apiClient = new ApiClient();
// Act
const user = await apiClient.getUser(userId);
// Assert
expect(user.id).toEqual(userId);
expect(user.name).toBeDefined();
});Why? Multiple async operations without clear separation make it unclear what's being tested.
When to Omit AAA Comments
For simple tests (≤10 lines with obvious structure), AAA comments add noise without value. Use blank lines for separation instead.
✅ Correct: simple test without AAA comments
it('should add two numbers', () => {
const calculator = new Calculator();
const result = calculator.add(2, 3);
expect(result).toEqual(5);
});Guidelines for AAA comments:
- Omit for simple tests: single setup line, single action, single assertion
- Include for complex tests: multiple setup steps, complex assertions, or when AAA boundaries are unclear
- Always use blank lines between sections regardless of whether comments are present
✅ More examples of simple tests without AAA comments:
it('should return empty array for no items', () => {
const cart = new ShoppingCart();
const items = cart.getItems();
expect(items).toEqual([]);
});
it('should concatenate strings', () => {
const result = concat('hello', 'world');
expect(result).toEqual('helloworld');
});When the test structure is obvious from blank lines alone, omit the comments. Comments should clarify, not state the obvious.
1.5 Assertions
Use strict, precise assertions that verify exact behavior. Loose assertions can pass even when code is wrong.
Equality Assertions
Prefer toEqual over toBe for objects and arrays. Use toStrictEqual when you need to verify undefined properties.
✅ Correct: toEqual for objects
it('should return user object with correct properties', () => {
const user = createUser({ name: 'John', email: 'john@example.com' });
expect(user).toEqual({
name: 'John',
email: 'john@example.com',
id: expect.any(String),
createdAt: expect.any(Date),
});
});❌ Incorrect: toBe for objects
it('should return user object', () => {
const user = createUser({ name: 'John' });
expect(user).toBe({ name: 'John' }); // Always fails - different references!
});toEqual vs toStrictEqual
Use toStrictEqual when you need to verify that undefined properties don't exist.
✅ Correct: toStrictEqual catches undefined properties
it('should not include undefined properties', () => {
const user = { name: 'John', email: 'john@example.com' };
expect(user).toStrictEqual({ name: 'John', email: 'john@example.com' });
});⚠️ Potential issue: toEqual ignores undefined
it('may not catch unexpected undefined', () => {
const user = { name: 'John', email: undefined };
// This passes with toEqual!
expect(user).toEqual({ name: 'John' });
// This fails with toStrictEqual (better)
expect(user).toStrictEqual({ name: 'John' }); // Fails - email is undefined
});Primitives: toBe vs toEqual
For primitives (numbers, strings, booleans), both work, but toBe is more semantically correct.
✅ Correct: toBe for primitives
expect(count).toBe(5);
expect(name).toBe('John');
expect(isValid).toBe(true);✅ Also correct but less semantic: toEqual for primitives
expect(count).toEqual(5); // Works but toBe is clearer for primitivesAvoid Loose Assertions
Don't use fuzzy matchers when you can be precise.
❌ Incorrect: loose assertion
it('should return user data', () => {
const result = fetchUser('123');
expect(result).toContain('john'); // Too vague!
});✅ Correct: precise assertion
it('should return user with email', () => {
const result = fetchUser('123');
expect(result).toEqual({
id: '123',
name: 'John Doe',
email: 'john@example.com',
});
});Array Assertions
Use specific array matchers for clarity.
✅ Correct: specific array matchers
describe('filterActiveUsers', () => {
it('should return array of active users only', () => {
const users = [
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true },
];
const result = filterActiveUsers(users);
expect(result).toHaveLength(2);
expect(result).toEqual([
{ id: 1, name: 'Alice', active: true },
{ id: 3, name: 'Charlie', active: true },
]);
});
it('should contain specific user', () => {
const result = getAllUsers();
expect(result).toContainEqual({ id: 1, name: 'Alice', active: true });
});
it('should return empty array when no active users', () => {
const users = [{ id: 1, name: 'Bob', active: false }];
expect(filterActiveUsers(users)).toEqual([]);
});
});❌ Incorrect: imprecise array checks
it('should return users', () => {
const result = filterActiveUsers(users);
expect(result.length).toBeGreaterThan(0); // How many? Which users?
});String Assertions
Use appropriate string matchers based on what you're testing.
✅ Correct: precise string assertions
describe('formatName', () => {
it('should return full name', () => {
expect(formatName('john', 'doe')).toBe('John Doe');
});
it('should include title when provided', () => {
const result = formatName('john', 'doe', 'Dr.');
expect(result).toBe('Dr. John Doe');
});
it('should match name pattern', () => {
const result = formatName('john', 'doe');
expect(result).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/);
});
it('should contain first name', () => {
const result = formatName('john', 'doe');
expect(result).toContain('John');
});
});❌ Incorrect: overly loose string checks
it('should format name', () => {
const result = formatName('john', 'doe');
expect(result).toBeTruthy(); // Way too vague!
expect(result.length).toBeGreaterThan(0); // Still vague!
});Number Assertions
Use comparison matchers for numeric ranges and boundaries.
✅ Correct: numeric matchers
describe('calculateDiscount', () => {
it('should return positive discount', () => {
const discount = calculateDiscount(100, 0.1);
expect(discount).toBeGreaterThan(0);
expect(discount).toBeLessThanOrEqual(100);
});
it('should return exact discount amount', () => {
expect(calculateDiscount(100, 0.1)).toBe(10);
});
it('should handle floating point comparison', () => {
const result = calculateTax(99.99, 0.0825);
expect(result).toBeCloseTo(8.25, 2); // Within 2 decimal places
});
});Boolean and Nullish Assertions
Be explicit about boolean, null, and undefined checks.
✅ Correct: explicit boolean checks
describe('isValidEmail', () => {
it('should return true for valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should return false for invalid email', () => {
expect(isValidEmail('invalid')).toBe(false);
});
});
describe('findUser', () => {
it('should return null when user not found', () => {
expect(findUser('invalid-id')).toBeNull();
});
it('should return undefined for missing optional field', () => {
const user = createUser({ name: 'John' });
expect(user.middleName).toBeUndefined();
});
it('should have defined email', () => {
const user = createUser({ name: 'John', email: 'john@example.com' });
expect(user.email).toBeDefined();
});
});❌ Incorrect: loose truthy/falsy checks
it('should validate email', () => {
expect(isValidEmail('test@example.com')).toBeTruthy(); // Could be any truthy value!
});
it('should not find user', () => {
expect(findUser('invalid')).toBeFalsy(); // Could be false, null, undefined, 0, etc.
});Why? toBeTruthy/toBeFalsy are too permissive. Be explicit about the expected value.
Object Property Assertions
Use matchers that verify object structure and properties.
✅ Correct: object property matchers
describe('createUser', () => {
it('should have required properties', () => {
const user = createUser({ name: 'John', email: 'john@example.com' });
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name', 'John');
expect(user).toHaveProperty('email', 'john@example.com');
expect(user).toHaveProperty('createdAt');
});
it('should match expected shape', () => {
const user = createUser({ name: 'John', email: 'john@example.com' });
expect(user).toMatchObject({
name: 'John',
email: 'john@example.com',
});
// toMatchObject allows extra properties like id, createdAt
});
});Type Assertions
Verify types when type checking is important.
✅ Correct: type assertions
describe('parseData', () => {
it('should return correct types', () => {
const result = parseData('{"count": 5}');
expect(result.count).toEqual(expect.any(Number));
expect(result.timestamp).toEqual(expect.any(Date));
expect(result.tags).toEqual(expect.any(Array));
});
it('should return string array', () => {
const tags = getTags();
expect(tags).toEqual(expect.arrayContaining([expect.any(String)]));
});
});Asymmetric Matchers
Use asymmetric matchers when exact values aren't known but structure is.
✅ Correct: asymmetric matchers for dynamic values
describe('createOrder', () => {
it('should create order with generated ID', () => {
const order = createOrder({ items: [{ id: 1, quantity: 2 }] });
expect(order).toEqual({
id: expect.stringMatching(/^order-[a-f0-9]+$/),
items: expect.arrayContaining([
expect.objectContaining({ id: 1, quantity: 2 }),
]),
createdAt: expect.any(Date),
status: 'pending',
});
});
});Negation
Use .not to assert something is NOT true, but be specific.
✅ Correct: specific negation
it('should not include deleted users', () => {
const users = getActiveUsers();
expect(users).not.toContainEqual(
expect.objectContaining({ status: 'deleted' })
);
});
it('should not be empty string', () => {
const username = generateUsername();
expect(username).not.toBe('');
expect(username.length).toBeGreaterThan(0);
});❌ Incorrect: vague negation
it('should not be wrong', () => {
expect(result).not.toBeFalsy(); // What IS it then?
});Custom Error Messages
Add custom messages to clarify assertion failures.
✅ Correct: custom error messages for complex assertions
it('should process all items', () => {
const result = processItems(items);
expect(
result.every(item => item.processed === true),
'All items should have processed=true'
).toBe(true);
});Common Assertion Anti-Patterns
❌ Incorrect: no assertion
it('should create user', () => {
createUser({ name: 'John' });
// No assertion! Test always passes!
});❌ Incorrect: asserting implementation details
it('should call internal helper', () => {
const spy = vi.spyOn(service, '_internalHelper');
service.publicMethod();
expect(spy).toHaveBeenCalled(); // Testing internal implementation
});Why? Test behavior, not implementation. Internal helpers can change without breaking public API.
❌ Incorrect: multiple unrelated assertions
it('should work', () => {
const user = createUser({ name: 'John' });
expect(user.name).toBe('John');
const product = createProduct({ name: 'Widget' });
expect(product.name).toBe('Widget'); // Different concern - split into separate test
});1.7 Async Testing
Testing asynchronous code requires special handling to ensure tests wait for async operations to complete.
Basic Async/Await
Always use async/await for testing async functions.
✅ Correct: async/await
describe('fetchUser', () => {
it('should return user data', async () => {
const user = await fetchUser('user-123');
expect(user).toEqual({
id: 'user-123',
name: 'John Doe',
email: 'john@example.com',
});
});
});❌ Incorrect: not awaiting async function
it('should return user data', () => {
const user = fetchUser('user-123'); // Returns Promise, not user!
expect(user).toEqual({ id: 'user-123' }); // Test passes but wrong!
});Why? Without await, you're comparing a Promise object, not the actual result.
Testing Promises
Use resolves and rejects matchers for clean promise testing.
✅ Correct: using resolves matcher
it('should resolve with user data', async () => {
await expect(fetchUser('user-123')).resolves.toEqual({
id: 'user-123',
name: 'John Doe',
});
});✅ Correct: using rejects matcher
it('should reject for invalid user', async () => {
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});❌ Incorrect: manual promise handling
it('should return user', () => {
return fetchUser('user-123').then(user => {
expect(user.id).toBe('user-123');
});
});Why? While this works, async/await is clearer and more maintainable.
Testing Multiple Async Operations
✅ Correct: sequential async operations
it('should create and update user', async () => {
// Arrange
const userData = { name: 'John', email: 'john@example.com' };
// Act
const created = await userService.create(userData);
const updated = await userService.update(created.id, { name: 'Jane' });
// Assert
expect(updated.name).toBe('Jane');
expect(updated.email).toBe('john@example.com');
});✅ Correct: parallel async operations
it('should fetch multiple users in parallel', async () => {
const [user1, user2, user3] = await Promise.all([
fetchUser('user-1'),
fetchUser('user-2'),
fetchUser('user-3'),
]);
expect(user1.id).toBe('user-1');
expect(user2.id).toBe('user-2');
expect(user3.id).toBe('user-3');
});Testing Async Callbacks
For functions that use callbacks, promisify them or use done callback.
✅ Correct: promisify callback-based code
function fetchDataCallback(callback: (err: Error | null, data?: Data) => void) {
// ... async operation
}
function fetchDataPromise(): Promise<Data> {
return new Promise((resolve, reject) => {
fetchDataCallback((err, data) => {
if (err) reject(err);
else resolve(data!);
});
});
}
it('should fetch data', async () => {
const data = await fetchDataPromise();
expect(data).toBeDefined();
});✅ Correct: using done callback (when promisify isn't possible)
it('should call callback with data', (done) => {
fetchDataCallback((err, data) => {
expect(err).toBeNull();
expect(data).toBeDefined();
done();
});
});Testing Timeouts and Delays
Use fake timers to speed up tests that involve delays.
✅ Correct: fake timers for delays
describe('retry logic', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should retry after delay', async () => {
const mockFn = vi.fn()
.mockRejectedValueOnce(new Error('Fail'))
.mockResolvedValueOnce('Success');
const promise = retryWithDelay(mockFn, { delay: 1000, maxRetries: 2 });
// Fast-forward time
await vi.advanceTimersByTimeAsync(1000);
const result = await promise;
expect(result).toBe('Success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
});❌ Incorrect: actually waiting for delays
it('should retry after delay', async () => {
// Test takes 1+ second to run!
const result = await retryWithDelay(mockFn, { delay: 1000 });
expect(result).toBe('Success');
});Why? Real delays slow down your test suite. Use fake timers instead.
Testing Concurrent Operations
Test that async operations work correctly when running concurrently.
✅ Correct: testing race conditions
describe('RateLimiter', () => {
it('should limit concurrent requests', async () => {
const limiter = new RateLimiter({ maxConcurrent: 2 });
const calls: number[] = [];
const task = async (id: number) => {
await limiter.acquire();
calls.push(id);
await delay(10);
limiter.release();
};
await Promise.all([
task(1),
task(2),
task(3),
task(4),
]);
expect(calls).toHaveLength(4);
// Verify no more than 2 concurrent
expect(limiter.getMaxConcurrent()).toBe(2);
});
});Testing Async Iteration
Test async generators and iterables properly.
✅ Correct: testing async generators
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
it('should yield all numbers', async () => {
const numbers: number[] = [];
for await (const num of generateNumbers()) {
numbers.push(num);
}
expect(numbers).toEqual([1, 2, 3]);
});Testing Event Emitters
Wait for async events using promises.
✅ Correct: testing event emitters
it('should emit "complete" event after processing', async () => {
const processor = new DataProcessor();
const completePromise = new Promise((resolve) => {
processor.once('complete', resolve);
});
processor.process(data);
await completePromise;
expect(processor.isComplete()).toBe(true);
});✅ Correct: testing multiple events
it('should emit progress events', async () => {
const processor = new DataProcessor();
const events: string[] = [];
processor.on('progress', (event) => {
events.push(event);
});
const completePromise = new Promise((resolve) => {
processor.once('complete', resolve);
});
processor.process(data);
await completePromise;
expect(events).toEqual(['started', 'processing', 'done']);
});Testing Async Setup/Teardown
Use async beforeEach and afterEach for async setup.
✅ Correct: async setup and teardown
describe('DatabaseTests', () => {
let db: Database;
beforeEach(async () => {
db = await createDatabase();
await db.migrate();
await db.seed();
});
afterEach(async () => {
await db.clear();
await db.close();
});
it('should query users', async () => {
const users = await db.query('SELECT * FROM users');
expect(users).toHaveLength(3);
});
});Testing Promise Rejection
Always test both success and failure paths for async operations.
✅ Correct: testing rejection cases
describe('authenticateUser', () => {
it('should resolve with token for valid credentials', async () => {
const token = await authenticateUser('user@example.com', 'password123');
expect(token).toMatch(/^[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$/);
});
it('should reject for invalid credentials', async () => {
await expect(
authenticateUser('user@example.com', 'wrongpassword')
).rejects.toThrow('Invalid credentials');
});
it('should reject for non-existent user', async () => {
await expect(
authenticateUser('nonexistent@example.com', 'password')
).rejects.toThrow('User not found');
});
});Avoiding Unhandled Promise Rejections
Always handle or assert on promises.
❌ Incorrect: unhandled promise
it('should handle error', () => {
fetchUser('invalid'); // Promise rejection not handled!
});✅ Correct: handling promise
it('should handle error', async () => {
await expect(fetchUser('invalid')).rejects.toThrow();
});Testing Async Error Handling
Verify that errors are properly caught and handled.
✅ Correct: testing try/catch behavior
async function processWithRetry(fn: () => Promise<any>) {
try {
return await fn();
} catch (error) {
// Retry once
return await fn();
}
}
it('should retry on failure', async () => {
const mockFn = vi.fn()
.mockRejectedValueOnce(new Error('Fail'))
.mockResolvedValueOnce('Success');
const result = await processWithRetry(mockFn);
expect(result).toBe('Success');
expect(mockFn).toHaveBeenCalledTimes(2);
});Testing Async Timeout Behavior
Test that operations timeout correctly.
✅ Correct: testing timeouts
describe('fetchWithTimeout', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should timeout after specified duration', async () => {
const slowFn = () => new Promise(resolve => {
setTimeout(() => resolve('done'), 5000);
});
const promise = fetchWithTimeout(slowFn, 1000);
await vi.advanceTimersByTimeAsync(1000);
await expect(promise).rejects.toThrow('Timeout');
});
it('should resolve before timeout', async () => {
const fastFn = () => new Promise(resolve => {
setTimeout(() => resolve('done'), 500);
});
const promise = fetchWithTimeout(fastFn, 1000);
await vi.advanceTimersByTimeAsync(500);
await expect(promise).resolves.toBe('done');
});
});Common Async Testing Anti-Patterns
❌ Incorrect: forgetting async keyword
it('should fetch user', () => { // Missing async!
await fetchUser('user-123'); // Syntax error!
});❌ Incorrect: mixing async/await with done
it('should fetch user', async (done) => { // Don't mix these!
const user = await fetchUser('user-123');
expect(user).toBeDefined();
done();
});❌ Incorrect: not returning or awaiting promise
it('should fetch user', () => {
// Promise not returned or awaited - test finishes before fetch!
fetchUser('user-123').then(user => {
expect(user).toBeDefined();
});
});Fix: Either await the promise or return it.
✅ Correct: returning promise
it('should fetch user', () => {
return fetchUser('user-123').then(user => {
expect(user).toBeDefined();
});
});✅ Better: using async/await
it('should fetch user', async () => {
const user = await fetchUser('user-123');
expect(user).toBeDefined();
});1.4 Error Handling
Error handling is critical - test it as thoroughly as the happy path. Good error handling tests verify that your code fails gracefully and provides meaningful feedback.
Basic Exception Testing
Use toThrow to verify exceptions are thrown for invalid inputs.
✅ Correct: testing specific error messages
describe('divide', () => {
it('should throw TypeError for division by zero', () => {
expect(() => divide(10, 0)).toThrow(TypeError);
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});
it('should throw TypeError for non-numeric inputs', () => {
expect(() => divide('10' as any, 5)).toThrow(TypeError);
expect(() => divide('10' as any, 5)).toThrow('Arguments must be numbers');
});
});❌ Incorrect: not testing error specifics
it('should throw for division by zero', () => {
expect(() => divide(10, 0)).toThrow(); // Which error? What message?
});Why? Testing only that an error is thrown doesn't verify you're throwing the right error with the right message.
Testing Error Types
Always verify the error type and message, not just that an error was thrown.
✅ Correct: specific error type and message
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
describe('validateUser', () => {
it('should throw ValidationError for missing email', () => {
const invalidUser = { name: 'John' };
expect(() => validateUser(invalidUser)).toThrow(ValidationError);
expect(() => validateUser(invalidUser)).toThrow('Email is required');
});
it('should throw ValidationError for invalid email format', () => {
const invalidUser = { name: 'John', email: 'not-an-email' };
expect(() => validateUser(invalidUser)).toThrow(ValidationError);
expect(() => validateUser(invalidUser)).toThrow('Invalid email format');
});
});❌ Incorrect: generic error testing
it('should throw error for invalid user', () => {
expect(() => validateUser({ name: 'John' })).toThrow(Error);
});Why? Any error will pass this test, including unexpected errors from bugs in your code.
Async Error Testing
Use rejects matchers for async functions that throw.
✅ Correct: async error testing
describe('fetchUser', () => {
it('should reject with error for non-existent user', async () => {
await expect(fetchUser('invalid-id')).rejects.toThrow('User not found');
});
it('should reject with NotFoundError', async () => {
await expect(fetchUser('invalid-id')).rejects.toThrow(NotFoundError);
});
});❌ Incorrect: improper async error testing
it('should throw for non-existent user', async () => {
try {
await fetchUser('invalid-id');
// Missing fail() here - test will pass if no error!
} catch (error) {
expect(error.message).toEqual('User not found');
}
});Why? If the function doesn't throw, the test will pass incorrectly. Use rejects matcher instead.
Negative Testing
Test boundary conditions and invalid inputs comprehensively.
✅ Correct: comprehensive negative testing
describe('calculateAge', () => {
it.each([
{ birthdate: 'not-a-date', error: 'Invalid date format' },
{ birthdate: new Date('2099-01-01'), error: 'Birth date cannot be in the future' },
{ birthdate: null, error: 'Birth date is required' },
{ birthdate: undefined, error: 'Birth date is required' },
])('should throw for invalid birthdate: $birthdate',
({ birthdate, error }) => {
expect(() => calculateAge(birthdate as any)).toThrow(error);
}
);
it('should calculate age for valid birthdate', () => {
const birthdate = new Date('1990-01-01');
const age = calculateAge(birthdate);
expect(age).toBeGreaterThan(0);
});
});Fault Injection
Simulate system failures to test error handling and resilience.
✅ Correct: simulating network failures
describe('DataService', () => {
it('should retry on network failure', async () => {
const apiClient = {
fetch: vi.fn()
.mockRejectedValueOnce(new Error('Network error'))
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ data: 'success' }),
};
const service = new DataService(apiClient);
const result = await service.fetchWithRetry('/api/data');
expect(result).toEqual({ data: 'success' });
expect(apiClient.fetch).toHaveBeenCalledTimes(3);
});
it('should fail after max retries', async () => {
const apiClient = {
fetch: vi.fn().mockRejectedValue(new Error('Network error')),
};
const service = new DataService(apiClient);
await expect(service.fetchWithRetry('/api/data'))
.rejects.toThrow('Max retries exceeded');
expect(apiClient.fetch).toHaveBeenCalledTimes(3);
});
});✅ Correct: simulating database errors
describe('UserRepository', () => {
it('should handle database connection errors', async () => {
const db = {
query: vi.fn().mockRejectedValue(new Error('Connection lost')),
};
const repo = new UserRepository(db);
await expect(repo.findById('user-123'))
.rejects.toThrow('Database error: Connection lost');
});
it('should handle query timeouts', async () => {
const db = {
query: vi.fn().mockRejectedValue(new Error('Query timeout')),
};
const repo = new UserRepository(db);
await expect(repo.findById('user-123'))
.rejects.toThrow('Query timeout');
});
});Recovery Testing
Verify that systems can recover from failures and return to normal operation.
✅ Correct: testing circuit breaker recovery
describe('CircuitBreaker', () => {
it('should open circuit after threshold failures', async () => {
const failingService = vi.fn().mockRejectedValue(new Error('Service down'));
const breaker = new CircuitBreaker(failingService, { threshold: 3 });
// Trigger failures to open circuit
for (let i = 0; i < 3; i++) {
await expect(breaker.call()).rejects.toThrow('Service down');
}
// Circuit should now be open
await expect(breaker.call()).rejects.toThrow('Circuit breaker is open');
expect(failingService).toHaveBeenCalledTimes(3); // No more calls
});
it('should close circuit after recovery period', async () => {
vi.useFakeTimers();
const service = vi.fn()
.mockRejectedValueOnce(new Error('Service down'))
.mockResolvedValue('success');
const breaker = new CircuitBreaker(service, {
threshold: 1,
resetTimeout: 5000,
});
// Open circuit
await expect(breaker.call()).rejects.toThrow('Service down');
await expect(breaker.call()).rejects.toThrow('Circuit breaker is open');
// Wait for reset timeout
vi.advanceTimersByTime(5000);
// Circuit should allow retry
const result = await breaker.call();
expect(result).toEqual('success');
vi.useRealTimers();
});
});Error Guessing
Anticipate edge cases based on domain knowledge.
✅ Correct: testing common edge cases
describe('parseJSON', () => {
it.each([
{ input: '', description: 'empty string' },
{ input: 'null', description: 'null value' },
{ input: 'undefined', description: 'undefined as string' },
{ input: '{broken json', description: 'malformed JSON' },
{ input: '{"key": undefined}', description: 'undefined in object' },
{ input: 'NaN', description: 'NaN value' },
{ input: '{\"key\": Infinity}', description: 'Infinity value' },
])('should handle $description gracefully', ({ input }) => {
expect(() => parseJSON(input)).toThrow(SyntaxError);
});
});
describe('processFile', () => {
it.each([
{ filename: '', error: 'Filename cannot be empty' },
{ filename: '../../../etc/passwd', error: 'Invalid filename' },
{ filename: 'file\x00name', error: 'Invalid characters' },
{ filename: '.'.repeat(300), error: 'Filename too long' },
])('should reject dangerous filename: "$filename"',
({ filename, error }) => {
expect(() => processFile(filename)).toThrow(error);
}
);
});Testing Error Boundaries (React)
For React components, test error boundaries handle errors gracefully.
✅ Correct: testing error boundary
describe('ErrorBoundary', () => {
it('should catch errors and display fallback UI', () => {
const ThrowError = () => {
throw new Error('Test error');
};
const { getByText } = render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(getByText('Error occurred')).toBeInTheDocument();
});
it('should log error to error reporting service', () => {
const errorLogger = vi.fn();
const ThrowError = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary onError={errorLogger}>
<ThrowError />
</ErrorBoundary>
);
expect(errorLogger).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Test error',
})
);
});
});Validation Errors
Test comprehensive input validation.
✅ Correct: thorough validation testing
describe('createOrder', () => {
it('should validate all required fields', () => {
const invalidOrders = [
{ error: 'Customer ID is required' },
{ customerId: 'c1', error: 'Items array cannot be empty' },
{ customerId: 'c1', items: [], error: 'Items array cannot be empty' },
{ customerId: '', items: [{ id: 1 }], error: 'Customer ID is required' },
];
invalidOrders.forEach(({ error, ...order }) => {
expect(() => createOrder(order as any)).toThrow(error);
});
});
it('should validate item quantities', () => {
const order = {
customerId: 'c1',
items: [{ id: 1, quantity: -1 }],
};
expect(() => createOrder(order)).toThrow('Quantity must be positive');
});
it('should validate item prices', () => {
const order = {
customerId: 'c1',
items: [{ id: 1, quantity: 1, price: -10 }],
};
expect(() => createOrder(order)).toThrow('Price cannot be negative');
});
});Error Messages Quality
Test that error messages are helpful and actionable.
✅ Correct: descriptive error messages
describe('processPayment', () => {
it('should provide helpful error for insufficient funds', async () => {
const payment = { amount: 1000, accountBalance: 100 };
await expect(processPayment(payment))
.rejects.toThrow('Insufficient funds: balance $100, required $1000');
});
it('should include transaction ID in error messages', async () => {
const payment = { amount: 1000, transactionId: 'txn-123' };
await expect(processPayment(payment))
.rejects.toThrow(expect.stringContaining('txn-123'));
});
});❌ Incorrect: vague error messages
it('should throw error for invalid payment', async () => {
await expect(processPayment(payment)).rejects.toThrow('Error'); // Not helpful!
});Testing Runtime Validation (Beyond TypeScript Types)
Critical concept: TypeScript types are compile-time only and provide zero runtime protection. At runtime, functions can receive any value regardless of type annotations.
Why Type-Invalid Tests Matter
TypeScript types vanish during compilation. Your typed function can receive:
nullorundefineddespite non-nullable types- Wrong types from
JSON.parse()results - Malformed data from external APIs
- Invalid objects from untyped libraries
- User input that bypasses type checking
- Database records with missing fields
❌ Incorrect: only testing type-valid inputs
// Function signature
function processUser(user: User): string {
return `${user.name} <${user.email}>`;
}
// Only testing type-valid inputs
describe('processUser', () => {
it('should format user with name and email', () => {
const user = { name: 'Alice', email: 'alice@example.com' };
expect(processUser(user)).toEqual('Alice <alice@example.com>');
});
});Why incorrect? This test assumes TypeScript prevents invalid inputs. In production, processUser might receive null, undefined, or { name: 'Bob' } (missing email) from:
JSON.parse(apiResponse)without validation- External library returning unexpected data
- Database query with null fields
✅ Correct: testing defensive runtime validation
describe('processUser', () => {
it('should format valid user', () => {
const user = { name: 'Alice', email: 'alice@example.com' };
const result = processUser(user);
expect(result).toEqual('Alice <alice@example.com>');
});
it('should handle null user', () => {
expect(() => processUser(null as any))
.toThrow('User cannot be null or undefined');
});
it('should handle undefined user', () => {
expect(() => processUser(undefined as any))
.toThrow('User cannot be null or undefined');
});
it('should handle missing name field', () => {
const user = { email: 'alice@example.com' };
expect(() => processUser(user as any))
.toThrow('User name is required');
});
it('should handle missing email field', () => {
const user = { name: 'Alice' };
expect(() => processUser(user as any))
.toThrow('User email is required');
});
it('should handle empty object', () => {
expect(() => processUser({} as any))
.toThrow('User name is required');
});
});Why correct? These tests verify the function has defensive runtime validation. The as any casts are intentional—they simulate real runtime scenarios where type safety is bypassed.
Common Runtime Validation Scenarios
✅ Correct: testing JSON API responses
describe('parseUserResponse', () => {
it('should parse valid response', () => {
const json = '{"id": 1, "name": "Alice", "email": "alice@example.com"}';
const user = parseUserResponse(json);
expect(user).toEqual({ id: 1, name: 'Alice', email: 'alice@example.com' });
});
it('should reject response missing required fields', () => {
const json = '{"id": 1, "name": "Alice"}'; // Missing email
expect(() => parseUserResponse(json))
.toThrow('Email is required');
});
it('should reject malformed JSON', () => {
const json = '{invalid json}';
expect(() => parseUserResponse(json))
.toThrow('Invalid JSON');
});
it('should reject null response', () => {
const json = 'null';
expect(() => parseUserResponse(json))
.toThrow('Response cannot be null');
});
it('should reject response with wrong types', () => {
const json = '{"id": "not-a-number", "name": "Alice", "email": "alice@example.com"}';
expect(() => parseUserResponse(json))
.toThrow('ID must be a number');
});
});✅ Correct: testing external library responses
describe('processExternalData', () => {
it('should handle library returning null', () => {
const mockLib = { getData: () => null };
expect(() => processExternalData(mockLib))
.toThrow('Library returned null data');
});
it('should handle library returning unexpected structure', () => {
const mockLib = { getData: () => ({ wrongField: 'value' }) };
expect(() => processExternalData(mockLib))
.toThrow('Invalid data structure');
});
it('should handle library throwing non-Error objects', () => {
const mockLib = { getData: () => { throw 'string error'; } };
expect(() => processExternalData(mockLib))
.toThrow('Unexpected error type');
});
});When to Use as any in Tests
Using as any is intentional and correct when testing defensive programming:
✅ Correct use of `as any`:
// Testing that function validates inputs at runtime
it('should reject null input', () => {
expect(() => processData(null as any))
.toThrow('Input cannot be null');
});
// Testing JSON parsing scenarios
it('should handle API returning wrong type', () => {
const response = { id: 'string-instead-of-number' };
expect(() => validate(response as any))
.toThrow('ID must be number');
});❌ Incorrect use of `as any`:
// Bypassing types to make broken code compile
it('should process user', () => {
const user = { name: 'Alice' } as any; // Missing required email
const result = processUser(user); // Should fail in test, not production
expect(result).toBeDefined();
});Validation Testing Patterns
✅ Comprehensive validation testing pattern:
describe('createOrder', () => {
// Test happy path with valid input
it('should create order with valid data', () => {
const validOrder = {
customerId: 'c-123',
items: [{ id: 'i-1', quantity: 2, price: 10.00 }],
};
const order = createOrder(validOrder);
expect(order.total).toEqual(20.00);
});
// Test null/undefined inputs
it.each([
{ input: null, expected: 'Order data cannot be null' },
{ input: undefined, expected: 'Order data cannot be undefined' },
])('should reject $input', ({ input, expected }) => {
expect(() => createOrder(input as any)).toThrow(expected);
});
// Test missing required fields
it.each([
{ data: {}, field: 'customerId' },
{ data: { customerId: 'c-123' }, field: 'items' },
])('should reject missing $field', ({ data, field }) => {
expect(() => createOrder(data as any))
.toThrow(`${field} is required`);
});
// Test wrong types
it.each([
{ data: { customerId: 123, items: [] }, field: 'customerId', expectedType: 'string' },
{ data: { customerId: 'c-123', items: 'not-array' }, field: 'items', expectedType: 'array' },
])('should reject wrong type for $field', ({ data, field, expectedType }) => {
expect(() => createOrder(data as any))
.toThrow(`${field} must be ${expectedType}`);
});
// Test invalid values
it('should reject empty items array', () => {
const order = { customerId: 'c-123', items: [] };
expect(() => createOrder(order))
.toThrow('Order must contain at least one item');
});
it('should reject negative quantities', () => {
const order = {
customerId: 'c-123',
items: [{ id: 'i-1', quantity: -1, price: 10.00 }],
};
expect(() => createOrder(order))
.toThrow('Quantity must be positive');
});
});Key Principles
1. TypeScript types are documentation, not runtime protection - Always validate inputs at runtime 2. Test the validators, not the types - Verify your validation logic catches invalid data 3. `as any` is correct in validation tests - It simulates real runtime scenarios 4. Test all input sources - JSON APIs, external libraries, databases, user input 5. Defensive programming is testable - These tests verify your code fails safely
1.1 Organization
File Placement and Naming
Place test files next to their implementation for easy discovery and maintenance.
❌ Incorrect: separate test directory
src/
components/
button.tsx
utils/
formatters.ts
tests/
components/
button.test.tsx
utils/
formatters.test.ts✅ Correct: co-located test files
src/
components/
button.tsx
button.test.tsx
utils/
formatters.ts
formatters.test.ts
services/
api.ts
api.test.tsWhy? Separate test directories make it harder to find tests, keep them in sync with implementation, and increase cognitive overhead.
Naming Conventions
Use consistent naming patterns for test files:
*.test.tsor*.spec.tsfor tests
❌ Incorrect: vague or inconsistent names
test1.ts
user_tests.ts // snake_case instead of kebab-case
payment.spec.js // mixing .js and .ts
authTest.ts // camelCase instead of kebab-case✅ Correct: descriptive test file names
user-service.test.ts
payment-processor.test.ts
auth.integration.test.tsOne Test File Per Module
Each module, component, or class should have exactly one corresponding test file.
❌ Incorrect: multiple test files for one module
user-service.test.ts
user-service.get-user.test.ts
user-service.update-user.test.ts✅ Correct: one-to-one mapping
// user-service.ts
export class UserService {
getUser(id: string) { /* ... */ }
updateUser(id: string, data: UserData) { /* ... */ }
}
// user-service.test.ts
describe('UserService', () => {
describe('getUser', () => { /* ... */ });
describe('updateUser', () => { /* ... */ });
});Why? Multiple test files fragment related tests, making it harder to understand the full behavior of a module.
Shared Test Utilities
Store reusable test setup, fixtures, and helpers in dedicated directories.
✅ Correct: organized test utilities
src/
test-utils/
setup.ts // Global test configuration
factories.ts // Test data factories
matchers.ts // Custom matchers
fixtures/
users.json // Test data
products.json
__tests__/
integration/ // Shared integration test setup
db-setup.tsExample test utility:
// test-utils/factories.ts
export function createMockUser(overrides?: Partial<User>): User {
return {
id: 'test-user-id',
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides,
};
}
// user-service.test.ts
import { createMockUser } from '../test-utils/factories';
it('should update user email', () => {
const user = createMockUser({ email: 'old@example.com' });
// ...
});Test Only the Public API Surface
Never export internal functions, private helpers, or implementation details just to make them testable.
❌ Incorrect: exporting internal functions for testing
// user-service.ts
export class UserService {
createUser(data: UserData): User {
const validated = this.validateUserData(data);
const normalized = this.normalizeEmail(validated.email);
return { ...validated, email: normalized };
}
// ❌ Exported only for testing!
export function validateUserData(data: UserData): UserData { /* ... */ }
// ❌ Exported only for testing!
export function normalizeEmail(email: string): string { /* ... */ }
}
// user-service.test.ts
describe('UserService', () => {
// ❌ Testing implementation details
it('should validate user data', () => {
const result = validateUserData({ email: 'TEST@EXAMPLE.COM' });
expect(result).toBeDefined();
});
// ❌ Testing implementation details
it('should normalize email', () => {
expect(normalizeEmail('TEST@EXAMPLE.COM')).toBe('test@example.com');
});
});Why? This creates several problems: (1) Pollutes the module's public API with implementation details, (2) Makes refactoring harder because "private" functions are now part of the public contract, (3) Tests become coupled to implementation, breaking when you refactor even if behavior is unchanged.
✅ Correct: test through public API
// user-service.ts
export class UserService {
createUser(data: UserData): User {
const validated = this.validateUserData(data);
const normalized = this.normalizeEmail(validated.email);
return { ...validated, email: normalized };
}
// Private - not exported
private validateUserData(data: UserData): UserData { /* ... */ }
// Private - not exported
private normalizeEmail(email: string): string { /* ... */ }
}
// user-service.test.ts
describe('UserService', () => {
describe('createUser', () => {
// ✅ Test validation through public API
it('should reject invalid email addresses', () => {
const service = new UserService();
expect(() => service.createUser({ email: 'not-an-email' }))
.toThrow('Invalid email');
});
// ✅ Test normalization through public API
it('should normalize email to lowercase', () => {
const service = new UserService();
const user = service.createUser({
name: 'Test User',
email: 'TEST@EXAMPLE.COM'
});
expect(user.email).toBe('test@example.com');
});
// ✅ Test multiple validation rules through public API
it('should accept valid user data', () => {
const service = new UserService();
const user = service.createUser({
name: 'John Doe',
email: 'john@example.com',
age: 30,
});
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
});
});
});Why? Private functions are tested indirectly through the public API that uses them. This makes tests resilient to refactoring - you can change how validation or normalization works internally without breaking tests, as long as the behavior stays the same.
✅ Alternative: extract complex logic into separate module
// email-validator.ts (new module with its own public API)
export function isValidEmail(email: string): boolean { /* ... */ }
export function normalizeEmail(email: string): string { /* ... */ }
// email-validator.test.ts
describe('Email Validator', () => {
describe('isValidEmail', () => {
it('should accept valid emails', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should reject invalid emails', () => {
expect(isValidEmail('not-an-email')).toBe(false);
});
});
describe('normalizeEmail', () => {
it('should convert to lowercase', () => {
expect(normalizeEmail('TEST@EXAMPLE.COM')).toBe('test@example.com');
});
});
});
// user-service.ts (uses the new module)
import { isValidEmail, normalizeEmail } from './email-validator';
export class UserService {
createUser(data: UserData): User {
if (!isValidEmail(data.email)) {
throw new Error('Invalid email');
}
return { ...data, email: normalizeEmail(data.email) };
}
}
// user-service.test.ts (tests high-level behavior)
describe('UserService', () => {
it('should create user with normalized email', () => {
const service = new UserService();
const user = service.createUser({
name: 'Test',
email: 'TEST@EXAMPLE.COM',
});
expect(user.email).toBe('test@example.com');
});
});Why? If internal logic is complex enough to deserve dedicated unit tests, it's complex enough to be its own module. This gives it a real public API, makes it reusable, and allows both focused unit tests (email-validator.test.ts) and integration tests (user-service.test.ts).
What to Test: Your Code, Not Libraries
Test your business logic, not library internals. Standard libraries (JavaScript/TypeScript built-ins) and well-established third-party libraries are already thoroughly tested. Testing that libraries work correctly wastes time and adds no value.
❌ Incorrect: testing library functionality
describe('array operations', () => {
// ❌ Testing that Array.prototype.map works
it('should map array values', () => {
const input = [1, 2, 3];
const result = input.map(x => x * 2);
expect(result).toEqual([2, 4, 6]);
});
// ❌ Testing that Array.prototype.filter works
it('should filter array values', () => {
const input = [1, 2, 3, 4];
const result = input.filter(x => x > 2);
expect(result).toEqual([3, 4]);
});
// ❌ Testing that lodash works
it('should deeply clone object', () => {
const input = { a: { b: 1 } };
const result = _.cloneDeep(input);
expect(result).toEqual({ a: { b: 1 } });
expect(result).not.toBe(input);
});
});
describe('React hooks', () => {
// ❌ Testing that useState works
it('should update state', () => {
const { result } = renderHook(() => useState(0));
const [, setState] = result.current;
act(() => setState(1));
expect(result.current[0]).toBe(1);
});
});
describe('axios', () => {
// ❌ Testing that axios makes HTTP requests
it('should make GET request', async () => {
const response = await axios.get('https://api.example.com/data');
expect(response.status).toBe(200);
});
});Why incorrect? These tests verify that the language, framework, and libraries work correctly. JavaScript's Array.prototype.map, lodash's cloneDeep, React's useState, and axios's HTTP functionality are already extensively tested by their maintainers. These tests add no value and waste time.
✅ Correct: test how YOUR code uses libraries
describe('UserProcessor', () => {
// ✅ Testing business logic that happens to use map
it('should extract user IDs from user objects', () => {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
const processor = new UserProcessor();
const ids = processor.extractIds(users);
expect(ids).toEqual([1, 2]);
});
// ✅ Testing business rules that use filter
it('should return only active premium users', () => {
const users = [
{ id: 1, status: 'active', tier: 'premium' },
{ id: 2, status: 'inactive', tier: 'premium' },
{ id: 3, status: 'active', tier: 'free' },
];
const processor = new UserProcessor();
const activePremium = processor.getActivePremiumUsers(users);
expect(activePremium).toEqual([
{ id: 1, status: 'active', tier: 'premium' },
]);
});
// ✅ Testing business logic that uses lodash
it('should create user snapshot without modifying original', () => {
const user = { id: 1, profile: { name: 'Alice' } };
const processor = new UserProcessor();
const snapshot = processor.createSnapshot(user);
snapshot.profile.name = 'Bob';
// Testing OUR business requirement: snapshots are independent
expect(user.profile.name).toBe('Alice');
expect(snapshot.profile.name).toBe('Bob');
});
});
describe('useUserData hook', () => {
// ✅ Testing custom hook behavior, not useState itself
it('should initialize with loading state', () => {
const { result } = renderHook(() => useUserData('user-123'));
expect(result.current.isLoading).toBe(true);
expect(result.current.user).toBeNull();
});
// ✅ Testing business logic: error handling
it('should set error when user not found', async () => {
mockAPI.getUser.mockRejectedValue(new Error('User not found'));
const { result } = renderHook(() => useUserData('invalid-id'));
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe('User not found');
});
});
});
describe('ApiClient', () => {
// ✅ Testing OUR API client logic, not axios
it('should add authentication header to requests', async () => {
const mockAxios = { get: vi.fn().mockResolvedValue({ data: 'test' }) };
const client = new ApiClient(mockAxios, 'auth-token-123');
await client.fetchData('/api/users');
expect(mockAxios.get).toHaveBeenCalledWith(
'/api/users',
expect.objectContaining({
headers: { Authorization: 'Bearer auth-token-123' },
})
);
});
// ✅ Testing OUR retry logic, not axios
it('should retry failed requests up to 3 times', async () => {
const mockAxios = {
get: vi.fn()
.mockRejectedValueOnce(new Error('Network error'))
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ data: 'success' }),
};
const client = new ApiClient(mockAxios);
const result = await client.fetchWithRetry('/api/data');
expect(result).toBe('success');
expect(mockAxios.get).toHaveBeenCalledTimes(3);
});
});Why correct? These tests verify YOUR business logic, YOUR error handling, YOUR authentication logic, and YOUR retry strategy. The libraries are implementation details—you could swap lodash for Ramda, or axios for fetch, and these tests should still pass (after adjusting the implementation).
Guidelines for What to Test
Test YOUR code:
- Business logic and rules
- Data transformations specific to your domain
- Error handling and edge cases in your code
- Integration points where your code coordinates multiple libraries
- Custom behavior and workflows
Do NOT test library code:
- Standard library methods (
Array.map,String.split,Object.keys) - Framework internals (React hooks, Vue reactivity, Angular services)
- Third-party library functionality (lodash, axios, moment, zod)
- Language features (promises, async/await, destructuring)
When Library Usage Needs Testing
Test library usage only when:
1. You're wrapping/adapting the library → Test your wrapper, not the library
// ✅ Test your date formatting wrapper
it('should format date in US format', () => {
const formatter = new DateFormatter();
expect(formatter.toUSFormat(new Date('2024-01-15'))).toBe('01/15/2024');
});2. You're combining libraries in complex ways → Test the integration
// ✅ Test how you integrate zod validation with axios
it('should validate response schema before returning data', async () => {
const client = new TypedApiClient(UserSchema);
await expect(client.fetchUser('invalid'))
.rejects.toThrow('Invalid response schema');
});3. You suspect the library has a bug → Fix/report the bug upstream, don't test around it
// ❌ Don't test library bugs in your test suite
it('should work around lodash bug in version X.Y.Z', () => {
// This belongs in the library's test suite, not yours
});The "Trust Boundary" Principle
Libraries you depend on form a trust boundary:
- Inside the boundary (your code): Test thoroughly
- Outside the boundary (libraries): Trust, don't test
- At the boundary (integration points): Test that your code uses libraries correctly
┌─────────────────────────────────────┐
│ Your Application (test this) │
│ ┌────────────────────────────────┐ │
│ │ Business Logic │ │ ← Test
│ │ Error Handling │ │ ← Test
│ │ Data Transformations │ │ ← Test
│ └────────────────────────────────┘ │
│ ┌────────────────────────────────┐ │
│ │ Library Integration Layer │ │ ← Test (how you use libraries)
│ └────────────────────────────────┘ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Libraries (don't test) │
│ ├─ React, lodash, axios, zod │ ← Don't test
│ ├─ Array.map, Promise, fetch │ ← Don't test
│ └─ TypeScript standard library │ ← Don't test
└─────────────────────────────────────┘Describe Block Organization
Use flat, focused describe blocks to group related tests.
❌ Incorrect: deep nesting
describe('ShoppingCart', () => {
describe('when cart is empty', () => {
describe('addItem', () => {
describe('with valid item', () => {
it('should add item', () => { /* ... */ });
});
describe('with invalid item', () => {
it('should throw', () => { /* ... */ });
});
});
});
describe('when cart has items', () => {
describe('addItem', () => {
// deeply nested...
});
});
});✅ Correct: flat structure, grouped by method
describe('ShoppingCart', () => {
describe('addItem', () => {
it('should add item to empty cart', () => { /* ... */ });
it('should increment quantity when adding existing item', () => { /* ... */ });
it('should throw when adding invalid item', () => { /* ... */ });
});
describe('removeItem', () => {
it('should remove item from cart', () => { /* ... */ });
it('should throw when removing non-existent item', () => { /* ... */ });
});
describe('calculateTotal', () => {
it('should return 0 for empty cart', () => { /* ... */ });
it('should sum all item prices', () => { /* ... */ });
});
});Why? Deep nesting makes tests harder to read and adds unnecessary indentation. Put context in the test name instead.
Test Description Format
Test descriptions must be written in lowercase and complete the sentence "it ...".
❌ Incorrect: capitalized or non-sentence formats
describe('ShoppingCart', () => {
it('Add item to cart', () => { /* ... */ });
it('It should calculate total', () => { /* ... */ });
it('Calculate Total', () => { /* ... */ });
it('SHOULD_REMOVE_ITEM', () => { /* ... */ });
it('addToCart test', () => { /* ... */ });
});✅ Correct: lowercase sentence format
describe('ShoppingCart', () => {
it('should add item to cart', () => { /* ... */ });
it('should calculate total price', () => { /* ... */ });
it('should remove item when quantity reaches zero', () => { /* ... */ });
it('should apply discount to premium members', () => { /* ... */ });
});Why? The test description completes the sentence "it ..." — reading as "it should add item to cart", "it should calculate total price". This creates natural, readable test output and maintains consistency across test suites. When tests fail, the output reads like English: "ShoppingCart › should add item to cart ✗".
Pattern: it('should [action] [context]')
- Start with "should" to describe expected behavior
- Use lowercase for the entire description
- Be specific about what is being tested
- Include relevant context when needed
Examples of good test descriptions:
it('should return empty array when no results found')
it('should throw error for invalid email format')
it('should update user profile with new data')
it('should calculate discount for premium members')
it('should preserve order when sorting by date')
it('should retry failed requests up to 3 times')Special case: Property-based tests
// Property-based tests use 'property:' prefix
it('property: decode(encode(x)) === x for all valid inputs', () => {
fc.assert(fc.property(fc.string(), (input) => {
expect(decode(encode(input))).toEqual(input);
}));
});Setup and Teardown
Use beforeEach and afterEach for common setup, but keep tests independent.
❌ Incorrect: shared mutable state between tests
describe('DatabaseService', () => {
const db = createTestDatabase(); // Shared across all tests!
it('should insert record', async () => {
await db.insert({ name: 'Test' });
// ...
});
it('should update record', async () => {
// Depends on previous test's data!
await db.update(1, { name: 'Updated' });
});
});✅ Correct: clean setup per test
describe('DatabaseService', () => {
let db: Database;
beforeEach(async () => {
db = await createTestDatabase();
});
afterEach(async () => {
await db.close();
});
it('should insert record', async () => {
await db.insert({ name: 'Test' });
const records = await db.query('SELECT * FROM users');
expect(records).toHaveLength(1);
});
});Why? Tests that share state are fragile and order-dependent. Each test should be fully independent.
1.3 Parameterized Tests
Use it.each to test the same behavior with different inputs. This eliminates code duplication while keeping tests focused and readable.
Basic Parameterized Tests
Test one behavior with multiple input/output combinations using it.each.
✅ Correct: it.each for variations of same behavior
describe('factorial', () => {
it.each([
{ input: 0, expected: 1 },
{ input: 1, expected: 1 },
{ input: 5, expected: 120 },
{ input: 7, expected: 5040 },
])('should return $expected when given $input', ({ input, expected }) => {
expect(factorial(input)).toEqual(expected);
});
it('should throw when the input is negative', () => {
expect(() => factorial(-1)).toThrow('Number must not be negative');
});
});❌ Incorrect: duplicate tests
describe('factorial', () => {
it('should return 1 when given 0', () => {
expect(factorial(0)).toEqual(1);
});
it('should return 1 when given 1', () => {
expect(factorial(1)).toEqual(1);
});
it('should return 120 when given 5', () => {
expect(factorial(5)).toEqual(120);
});
it('should return 5040 when given 7', () => {
expect(factorial(7)).toEqual(5040);
});
});Why? Duplicated test code is harder to maintain. If the assertion logic changes, you need to update multiple tests.
Template Strings in Test Names
Use $variable syntax in test descriptions to show which inputs are being tested.
✅ Correct: descriptive test names with variables
it.each([
{ email: 'test@example.com', valid: true },
{ email: 'invalid-email', valid: false },
{ email: 'test@', valid: false },
{ email: '@example.com', valid: false },
])('should return $valid for email "$email"', ({ email, valid }) => {
expect(isValidEmail(email)).toEqual(valid);
});❌ Incorrect: generic test name
it.each([
{ email: 'test@example.com', valid: true },
{ email: 'invalid-email', valid: false },
])('should validate email', ({ email, valid }) => {
expect(isValidEmail(email)).toEqual(valid);
});Why? When a test fails, you won't know which input caused the failure without checking test output details.
Array Syntax
You can also use array syntax instead of objects for simpler cases.
✅ Correct: array syntax for simple cases
it.each([
[0, 1],
[1, 1],
[5, 120],
[7, 5040],
])('factorial(%i) should equal %i', (input, expected) => {
expect(factorial(input)).toEqual(expected);
});Object syntax is preferred when you have many parameters or want clearer naming:
✅ Better: object syntax for clarity
it.each([
{ n: 0, expected: 1 },
{ n: 1, expected: 1 },
{ n: 5, expected: 120 },
{ n: 7, expected: 5040 },
])('factorial($n) should equal $expected', ({ n, expected }) => {
expect(factorial(n)).toEqual(expected);
});Testing Multiple Related Behaviors
Don't mix different behaviors in one parameterized test. Split them into separate tests.
✅ Correct: separate tests for different behaviors
describe('calculateDiscount', () => {
it.each([
{ price: 100, discount: 0.1, expected: 90 },
{ price: 50, discount: 0.2, expected: 40 },
{ price: 200, discount: 0.15, expected: 170 },
])('should return $expected when price is $price and discount is $discount',
({ price, discount, expected }) => {
expect(calculateDiscount(price, discount)).toEqual(expected);
}
);
it.each([
{ price: -10, discount: 0.1 },
{ price: 100, discount: -0.1 },
{ price: 100, discount: 1.5 },
])('should throw for invalid inputs: price=$price, discount=$discount',
({ price, discount }) => {
expect(() => calculateDiscount(price, discount)).toThrow();
}
);
});❌ Incorrect: mixing valid and error cases
it.each([
{ price: 100, discount: 0.1, expected: 90, shouldThrow: false },
{ price: 50, discount: 0.2, expected: 40, shouldThrow: false },
{ price: -10, discount: 0.1, expected: null, shouldThrow: true },
{ price: 100, discount: -0.1, expected: null, shouldThrow: true },
])('should handle price=$price and discount=$discount',
({ price, discount, expected, shouldThrow }) => {
if (shouldThrow) {
expect(() => calculateDiscount(price, discount)).toThrow();
} else {
expect(calculateDiscount(price, discount)).toEqual(expected);
}
}
);Why? Conditional logic in tests makes them harder to understand and maintain. Each test should have one clear purpose.
Complex Test Data
For complex test cases, extract data to a separate constant.
✅ Correct: extracted test data
const USER_VALIDATION_CASES = [
{
user: { email: 'valid@example.com', age: 25, name: 'John' },
expected: true,
description: 'valid user',
},
{
user: { email: 'invalid-email', age: 25, name: 'John' },
expected: false,
description: 'invalid email',
},
{
user: { email: 'valid@example.com', age: 15, name: 'John' },
expected: false,
description: 'underage user',
},
{
user: { email: 'valid@example.com', age: 25, name: '' },
expected: false,
description: 'empty name',
},
];
describe('validateUser', () => {
it.each(USER_VALIDATION_CASES)(
'should return $expected for $description',
({ user, expected }) => {
expect(validateUser(user)).toEqual(expected);
}
);
});❌ Incorrect: inline complex data obscures test structure
it.each([
{ user: { email: 'valid@example.com', age: 25, name: 'John', address: { street: '123 Main', city: 'NYC', zip: '10001' }, preferences: { newsletter: true, notifications: false } }, expected: true },
{ user: { email: 'invalid', age: 25, name: 'John', address: { street: '123 Main', city: 'NYC', zip: '10001' }, preferences: { newsletter: true, notifications: false } }, expected: false },
// ... more complex objects
])('should validate user', ({ user, expected }) => {
expect(validateUser(user)).toEqual(expected);
});Edge Cases and Boundaries
Use parameterized tests to comprehensively cover edge cases and boundary conditions.
✅ Correct: comprehensive edge case coverage
describe('clamp', () => {
it.each([
{ value: 5, min: 0, max: 10, expected: 5, case: 'value within range' },
{ value: -5, min: 0, max: 10, expected: 0, case: 'value below min' },
{ value: 15, min: 0, max: 10, expected: 10, case: 'value above max' },
{ value: 0, min: 0, max: 10, expected: 0, case: 'value equals min' },
{ value: 10, min: 0, max: 10, expected: 10, case: 'value equals max' },
{ value: 5, min: 5, max: 5, expected: 5, case: 'min equals max' },
])('should return $expected when $case', ({ value, min, max, expected }) => {
expect(clamp(value, min, max)).toEqual(expected);
});
});Using describe.each for Test Groups
For testing multiple related scenarios, use describe.each to create test suites.
✅ Correct: describe.each for different user roles
describe.each([
{ role: 'admin', canEdit: true, canDelete: true, canView: true },
{ role: 'editor', canEdit: true, canDelete: false, canView: true },
{ role: 'viewer', canEdit: false, canDelete: false, canView: true },
])('User with $role role', ({ role, canEdit, canDelete, canView }) => {
let user: User;
beforeEach(() => {
user = createUser({ role });
});
it(`should ${canEdit ? '' : 'not '}be able to edit`, () => {
expect(user.canEdit()).toEqual(canEdit);
});
it(`should ${canDelete ? '' : 'not '}be able to delete`, () => {
expect(user.canDelete()).toEqual(canDelete);
});
it(`should ${canView ? '' : 'not '}be able to view`, () => {
expect(user.canView()).toEqual(canView);
});
});When NOT to Use Parameterized Tests
Don't use it.each when test cases have different setup or assertion logic.
❌ Incorrect: forcing parameterization
it.each([
{ type: 'email', input: 'test@example.com', setupFn: setupEmail },
{ type: 'phone', input: '123-456-7890', setupFn: setupPhone },
])('should validate $type', ({ type, input, setupFn }) => {
setupFn(); // Different setup for each type
if (type === 'email') {
expect(validateEmail(input)).toEqual(true);
} else {
expect(validatePhone(input)).toEqual(true);
}
});✅ Correct: separate tests with different logic
describe('validateEmail', () => {
it.each([
'test@example.com',
'user.name@example.co.uk',
'user+tag@example.com',
])('should return true for valid email: %s', (email) => {
expect(validateEmail(email)).toEqual(true);
});
});
describe('validatePhone', () => {
it.each([
'123-456-7890',
'(123) 456-7890',
'+1-123-456-7890',
])('should return true for valid phone: %s', (phone) => {
expect(validatePhone(phone)).toEqual(true);
});
});Why? When setup or assertions differ significantly, separate tests are clearer than conditional logic within a parameterized test.
Quick Start Example
Overview
This example demonstrates how to transform an unclear test into a clear, maintainable one following vitest best practices.
Before and After
❌ Incorrect: unclear test
test('product test', () => {
const p = new ProductService().add({name: 'Widget'});
expect(p.status).toBe('pendingApproval');
});Issues:
- Vague test name doesn't describe behavior
- No AAA structure separation
- Unclear what's being tested
- Uses loose assertion (
toBeinstead oftoEqual) - No describe blocks for organization
- Abbreviated variable names
✅ Correct: optimized with vitest best practices
describe('ProductService', () => {
describe('Add new product', () => {
it('should have status "pending approval" when no price is specified', () => {
// Arrange
const productService = new ProductService();
// Act
const newProduct = productService.add({name: 'Widget'});
// Assert
expect(newProduct.status).toEqual('pendingApproval');
});
});
});Improvements:
- Clear, descriptive test name that explains the behavior
- Test description in lowercase, reads as sentence: "it should have status..."
- AAA pattern with comment markers for clarity
- Organized with describe blocks (module > behavior)
- Descriptive variable names (not abbreviated)
- Strict assertion (
toEqualinstead oftoBe)
Key Transformations
1. Test name: 'product test' → 'should have status "pending approval" when no price is specified' 2. Organization: Flat test() → Nested describe() blocks with it() 3. Structure: Mixed code → Clear AAA sections 4. Variables: p → newProduct 5. Assertions: toBe() → toEqual()
This example applies principles from:
- organization.md - Describe block structure
- aaa-pattern.md - Arrange-Act-Assert separation
- assertions.md - Strict assertions
Related skills
FAQ
What is Accelint TS Testing for?
Accelint TS Testing is an agent skill for TypeScript test development and testing infrastructure management. Developers use it during builds to scaffold suites and align test patterns without writing every file manually.
Does Accelint TS Testing replace a test runner?
Accelint TS Testing does not replace Vitest, Jest, or another runner. The skill guides how to structure TypeScript tests and supporting infrastructure so existing runners execute meaningful coverage.