Language-Agnostic Testing Principles
Core Testing Philosophy
- •Tests are First-Class Code: Maintain test quality equal to production code
- •Fast Feedback: Tests should run quickly and provide immediate feedback
- •Reliability: Tests should be deterministic and reproducible
- •Independence: Each test should run in isolation
Test-Driven Development (TDD)
The RED-GREEN-REFACTOR Cycle
Always follow this cycle:
- •
RED: Write a failing test first
- •Write the test before implementation
- •Ensure the test fails for the right reason
- •Verify test can actually fail
- •
GREEN: Write minimal code to pass
- •Implement just enough to make the test pass
- •Don't optimize prematurely
- •Focus on making it work
- •
REFACTOR: Improve code structure
- •Clean up implementation
- •Eliminate duplication
- •Improve naming and clarity
- •Keep all tests passing
- •
VERIFY: Ensure all tests still pass
- •Run full test suite
- •Check for regressions
- •Validate refactoring didn't break anything
TDD Benefits
- •Better design through testability requirements
- •Comprehensive test coverage by default
- •Living documentation of expected behavior
- •Confidence to refactor
Quality Requirements
Coverage Standards
- •Minimum 80% code coverage for production code
- •Prioritize critical paths and business logic
- •Don't sacrifice quality for coverage percentage
- •Use coverage as a guide, not a goal
Test Characteristics
All tests must be:
- •Independent: No dependencies between tests (see Test Independence Verification for detailed criteria)
- •Reproducible: Same input always produces same output
- •Fast: Complete test suite runs in reasonable time
- •Self-checking: Clear pass/fail without manual verification
- •Timely: Written close to the code they test
Test Types
Unit Tests
Purpose: Test individual components in isolation
Characteristics:
- •Test single function, method, or class
- •Fast execution (milliseconds)
- •No external dependencies
- •Mock external services
- •Majority of your test suite
Example Scope:
✓ Test calculateTotal() function ✓ Test UserValidator class ✓ Test parseDate() utility
Integration Tests
Purpose: Test interactions between components
Characteristics:
- •Test multiple components together
- •May include database, file system, or APIs
- •Slower than unit tests
- •Verify contracts between modules
- •Smaller portion of test suite
Example Scope:
✓ Test UserService with Database ✓ Test API endpoint with authentication ✓ Test file processing pipeline
End-to-End (E2E) Tests
Purpose: Test complete workflows from user perspective
Characteristics:
- •Test entire application stack
- •Simulate real user interactions
- •Slowest test type
- •Fewest in number
- •Highest confidence level
Example Scope:
✓ Test user registration flow ✓ Test checkout process ✓ Test complete report generation
Test Pyramid
Follow the test pyramid structure:
/\ ← Few E2E Tests (High confidence, slow) / \ / \ ← Some Integration Tests (Medium confidence, medium speed) / \ /________\ ← Many Unit Tests (Fast, foundational)
Test Design Principles
AAA Pattern (Arrange-Act-Assert)
Structure every test in three clear phases:
// Arrange: Setup test data and conditions user = createTestUser() validator = createValidator() // Act: Execute the code under test result = validator.validate(user) // Assert: Verify expected outcome assert(result.isValid == true)
Adaptation: Apply this structure using your language's idioms (methods, functions, procedures)
One Assertion Per Concept
- •Test one behavior per test case
- •Multiple assertions OK if testing single concept
- •Split unrelated assertions into separate tests
Good:
test("validates user email format")
test("validates user age is positive")
test("validates required fields are present")
Bad:
test("validates user") // Tests everything at once
Descriptive Test Names
Test names should clearly describe:
- •What is being tested
- •Under what conditions
- •What the expected outcome is
Recommended format: "should [expected behavior] when [condition]"
Examples:
test("should return error when email is invalid")
test("should calculate discount when user is premium")
test("should throw exception when file not found")
Adaptation: Follow your project's naming convention (camelCase, snake_case, describe/it blocks)
Test Independence
Isolation Requirements
For detailed isolation criteria, see Test Independence Verification.
Setup and Teardown
- •Use setup hooks to prepare test environment
- •Use teardown hooks to clean up resources
- •Keep setup minimal and focused
- •Ensure teardown runs even if test fails
Mocking and Test Doubles
When to Use Mocks
- •Mock external dependencies: APIs, databases, file systems
- •Mock slow operations: Network calls, heavy computations
- •Mock unpredictable behavior: Random values, current time
- •Mock unavailable services: Third-party services
Mocking Principles
- •Mock at boundaries, not internally
- •Keep mocks simple and focused
- •Verify mock expectations when relevant
- •Don't mock external libraries/frameworks you don't control (prefer adapters)
Types of Test Doubles
- •Stub: Returns predetermined values
- •Mock: Verifies it was called correctly
- •Spy: Records information about calls
- •Fake: Simplified working implementation
- •Dummy: Passed but never used
Test Quality Practices
Keep Tests Active
- •Fix or delete failing tests: Resolve failures immediately
- •Remove commented-out tests: Fix them or delete entirely
- •Keep tests running: Broken tests lose value quickly
- •Maintain test suite: Refactor tests as needed
Test Code Quality
- •Apply same standards as production code
- •Use descriptive variable names
- •Extract test helpers to reduce duplication
- •Keep tests readable and maintainable
- •Review test code thoroughly
Test Helpers and Utilities
- •Create reusable test data builders
- •Extract common setup into helper functions
- •Build test utilities for complex scenarios
- •Share helpers across test files appropriately
What to Test
Focus on Behavior
Test observable behavior, not implementation:
✓ Good: Test that function returns expected output ✓ Good: Test that correct API endpoint is called ✗ Bad: Test that internal variable was set ✗ Bad: Test order of private method calls
Test Public APIs
- •Test through public interfaces
- •Avoid testing private methods directly
- •Test return values, outputs, exceptions
- •Test side effects (database, files, logs)
Test Edge Cases
Always test:
- •Boundary conditions: Min/max values, empty collections
- •Error cases: Invalid input, null values, missing data
- •Edge cases: Special characters, extreme values
- •Happy path: Normal, expected usage
Test Quality Criteria
These criteria ensure reliable, maintainable tests.
Literal Expected Values
- •Use hardcoded literal values in assertions
- •Calculate expected values independently from the implementation
- •If the implementation has a bug, the test catches it through independent verification
- •If expected value equals mock return value unchanged, the test verifies nothing (no transformation occurred)
Result-Based Verification
- •Verify final results and observable outcomes
- •Assert on return values, output data, or system state changes
- •For mock verification, check that correct arguments were passed
Meaningful Assertions
- •Every test must include at least one assertion
- •Assertions must validate observable behavior
- •A test without assertions always passes and provides no value
Appropriate Mock Scope
- •Mock direct external I/O dependencies: databases, HTTP clients, file systems
- •Use real implementations for internal utilities and business logic
- •Over-mocking reduces test value by verifying wiring instead of behavior
Boundary Value Testing
Test at boundaries of valid input ranges:
- •Minimum valid value
- •Maximum valid value
- •Just below minimum (invalid)
- •Just above maximum (invalid)
- •Empty input (where applicable)
Test Independence Verification
Each test must:
- •Create its own test data
- •Not depend on execution order
- •Clean up its own state
- •Pass when run in isolation
Verification Requirements
Before Commit
- •✓ All tests pass
- •✓ No tests skipped or commented
- •✓ No debug code left in tests
- •✓ Test coverage meets standards
- •✓ Tests run in reasonable time
Zero Tolerance Policy
- •Zero failing tests: Fix immediately
- •Zero skipped tests: Delete or fix
- •Zero flaky tests: Make deterministic
- •Zero slow tests: Optimize or split
Test Organization
File Structure
- •Mirror production structure: Tests follow code organization
- •Clear naming conventions: Follow project's test file patterns
- •Examples:
UserService.test.*,user_service_test.*,test_user_service.*,UserServiceTests.*
- •Examples:
- •Logical grouping: Group related tests together
- •Separate test types: Unit, integration, e2e in separate directories
Test Suite Organization
tests/ ├── unit/ # Fast, isolated unit tests ├── integration/ # Integration tests ├── e2e/ # End-to-end tests ├── fixtures/ # Test data and fixtures └── helpers/ # Shared test utilities
Performance Considerations
Test Speed
- •Unit tests: < 100ms each
- •Integration tests: < 1s each
- •Full suite: Should run frequently (< 10 minutes)
Optimization Strategies
- •Run tests in parallel when possible
- •Use in-memory databases for tests
- •Mock expensive operations
- •Split slow test suites
- •Profile and optimize slow tests
Continuous Integration
CI/CD Requirements
- •Run full test suite on every commit
- •Block merges if tests fail
- •Run tests in isolated environments
- •Test on target platforms/versions
Test Reports
- •Generate coverage reports
- •Track test execution time
- •Identify flaky tests
- •Monitor test trends
Common Anti-Patterns to Avoid
Test Smells
- •✗ Tests that test nothing (always pass)
- •✗ Tests that depend on execution order
- •✗ Tests that depend on external state
- •✗ Tests with complex logic (tests shouldn't need tests)
- •✗ Testing implementation details
- •✗ Excessive mocking (mocking everything)
- •✗ Test code duplication
Flaky Tests
Eliminate tests that fail intermittently:
- •Remove timing dependencies
- •Avoid random data in tests
- •Ensure proper cleanup
- •Fix race conditions
- •Make tests deterministic
Regression Testing
Prevent Regressions
- •Add test for every bug fix
- •Maintain comprehensive test suite
- •Run full suite regularly
- •Don't delete tests without good reason
Legacy Code
- •Add characterization tests before refactoring
- •Test existing behavior first
- •Gradually improve coverage
- •Refactor with confidence
Testing Best Practices by Language Paradigm
Type System Utilization
For languages with static type systems:
- •Leverage compile-time verification for correctness
- •Focus tests on business logic and runtime behavior
- •Use language's type system to prevent invalid states
For languages with dynamic typing:
- •Add comprehensive runtime validation tests
- •Explicitly test data contract validation
- •Consider property-based testing for broader coverage
Programming Paradigm Considerations
Functional approach:
- •Test pure functions thoroughly (deterministic, no side effects)
- •Test side effects at system boundaries
- •Leverage property-based testing for invariants
Object-oriented approach:
- •Test behavior through public interfaces
- •Mock dependencies via abstraction layers
- •Test polymorphic behavior carefully
Common principle: Adapt testing strategy to leverage language strengths while ensuring comprehensive coverage
Documentation and Communication
Tests as Documentation
- •Tests document expected behavior
- •Use clear, descriptive test names
- •Include examples of usage
- •Show edge cases and error handling
Test Failure Messages
- •Provide clear, actionable error messages
- •Include actual vs expected values
- •Add context about what was being tested
- •Make debugging easier