| name | testing-best-practices |
| description | Language-specific testing best practices. Load this skill when writing or reviewing tests. Covers test structure, factories, mocking boundaries, and framework conventions for TypeScript/JavaScript, Python, and Go. |
| license | MIT |
| compatibility | opencode |
Running the test suite
Always run pnpm test from the monorepo root. Never invoke the test runner directly (vitest, jest, etc.), never run from a workspace subdirectory, and never use --filter or any scope flag. The full suite — linting, TypeScript type checking, and every workspace — must exit with zero errors. A failure anywhere in the monorepo is a blocking failure for the task. TypeScript errors are test failures.
If build instructs you to run only a specific workspace's tests, ignore that instruction and run the full suite anyway.
Universal rules
These apply regardless of language or framework.
- Use test factories for all test data in unit and integration tests. Never construct domain objects or database records inline in test bodies. Factories centralise the knowledge of how to build valid objects and make tests resilient to schema changes.
- Test behaviour, not implementation. A test should survive a refactor of the internals as long as the external behaviour is unchanged.
- One concept per test. Multiple assertions are fine if they all verify the same logical outcome. Testing multiple independent behaviours in one test makes failures ambiguous.
- Tests must be hermetic. A test must not depend on the order it runs in, on shared mutable state, or on the side effects of another test.
- Avoid logic in tests. No conditionals, no loops, no try/catch. If you need logic to construct the expected value, that is a sign the test is too complex.
- Name the thing under test, the scenario, and the expected outcome:
calculateDiscount_whenUserIsVIP_returns20Percent.
Seed data and test credentials
Any project with authenticated endpoints must include seed data that covers every auth role the API defines (e.g. member, admin). Each seeded user must have a known, plaintext password so that an auth token can be obtained programmatically without mocking.
Document the credentials in the README under a "Local development" or "Test credentials" section. The format must be a simple table or list that a human or automated agent can read without inference:
## Test credentials
| Role | Email | Password |
|--------|------------------------|-------------|
| member | member@example.com | password123 |
| admin | admin@example.com | password123 |
These credentials are for local development only. They must never be used in staging or production environments. The seed script must be idempotent — running it multiple times must not create duplicate records or fail.
If the project does not yet have a seed script, create one as part of any task that introduces authentication. Seed data is not optional.
TypeScript / JavaScript
Framework: Use Vitest for unit and integration tests. Use Playwright for end-to-end.
Factories
Every domain object (User, Order, Product, etc.) has a corresponding factory class. Factories live in tests/factories/ and are the only place in the test suite that knows how to construct a valid instance of a domain object.
BaseFactory — define this once in test_utils/factories/base.ts and extend it for every domain type:
export abstract class BaseFactory {
abstract build(overrides?: Partial): T
buildList(count: number, overrides?: Partial): T[] {
return Array.from({ length: count }, () => this.build(overrides))
}
}
Domain factory — one file per domain type in test_utils/factories/:
import { randomUUID } from 'crypto'
import { BaseFactory } from './base'
import type { User } from '../../src/types'
const TEST_PASSWORD_HASH = '$2b$10$test-hash-for-testing-only'
class UserFactory extends BaseFactory {
build(overrides: Partial = {}): User {
return {
id: randomUUID(),
name: 'Test User',
email: `test-${randomUUID()}@example.com`,
passwordHash: TEST_PASSWORD_HASH,
isAdmin: false,
companyId: null,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
}
}
}
export const userFactory = new UserFactory()
Usage in tests — always use the factory, never inline objects:
const user = userFactory.build()
const admin = userFactory.build({ isAdmin: true })
const users = userFactory.buildList(3)
const user = { id: '123', name: 'Test User', email: 'test@example.com', ... }
Key rules for factories:
- Default values must produce a valid, fully-configured object with no required fields missing.
- Use
randomUUID() for IDs and unique fields (like email) so tests are hermetic by default — two calls to build() produce independent objects.
- Use a fixed constant for values that are expensive to compute or must be consistent across tests (like
TEST_PASSWORD_HASH).
- Overrides use the spread-last pattern: define all defaults first, then spread
overrides at the end. This ensures any override wins cleanly.
- Do not put logic or conditionals inside
build(). If you need a meaningfully different variant, create a named helper method on the factory:
class UserFactory extends BaseFactory {
build(overrides: Partial = {}): User { ... }
admin(overrides: Partial = {}): User {
return this.build({ isAdmin: true, ...overrides })
}
withCompany(companyId: string, overrides: Partial = {}): User {
return this.build({ companyId, ...overrides })
}
}
Mocking: Use vi.mock at module boundaries only — external services, database clients, filesystem. Do not mock functions within the same module you are testing. Prefer dependency injection over module-level mocking where possible.
Async: Always await async calls in tests. Never use done callbacks. Prefer async/await over .resolves/.rejects matchers for readability, except when the assertion is the only thing in the test.
React component tests: Use React Testing Library. Query by accessible role, label, or visible text. Do not use getByTestId or getByRole with fabricated roles. Do not test internal state or component methods. Test what a user would see and interact with.
expect(screen.getByRole('button', { name: 'Submit' })).toBeDisabled()
expect(wrapper.instance().state.isLoading).toBe(true)
Coverage: Do not chase coverage numbers. 100% coverage with behaviour-free tests is worthless. Aim for tests that would catch real regressions.
Python
Framework: Use pytest. Use pytest-factoryboy with factory_boy for factories, or plain fixture functions returning dataclass/Pydantic instances.
Factories: Define factories in a factories.py file per module or in a shared tests/factories/ directory. Register them as pytest fixtures where appropriate.
import factory
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda n: f"user-{n}")
email = factory.LazyAttribute(lambda o: f"{o.id}@example.com")
role = "member"
Fixtures: Use pytest fixtures for setup and teardown. Keep fixtures small and composable. Avoid giant conftest.py files that do everything — split by concern.
Mocking: Use pytest-mock (mocker fixture) or unittest.mock.patch as a context manager. Patch at the point of use, not at the point of definition.
def test_sends_email(mocker):
send = mocker.patch("myapp.notifications.send_email")
trigger_notification()
send.assert_called_once()
Async: Use pytest-asyncio for async tests. Mark async tests with @pytest.mark.asyncio.
Parametrize: Use @pytest.mark.parametrize to cover multiple input cases without duplicating test bodies. Each parameter set should have an id for readable failure output.