Testing guidelines
Philosophy
Tests are documentation. They should clearly communicate what the code does and serve as living documentation for future developers. Prioritize readability and maintainability over cleverness.
Test structure
Write tests as 3 separate blocks: Arrange, Act, Assert (also known as Given-When-Then).
# Ruby (RSpec)
it "returns the user full name" do
# Arrange
user = create(:user, first_name: "John", last_name: "Doe")
# Act
result = user.full_name
# Assert
expect(result).to eq("John Doe")
end# Python (pytest)
def test_returns_user_full_name():
# Arrange
user = User(first_name="John", last_name="Doe")
# Act
result = user.full_name()
# Assert
assert result == "John Doe"Do's
Write boring tests. The more boring a test is, the better.
Avoid abstractions unless there is a very good reason.
Repetition is acceptable. Copy & paste is allowed in tests.
Only extract to helper methods when it genuinely improves clarity.
Extracting large payloads or complex setup is fine.
Use human names like "John" instead of "Candidate 1" or "User A". It makes tests more readable and creates a narrative.
Test against hardcoded values, not dynamic references:
Mock every external call to the internet (HTTP, gRPC, external APIs). Use libraries like:
Ruby:
webmock,vcrPython:
responses,httpretty,pytest-httpserverTypeScript:
msw,nock
Test behavior, not implementation. Focus on what the code does, not how it does it internally.
Keep tests fast. Slow tests discourage running them frequently. Use unit tests for most coverage and reserve integration tests for critical paths.
One assertion per concept. It's fine to have multiple
expect/assertstatements if they verify the same logical concept.
Don'ts
Don't write "should" in test descriptions:
Don't use faker/random value generators (Faker, Chance, etc.) for most tests.
They can introduce flaky tests.
Hardcoded values help create a consistent story in the test suite.
Exception: Property-based testing / fuzz testing, where you intentionally run tests with random values to discover edge cases. Libraries:
hypothesis(Python),fast-check(TypeScript),rantly(Ruby).
Don't test more than one thing per unit test. Integration tests may verify multiple things since they are expensive to run.
Don't test private methods directly. Test them through the public interface.
Don't share state between tests. Each test should be independent and able to run in isolation.
Don't ignore flaky tests. Fix them immediately or delete them. A flaky test is worse than no test.
Test naming conventions
Use descriptive names that explain the scenario and expected outcome:
Test organization
Unit tests: Test individual functions/methods in isolation. Should be the majority of your tests.
Integration tests: Test how components work together (e.g., API endpoints, database interactions).
End-to-end tests: Test complete user flows. Use sparingly as they are slow and brittle.
Follow the testing pyramid: many unit tests, fewer integration tests, minimal E2E tests.
When testing capacity is limited: If a project's constraints (time, budget, team size) don't allow for comprehensive test coverage, prioritize E2E tests over unit tests. While slower and more brittle, E2E tests cover the most critical user paths with the fewest tests, providing maximum value per test written.
What to test
Do test: Business logic, edge cases, error handling, public APIs.
Don't test: Framework code, simple getters/setters, third-party libraries.
Database in tests
Use transactions to rollback after each test when possible.
Create only the data you need for each test.
Use factories/fixtures to simplify data creation:
Ruby:
factory_botPython:
factory_boy,pytest-factoryboyTypeScript:
fishery, custom factories
Continuous Integration
All tests must pass before merging.
Run the full test suite on every pull request.
Keep the CI pipeline fast (< 10 minutes ideally but it depends on the project size).
Last updated
