Testing
Testing in TypeScript projects should be intentional. The goal is to create confidence in production behavior with a test suite that is fast enough for regular use, broad enough to catch regressions, and simple enough to maintain over time.
New TypeScript projects should default to a layered testing strategy that combines fast unit tests, targeted integration tests, and a small number of high-value end-to-end tests.
Default Standard
Section titled “Default Standard”Use these defaults unless the project has a clear reason to do otherwise:
- Vitest for unit and most integration testing in TypeScript repositories
- Playwright for end-to-end browser testing
- Bun test runner when the project is already standardized on Bun and its test runner fits the framework and CI requirements
- Jest primarily for maintaining existing repositories that already depend on it
Choosing a different testing stack should be a deliberate decision based on runtime compatibility, framework fit, CI behavior, and team familiarity.
Testing Strategy
Section titled “Testing Strategy”A healthy TypeScript test suite usually has more unit tests than integration tests, and more integration tests than end-to-end tests.
Use each test level for a different purpose:
- Unit tests to validate business logic, parsing, transformations, validation rules, and edge cases in isolation
- Integration tests to validate interaction between modules, databases, APIs, queues, file systems, or framework boundaries
- End-to-end tests to validate critical user journeys and deployment-level behavior in a realistic environment
Do not try to force one layer of tests to solve every problem. Fast tests should catch most regressions early, while higher-level tests should confirm the most important workflows work together correctly.
Unit and Component Tests
Section titled “Unit and Component Tests”Vitest is the default test runner for new TypeScript projects because it is fast, works well with modern frontend tooling, and has a straightforward developer experience.
Unit tests should focus on code with meaningful logic, including:
- Domain rules and business logic
- Validation, parsing, and data transformation
- Utility functions with non-trivial behavior
- Error handling and edge cases
- UI component behavior when the project uses a component framework
Good unit tests are small, readable, and deterministic. They should fail for a clear reason and avoid depending on network calls, real databases, wall-clock timing, or unrelated framework setup.
Integration Tests
Section titled “Integration Tests”Integration tests should cover the boundaries that matter to the application, especially where multiple systems or layers interact.
Common integration test targets include:
- Database queries and persistence behavior
- API routes, request handlers, and serialization
- Authentication and authorization flows
- Framework integration points
- External service adapters, preferably behind controlled test doubles or local test environments
Prefer integration tests over excessive mocking when the risk is in how parts of the system work together. If a defect would only appear once real modules are wired together, unit tests alone are not enough.
End-to-End Tests
Section titled “End-to-End Tests”Playwright is the default end-to-end testing tool for TypeScript projects.
End-to-end tests should be limited to the most important flows, such as:
- Signing in and accessing protected areas
- Completing a primary user workflow
- Submitting forms and validating successful outcomes
- Navigating key routes and verifying core page behavior
End-to-end tests are valuable, but they are slower, more brittle, and more expensive to maintain than lower-level tests. Use them to protect business-critical journeys, not every UI variation or edge case.
Choosing Between Vitest, Jest, and Bun
Section titled “Choosing Between Vitest, Jest, and Bun”Vitest should be the default choice for most new TypeScript projects.
Jest remains a valid option in mature repositories that already use it, especially when the current setup is stable and migration would not provide enough value to justify the churn.
Bun test runner is appropriate when a team is already using Bun as a primary toolchain and wants a simpler, fast test runner without adding another dependency. It should be adopted intentionally, especially if the project depends on tooling or framework behavior that is more established in Vitest or Jest ecosystems.
If the repository already has a working test runner, keep it unless there is a concrete maintenance, performance, or compatibility problem worth fixing.
Mocking and Test Data
Section titled “Mocking and Test Data”Mocking should be used carefully.
Prefer these patterns:
- Mock external services and unstable boundaries in unit tests
- Use realistic test data that reflects actual application behavior
- Keep fixtures small and readable
- Test public behavior instead of private implementation details
Avoid mocking so much of the system that the test only proves the mock configuration is internally consistent.
CI and Coverage Expectations
Section titled “CI and Coverage Expectations”Tests should run automatically in CI and provide fast, reliable feedback.
Recommended expectations:
- Run unit and integration tests on every pull request
- Run end-to-end tests for critical paths before release or in the main CI pipeline when execution time is acceptable
- Keep test commands deterministic and easy to run locally
- Use coverage as a signal for gaps, not as the only definition of test quality
High coverage does not guarantee confidence. A smaller set of well-designed tests is usually more valuable than a large number of shallow assertions.
What to Avoid
Section titled “What to Avoid”Avoid these common mistakes:
- Relying only on end-to-end tests for application confidence
- Writing tests that are tightly coupled to implementation details
- Mocking internal modules so aggressively that integration problems are hidden
- Keeping flaky tests in the main suite without fixing or removing them
- Treating coverage percentage as more important than meaningful behavior coverage
- Running slow, full-suite checks in places where targeted fast feedback is the better developer experience
Testing should reduce risk and support delivery. If the test suite is slow, fragile, or difficult to trust, it is not meeting that goal.