| applyTo | **/* |
|---|---|
| description | Test writing best practices and conventions |
Language-agnostic guidelines for writing effective tests.
Structure each test in three clear sections:
// Arrange - Set up test data and preconditions
// Act - Execute the code being tested
// Assert - Verify the expected outcomeExample:
// Arrange
user = createTestUser(name: "Alice", role: "admin")
// Act
result = user.hasPermission("delete")
// Assert
expect(result).toBe(true)Alternative structure for behavior-focused tests:
// Given - Initial context
// When - Action occurs
// Then - Expected outcomePattern: <unit>_<scenario>_<expectedResult>
Good examples:
calculateTotal_withEmptyCart_returnsZero
userLogin_withInvalidPassword_throwsAuthError
emailValidator_withValidEmail_returnsTrue
Avoid:
test1
testCalculate
itWorks
Place test files alongside source files or in a dedicated test directory:
src/
calculator.js
calculator.test.js # Adjacent to source
tests/
calculator.test.js # Or in test directory
Common extensions:
.test.js,.test.ts.spec.js,.spec.ts_test.goTest.cs.Tests.ps1
- Test individual functions or methods in isolation
- Mock external dependencies
- Fast execution (milliseconds)
- High coverage of edge cases
- Test interaction between components
- May use real databases or services
- Slower than unit tests
- Focus on component boundaries
- Test complete user workflows
- Use real browser/UI automation
- Slowest to execute
- Cover critical user paths
Each test should verify one logical concept:
Good:
test_addItem_increasesCartCount
test_addItem_updatesCartTotal
Avoid:
test_addItem_doesEverything // Tests multiple things
- Each test should run independently
- Don't rely on test execution order
- Clean up test data after each test
- Use fresh fixtures for each test
Bad:
test1_createUser() // Creates user
test2_loginUser() // Assumes user exists from test1Good:
test_loginUser() {
user = createTestUser() // Each test creates its own data
// ... test logic
}Good:
expect(user.isActive).toBe(true)
expect(result).toContain("success")
expect(list).toHaveLength(3)Avoid:
expect(x).toBe(true) // What is x?
assert(result) // What should result be?Good:
email = "valid.user@example.com"
invalidEmail = "not-an-email"Avoid:
email = "test"
x = "asdf"Create helper functions for test data:
function createTestUser(overrides = {}) {
return {
id: generateId(),
name: "Test User",
email: "test@example.com",
role: "user",
...overrides
}
}
// Usage
adminUser = createTestUser({ role: "admin" })- Empty inputs (null, undefined, empty string, empty array)
- Boundary values (0, -1, max int, min int)
- Invalid inputs (wrong type, malformed data)
- Large inputs (performance edge cases)
- Special characters and unicode
- Concurrent access (race conditions)
- External services (APIs, databases)
- Time-dependent operations
- Random number generation
- File system operations
- Network requests
- Simple value objects
- Pure functions with no side effects
- The code you're actually testing
- Only mock what you need
- Verify mock interactions when behavior matters
- Reset mocks between tests
- Prefer dependency injection for easier mocking
Prioritize testing:
- Business-critical functionality
- Error handling and edge cases
- Security-sensitive code
- Complex algorithms
- Aim for meaningful coverage, not 100%
- High coverage doesn't guarantee quality
- Focus on testing behavior, not implementation details
When fixing a bug, always follow this workflow:
- Write a failing test first - Create at least one test that reproduces the bug
- Verify the test fails - Confirm the test fails for the expected reason
- Fix the bug - Implement the minimal fix to make the test pass
- Verify all tests pass - Ensure both the new test and existing tests pass
Example workflow:
# 1. Create test that exposes the bug
test_calculateDiscount_withZeroQuantity_returnsZero()
# This test fails because of the bug
# 2. Run tests - confirm failure
> npm test
FAIL: calculateDiscount returns NaN instead of 0
# 3. Fix the bug in the source code
# 4. Run tests - confirm fix
> npm test
PASS: All tests passing
- Proves the bug exists - The failing test documents the exact issue
- Prevents regressions - The test ensures the bug won't return
- Validates the fix - You know the fix works when the test passes
- Documents behavior - Future developers understand the expected behavior
Name bug-related tests to indicate the scenario being fixed:
calculateTotal_withNullItems_returnsZeroInsteadOfCrashing
parseDate_withLeapYear_handlesFebruary29Correctly
userAuth_withExpiredToken_returnsUnauthorizedNotServerError
- Run related tests locally
- Ensure all tests pass
- Add tests for new functionality
- Update tests for changed behavior
- Tests should run on every PR
- Failed tests should block merging
- Keep test suite fast (parallelize when possible)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Testing implementation | Brittle tests | Test behavior/outcomes |
| Flaky tests | Unreliable CI | Fix timing/ordering issues |
| Slow tests | Developer friction | Optimize or parallelize |
| No assertions | False confidence | Always verify outcomes |
| Commented-out tests | Hidden failures | Delete or fix tests |
| Test data in production | Security risk | Use separate test environment |