18 KiB
Test Performance Optimization
Last Updated: 2026-01-28
Status: ✅ Active optimization program
Executive Summary
This document provides a comprehensive overview of test performance optimizations, risk assessments, and future opportunities. The test suite execution time has been reduced through systematic analysis and targeted optimizations.
Current Performance Metrics
| Metric | Value |
|---|---|
Total Execution Time (without :slow tests) |
~614 seconds (~10.2 minutes) |
| Total Tests | 1,368 tests (+ 25 doctests) |
| Async Execution | 317.7 seconds |
| Sync Execution | 296.2 seconds |
| Slow Tests Excluded | 9 tests (tagged with @tag :slow) |
| Top 20 Slowest Tests | 81.2 seconds (13.2% of total time) |
Optimization Impact Summary
| Optimization | Tests Affected | Time Saved | Status |
|---|---|---|---|
| Seeds tests reduction | 13 → 4 tests | ~10-16s | ✅ Completed |
| Performance tests tagging | 9 tests | ~3-4s per run | ✅ Completed |
| Critical test query filtering | 1 test | ~8-10s | ✅ Completed |
| Total Saved | ~21-30s |
Completed Optimizations
1. Seeds Test Suite Optimization
Date: 2026-01-28
Status: ✅ Completed
What Changed
- Reduced test count: From 13 tests to 4 tests (69% reduction)
- Reduced seeds executions: From 8-10 times to 5 times per test run
- Execution time: From 24-30 seconds to 13-17 seconds
- Time saved: ~10-16 seconds per test run (40-50% faster)
Removed Tests (9 tests)
Tests were removed because their functionality is covered by domain-specific test suites:
"at least one member has no membership fee type assigned"→ Covered bymembership_fees/*_test.exs"each membership fee type has at least one member"→ Covered bymembership_fees/*_test.exs"members with fee types have cycles with various statuses"→ Covered bycycle_generator_test.exs"creates all 5 authorization roles with correct permission sets"→ Covered byauthorization/*_test.exs"all roles have valid permission_set_names"→ Covered byauthorization/permission_sets_test.exs"does not change role of users who already have a role"→ Merged into idempotency test"role creation is idempotent"(detailed) → Merged into general idempotency test
Retained Tests (4 tests)
Critical deployment requirements are still covered:
- ✅ Smoke Test: Seeds run successfully and create basic data
- ✅ Idempotency Test: Seeds can be run multiple times without duplicating data
- ✅ Admin Bootstrap: Admin user exists with Admin role (critical for initial access)
- ✅ System Role Bootstrap: Mitglied system role exists (critical for user registration)
Risk Assessment
| Removed Test Category | Alternative Coverage | Risk Level |
|---|---|---|
| Member/fee type distribution | membership_fees/*_test.exs |
⚠️ Low |
| Cycle status variations | cycle_generator_test.exs |
⚠️ Low |
| Detailed role configs | authorization/*_test.exs |
⚠️ Very Low |
| Permission set validation | permission_sets_test.exs |
⚠️ Very Low |
Overall Risk: ⚠️ Low - All removed tests have equivalent or better coverage in domain-specific test suites.
2. Performance Test Suite Separation
Date: 2026-01-28
Status: ✅ Completed
What Changed
Performance tests that explicitly validate performance characteristics are now tagged with @tag :slow or @moduletag :slow and excluded from standard test runs.
Identified Performance Tests (9 tests)
-
Member LiveView - Boolean Filter Performance (
test/mv_web/member_live/index_test.exs)- Test:
"boolean filter performance with 150 members" - Duration: ~3.8 seconds
- Creates 150 members to test filter performance
- Test:
-
Group LiveView - Index Performance (
test/mv_web/live/group_live/index_test.exs)- 2 tests validating efficient page loading and member count calculation
-
Group LiveView - Show Performance (
test/mv_web/live/group_live/show_test.exs)- 2 tests validating efficient member list loading and slug lookup
-
Member LiveView - Membership Fee Status Performance (
test/mv_web/member_live/index_membership_fee_status_test.exs)- 1 test validating efficient cycle loading without N+1 queries
-
Group Integration - Query Performance (
test/membership/group_integration_test.exs)- 1 test validating N+1 query prevention for group-member relationships
Execution Commands
Fast Tests (Default):
just test-fast
# or
mix test --exclude slow
Performance Tests Only:
just test-slow
# or
mix test --only slow
All Tests:
just test
# or
mix test
CI/CD Integration
- Standard CI: Runs
mix test --exclude slowfor faster feedback loops - Nightly Builds: Separate pipeline (
nightly-tests) runs daily at 2 AM and executes all performance tests - Manual Execution: Can be triggered via Drone CLI or Web UI
Risk Assessment
Risk Level: ✅ Very Low
- Performance tests are still executed, just separately
- Standard test runs are faster, improving developer feedback
- Nightly builds ensure comprehensive coverage
- No functionality is lost, only execution timing changed
3. Critical Test Optimization
Date: 2026-01-28
Status: ✅ Completed
Problem Identified
The test test respects show_in_overview config was the slowest test in the suite:
- Isolated execution: 4.8 seconds
- In full test run: 14.7 seconds
- Difference: 9.9 seconds (test isolation issue)
Root Cause
The test loaded all members from the database, not just the 2 members from the test setup. In full test runs, many members from other tests were present in the database, significantly slowing down the query.
Solution Implemented
Query Filtering: Added search query parameter to filter to only the expected member.
Code Change:
# Before:
{:ok, _view, html} = live(conn, "/members")
# After:
{:ok, _view, html} = live(conn, "/members?query=Alice")
Results
| Execution | Before | After | Improvement |
|---|---|---|---|
| Isolated | 4.8s | 1.1s | -77% (3.7s saved) |
| In Module | 4.2s | 0.4s | -90% (3.8s saved) |
| Expected in Full Run | 14.7s | ~4-6s | -65% to -73% (8-10s saved) |
Risk Assessment
Risk Level: ✅ Very Low
- Test functionality unchanged - only loads expected data
- All assertions still pass
- Test is now faster and more isolated
- No impact on test coverage
Current Performance Analysis
Top 20 Slowest Tests (without :slow)
| Rank | Test | File | Time | % of Top 20 |
|---|---|---|---|---|
| 1 | test respects show_in_overview config |
index_member_fields_display_test.exs |
~4-6s (optimized) | ~6-7% |
| 2 | test Seeds script is idempotent when run multiple times |
seeds_test.exs |
5.0s | 6.2% |
| 3 | test sorting functionality initially sorts by email ascending |
user_live/index_test.exs |
4.5s | 5.5% |
| 4 | test Seeds script runs successfully and creates basic data |
seeds_test.exs |
4.3s | 5.3% |
| 5-20 | Various User LiveView and Policy tests | Multiple files | 2.6-4.2s each | 3-5% each |
Total Top 20: ~81 seconds (13.2% of total time)
Performance Hotspots Identified
1. Seeds Tests (~16.2s for 4 tests)
Status: ✅ Optimized (reduced from 13 tests)
Remaining Optimization Potential: 3-5 seconds
Opportunities:
- Settings update could potentially be moved to
setup_all(if sandbox allows) - Seeds execution could be further optimized (less data in test mode)
- Idempotency test could be optimized (only 1x seeds instead of 2x)
2. User LiveView Tests (~35.5s for 10 tests)
Status: ⏳ Identified for optimization
Optimization Potential: 15-20 seconds
Files:
test/mv_web/user_live/index_test.exs(3 tests, ~10.2s)test/mv_web/user_live/form_test.exs(4 tests, ~15.0s)test/mv_web/user_live/show_test.exs(3 tests, ~10.3s)
Patterns:
- Many tests create user/member data
- LiveView mounts are expensive
- Form submissions with validations are slow
Recommended Actions:
- Move shared fixtures to
setup_all - Reduce test data volume (3-5 users instead of 10+)
- Optimize setup patterns for recurring patterns
3. Policy Tests (~8.7s for 3 tests)
Status: ⏳ Identified for optimization
Optimization Potential: 5-8 seconds
Files:
test/mv/membership/member_policies_test.exs(2 tests, ~6.1s)test/mv/accounts/user_policies_test.exs(1 test, ~2.6s)
Pattern:
- Each test creates new roles/users/members
- Roles are identical across tests
Recommended Actions:
- Create roles in
setup_all(shared across tests) - Reuse common fixtures
- Maintain test isolation while optimizing setup
Future Optimization Opportunities
Priority 1: User LiveView Tests Optimization
Estimated Savings: 15-20 seconds
Actions:
- Move shared fixtures to
setup_allwhere possible - Reduce test data volume (use 3-5 users instead of 10+)
- Analyze and optimize recurring setup patterns
- Consider mocking expensive operations (if appropriate)
Risk Assessment: ⚠️ Low
- Requires careful testing to ensure isolation is maintained
- Should verify that shared fixtures don't cause test interdependencies
Priority 2: Policy Tests Optimization
Estimated Savings: 5-8 seconds
Actions:
- Create roles in
setup_all(they're identical across tests) - Reuse common fixtures (users, members)
- Maintain test isolation while optimizing setup
Risk Assessment: ⚠️ Low
- Roles are read-only in tests, safe to share
- Need to ensure user/member fixtures don't interfere with each other
Priority 3: Seeds Tests Further Optimization
Estimated Savings: 3-5 seconds
Actions:
- Investigate if settings update can be moved to
setup_all - Introduce seeds mode for tests (less data in test mode)
- Optimize idempotency test (only 1x seeds instead of 2x)
Risk Assessment: ⚠️ Low to Medium
- Sandbox limitations may prevent
setup_allusage - Seeds mode would require careful implementation
- Idempotency test optimization needs to maintain test validity
Priority 4: Additional Test Isolation Improvements
Estimated Savings: Variable (depends on specific tests)
Actions:
- Review tests that load all records (similar to the critical test fix)
- Add query filters where appropriate
- Ensure proper test isolation in async tests
Risk Assessment: ⚠️ Very Low
- Similar to the critical test optimization (proven approach)
- Improves test isolation and reliability
Estimated Total Optimization Potential
| Priority | Optimization | Estimated Savings |
|---|---|---|
| 1 | User LiveView Tests | 15-20s |
| 2 | Policy Tests | 5-8s |
| 3 | Seeds Tests Further | 3-5s |
| 4 | Additional Isolation | Variable |
| Total Potential | 23-33 seconds |
Projected Final Time: From ~614 seconds to ~580-590 seconds (~9.5-10 minutes)
Risk Assessment Summary
Overall Risk Level: ⚠️ Low
All optimizations maintain test coverage while improving performance:
| Optimization | Risk Level | Mitigation |
|---|---|---|
| Seeds tests reduction | ⚠️ Low | Coverage mapped to domain tests |
| Performance tests tagging | ✅ Very Low | Tests still executed, just separately |
| Critical test optimization | ✅ Very Low | Functionality unchanged, better isolation |
| Future optimizations | ⚠️ Low | Careful implementation with verification |
Monitoring Plan
Success Criteria
- ✅ Seeds tests execute in <20 seconds consistently
- ✅ No increase in seeds-related deployment failures
- ✅ No regression in authorization or membership fee bugs
- ⏳ Top 20 slowest tests: < 60 seconds (currently 81.2s)
- ⏳ Total execution time (without
:slow): < 10 minutes (currently 10.2 min)
What to Watch For
-
Production Seeds Failures:
- Monitor deployment logs for seeds errors
- If failures increase, consider restoring detailed tests
-
Authorization Bugs After Seeds Changes:
- If role/permission bugs appear after seeds modifications
- May indicate need for more seeds-specific role validation
-
Test Performance Regression:
- Monitor test execution times in CI
- Alert if times increase significantly
-
Developer Feedback:
- If developers report missing test coverage
- Adjust based on real-world experience
Benchmarking and Analysis
How to Benchmark Tests
ExUnit Built-in Benchmarking:
The test suite is configured to show the slowest tests automatically:
# test/test_helper.exs
ExUnit.start(
slowest: 10 # Shows 10 slowest tests at the end of test run
)
Run Benchmark Analysis:
# Show slowest tests
mix test --slowest 20
# Exclude slow tests for faster feedback
mix test --exclude slow --slowest 20
# Run only slow tests
mix test --only slow --slowest 10
# Benchmark specific test file
mix test test/mv_web/member_live/index_member_fields_display_test.exs --slowest 5
Benchmarking Best Practices
- Run benchmarks regularly (e.g., monthly) to catch performance regressions
- Compare isolated vs. full runs to identify test isolation issues
- Monitor CI execution times to track trends over time
- Document significant changes in test performance
Test Suite Structure
Test Execution Modes
Fast Tests (Default):
- Excludes performance tests (
@tag :slow) - Used for standard development workflow
- Command:
mix test --exclude sloworjust test-fast
Performance Tests:
- Explicitly tagged performance tests
- Run separately or in nightly builds
- Command:
mix test --only sloworjust test-slow
All Tests:
- Includes both fast and slow tests
- Used for comprehensive validation
- Command:
mix testorjust test
Test Organization
Tests are organized to mirror the lib/ directory structure:
test/
├── accounts/ # Accounts domain tests
├── membership/ # Membership domain tests
├── membership_fees/ # Membership fees domain tests
├── mv/ # Core application tests
│ ├── accounts/ # User-related tests
│ ├── membership/ # Member-related tests
│ └── authorization/ # Authorization tests
├── mv_web/ # Web layer tests
│ ├── controllers/ # Controller tests
│ ├── live/ # LiveView tests
│ └── components/ # Component tests
└── support/ # Test helpers
├── conn_case.ex # Controller test setup
└── data_case.ex # Database test setup
Best Practices for Test Performance
When Writing New Tests
- Use
async: truewhen possible (for parallel execution) - Filter queries to only load necessary data
- Share fixtures in
setup_allwhen appropriate - Tag performance tests with
@tag :slowif they use large datasets - Keep test data minimal - only create what's needed for the test
When Optimizing Existing Tests
- Measure first - Use
mix test --slowestto identify bottlenecks - Compare isolated vs. full runs - Identify test isolation issues
- Optimize setup - Move shared data to
setup_allwhere possible - Filter queries - Only load data needed for the test
- Verify coverage - Ensure optimizations don't reduce test coverage
Performance Test Guidelines
Tag as :slow when:
- Explicitly testing performance characteristics
- Using large datasets (50+ records)
- Testing scalability or query optimization
- Validating N+1 query prevention
Do NOT tag as :slow when:
- Test is slow due to inefficient setup (fix the setup instead)
- Test is slow due to bugs (fix the bug instead)
- It's an integration test (use
@tag :integrationinstead)
Changelog
2026-01-28: Initial Optimization Phase
Completed:
- ✅ Reduced seeds tests from 13 to 4 tests
- ✅ Tagged 9 performance tests with
@tag :slow - ✅ Optimized critical test with query filtering
- ✅ Created slow test suite infrastructure
- ✅ Updated CI/CD to exclude slow tests from standard runs
- ✅ Added nightly CI pipeline for slow tests
Time Saved: ~21-30 seconds per test run
Next Steps:
- ⏳ Optimize User LiveView tests (Priority 1)
- ⏳ Optimize Policy tests (Priority 2)
- ⏳ Further optimize Seeds tests (Priority 3)
References
- Testing Standards:
CODE_GUIDELINES.md- Section 4 (Testing Standards) - CI/CD Configuration:
.drone.yml - Test Helper:
test/test_helper.exs - Justfile Commands:
Justfile(test-fast, test-slow, test-all)
Questions & Answers
Q: What if seeds create wrong data and break the system?
A: The smoke test will fail if seeds raise errors. Domain tests ensure business logic is correct regardless of seeds content.
Q: What if we add a new critical bootstrap requirement?
A: Add a new test to the "Critical bootstrap invariants" section in test/seeds_test.exs.
Q: How do we know the removed tests aren't needed?
A: Monitor for 2-3 months. If no seeds-related bugs appear that would have been caught by removed tests, they were redundant.
Q: Should we restore the tests for important releases?
A: Consider running the full test suite (including slow tests) before major releases. Daily development uses the optimized suite.
Q: How do I add a new performance test?
A: Tag it with @tag :slow or @moduletag :slow. See "Performance Test Guidelines" section above.
Q: Can I run slow tests locally?
A: Yes, use just test-slow or mix test --only slow. They're excluded from standard runs for faster feedback.