Skip to content

Performance Best Practices

When to Use

Writing performance-critical code, optimizing slow tests, or ensuring your test suite runs fast enough to be run frequently.

Test Suite Performance

The 10-Second Rule: Unit test suite should run in under 10 seconds for rapid feedback.

Why Speed Matters: - Developers won't run slow tests frequently - Slow tests break TDD flow (red-green-refactor cycle measured in minutes) - CI/CD pipelines bottleneck on slow tests

Speed Targets: - Unit test: < 10ms each - Integration test: < 100ms each - E2E test: < 5s each - Full suite: < 2 minutes (for CI)

Making Tests Fast

Mock External Dependencies

# SLOW: Real HTTP call (200-1000ms)
def test_fetch_user_data():
    api = UserAPI()
    user = api.get_user(123)  # Real HTTP request
    assert user.name == 'Alice'

# FAST: Mocked HTTP (< 1ms)
def test_fetch_user_data():
    api = UserAPI()
    api.http_client = Mock()
    api.http_client.get.return_value = {'id': 123, 'name': 'Alice'}

    user = api.get_user(123)
    assert user.name == 'Alice'

Use In-Memory Databases

// SLOW: Real PostgreSQL (50-200ms setup per test)
beforeEach(async () => {
  db = new PostgreSQL('localhost:5432');
  await db.connect();
  await db.migrate();
});

// FAST: In-memory SQLite (5-10ms setup per test)
beforeEach(() => {
  db = new SQLite(':memory:');
  db.migrate();
});

Avoid Unnecessary I/O

// SLOW: Writing to disk
@Test
public void testFileProcessing() {
    File tempFile = new File("/tmp/test.txt");
    writeToFile(tempFile, "test data");

    String result = processor.process(tempFile);

    assertEquals("expected", result);
    tempFile.delete();
}

// FAST: In-memory
@Test
public void testFileProcessing() {
    InputStream input = new ByteArrayInputStream("test data".getBytes());

    String result = processor.process(input);

    assertEquals("expected", result);
}

Parallelize Tests

# Python: pytest with xdist
pytest -n auto  # Auto-detect CPU cores

# JavaScript: Jest runs parallel by default
jest --maxWorkers=4

# Go: Tests parallel by default
go test -parallel 4

# Ruby: RSpec with parallel_tests gem
parallel_rspec spec/

Performance Testing Strategy

Don't Performance Test in Unit Tests: Unit tests verify correctness, not speed.

Use Dedicated Performance Tests:

# Separate performance test suite
import pytest
from time import perf_counter

@pytest.mark.performance
def test_search_performance():
    database = create_database_with_million_records()

    start = perf_counter()
    results = database.search(query='Alice')
    duration = perf_counter() - start

    assert len(results) > 0  # Correctness
    assert duration < 0.1  # Performance threshold: < 100ms

Load Testing: Separate from unit tests

# Use locust, k6, or JMeter for load testing
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)

    @task
    def search_products(self):
        self.client.get("/api/products?q=laptop")

    @task(3)  # 3x more frequent
    def view_product(self):
        self.client.get("/api/products/123")

# Run: locust -f locustfile.py --users 100 --spawn-rate 10

Identifying Performance Bottlenecks

Profile Test Suite

# Python: pytest with profiling
pytest --profile

# JavaScript: Jest with --logHeapUsage
jest --logHeapUsage

# Identify slowest tests
pytest --durations=10  # Show 10 slowest tests

Common Bottlenecks: - Database queries (N+1 problem) - File I/O - Network calls - Complex setup/teardown - Inefficient algorithms

Pattern: Performance-Critical Code Testing

// Use benchmarks (separate from tests) for performance-critical code
func BenchmarkSearch(b *testing.B) {
    db := setupTestDatabase()

    b.ResetTimer()  // Don't include setup time
    for i := 0; i < b.N; i++ {
        db.Search("query")
    }
}

// Run: go test -bench=. -benchmem
// Output: BenchmarkSearch-8  100000  11234 ns/op  2048 B/op  15 allocs/op

// Test correctness separately
func TestSearch(t *testing.T) {
    db := setupTestDatabase()
    results := db.Search("query")

    assert.NotEmpty(t, results)
    assert.Equal(t, "expected", results[0].Name)
}

When to Optimize

Optimize test suite when: - Developers skip running tests because they're slow - CI pipeline is bottlenecked on tests - Individual test takes > 100ms (for unit tests)

Don't optimize prematurely: - Test suite under 30 seconds - Good enough for most projects - Optimization adds complexity - Only optimize if speed is actually a problem

Common Mistakes

  • Optimizing application code without tests - Optimization often breaks behavior; test first, then optimize
  • Sharing state between tests for speed - Causes flaky tests; better to parallelize independent tests
  • Skipping slow tests instead of fixing them - They'll never run, defeating the purpose
  • Testing performance in unit tests - Unreliable (CPU load, other processes); use dedicated benchmarks
  • Not profiling before optimizing - Profile to find actual bottlenecks, don't guess
  • Caching test data between runs - Tests become non-repeatable; each test should set up own data

See Also