Testing Strategy
DiveSuite is life-critical software. Our testing strategy reflects this: the deco engine requires significantly more rigorous testing than UI components.
Test Pyramid
Section titled “Test Pyramid” / E2E \ <- Few: critical user flows only /---------\ /Integration \ <- Medium: DB, WASM bridge, services /---------------\ / Unit Tests \ <- Many: pure logic, calculations /-------------------\Coverage Targets
Section titled “Coverage Targets”| Code Area | Target | Rationale |
|---|---|---|
| Deco Engine (Rust) | 95%+ branch | Life-critical calculations |
| Core Services (TS) | 80%+ line | Business logic |
| UI Components | No target | Test behavior, not rendering |
Running Tests
Section titled “Running Tests”TypeScript Tests
Section titled “TypeScript Tests”# Run all testsnpm test
# With coveragenpm test -- --coverage
# Watch modenpm test -- --watch
# Specific filenpm test -- dive-plan.test.tsRust Tests
Section titled “Rust Tests”cd rust-engine
# Run all testscargo test
# With outputcargo test -- --nocapture
# Specific testcargo test test_calculate_ndlDeco Engine Testing (Critical)
Section titled “Deco Engine Testing (Critical)”The deco engine is life-safety software. Testing must be significantly more rigorous.
Unit Tests
Section titled “Unit Tests”Every calculation function needs comprehensive unit tests:
#[cfg(test)]mod tests { use super::*;
#[test] fn test_ndl_air_18m() { let result = calculate_ndl(18.0, &GasMix::air(), 85); assert_eq!(result.unwrap(), 56); }
#[test] fn test_ndl_ean32_30m() { let result = calculate_ndl(30.0, &GasMix::nitrox(32), 85); assert_eq!(result.unwrap(), 20); }
#[test] fn test_ndl_invalid_depth() { let result = calculate_ndl(-5.0, &GasMix::air(), 85); assert!(result.is_err()); }}Property-Based Tests
Section titled “Property-Based Tests”Use proptest to verify invariants hold for random inputs:
use proptest::prelude::*;
proptest! { #[test] fn ndl_decreases_with_depth(depth in 1.0f64..100.0) { let shallow = calculate_ndl(depth, &GasMix::air(), 85).unwrap(); let deep = calculate_ndl(depth + 3.0, &GasMix::air(), 85).unwrap(); prop_assert!(deep <= shallow); }
#[test] fn gas_consumption_always_positive( depth in 1.0f64..100.0, time in 1u32..120, sac in 10.0f64..30.0 ) { let consumption = calculate_gas_consumption(depth, time, sac); prop_assert!(consumption > 0.0); }
#[test] fn no_fly_time_never_negative( tissue_loading in prop::collection::vec(0.0f64..3.0, 16) ) { let no_fly = calculate_no_fly_time(&tissue_loading); prop_assert!(no_fly >= 0); }}Validation Tests
Section titled “Validation Tests”Cross-reference against known-good implementations:
/// Test vectors from Subsurface (same algorithm)#[test]fn validate_against_subsurface() { let test_cases = [ // (depth, gas, gf_high, expected_ndl) (12.0, GasMix::air(), 85, 147), (18.0, GasMix::air(), 85, 56), (24.0, GasMix::air(), 85, 29), (30.0, GasMix::air(), 85, 20), (30.0, GasMix::nitrox(32), 85, 30), ];
for (depth, gas, gf, expected) in test_cases { let result = calculate_ndl(depth, &gas, gf).unwrap(); assert_eq!(result, expected, "NDL mismatch at {}m with {:?}", depth, gas); }}Regression Tests
Section titled “Regression Tests”Every bug fix gets a test:
/// Regression test for issue #47: NDL overflow at extreme depths#[test]fn regression_47_extreme_depth_overflow() { // Previously caused integer overflow let result = calculate_ndl(120.0, &GasMix::trimix(18, 45), 30); assert!(result.is_ok()); assert!(result.unwrap() < 1000);}TypeScript Testing
Section titled “TypeScript Testing”Unit Tests
Section titled “Unit Tests”Test pure functions and business logic:
import { calculateMOD, calculateEND, calculateEAD } from './gas-calculations';
describe('calculateMOD', () => { it('returns correct MOD for EAN32 at 1.4 ppO2', () => { expect(calculateMOD({ o2: 0.32 }, 1.4)).toBeCloseTo(33.75); });
it('throws for invalid gas mix', () => { expect(() => calculateMOD({ o2: 0 }, 1.4)).toThrow(); });});Hook Tests
Section titled “Hook Tests”Test custom hooks with @testing-library/react-hooks:
import { renderHook, act } from '@testing-library/react-hooks';import { useDivePlan } from './useDivePlan';
describe('useDivePlan', () => { it('initializes with default values', () => { const { result } = renderHook(() => useDivePlan()); expect(result.current.depth).toBe(18); expect(result.current.duration).toBe(45); });
it('validates depth range', () => { const { result } = renderHook(() => useDivePlan()); act(() => { result.current.setDepth(150); }); expect(result.current.errors.depth).toBeDefined(); });});Integration Tests
Section titled “Integration Tests”Test service layer end-to-end:
import { PlanningService } from './planning-service';import { createTestDatabase } from '@core/database/test-utils';
describe('PlanningService', () => { let service: PlanningService; let db: Database;
beforeEach(async () => { db = await createTestDatabase(); service = new PlanningService(db); });
afterEach(async () => { await db.close(); });
it('creates and saves a dive plan', async () => { const plan = await service.createPlan({ depth: 30, duration: 25, gasMix: { o2: 0.32, he: 0, n2: 0.68 }, });
expect(plan.ndl).toBeGreaterThan(0); expect(plan.id).toBeDefined();
const saved = await service.getPlan(plan.id); expect(saved).toEqual(plan); });});E2E Tests
Section titled “E2E Tests”Use Detox or Maestro for critical user flows only:
describe('Plan a dive flow', () => { beforeAll(async () => { await device.launchApp(); });
it('can plan a recreational dive', async () => { // Navigate to planning await element(by.id('tab-plan')).tap();
// Enter parameters await element(by.id('depth-input')).typeText('25'); await element(by.id('duration-input')).typeText('40');
// Calculate await element(by.id('calculate-button')).tap();
// Verify result await expect(element(by.id('ndl-result'))).toBeVisible(); await expect(element(by.id('safety-disclaimer'))).toBeVisible(); });});i18n Tests
Section titled “i18n Tests”Verify all translation keys exist:
import en from '@core/i18n/locales/en.json';import de from '@core/i18n/locales/de.json';
describe('i18n completeness', () => { const getAllKeys = (obj: object, prefix = ''): string[] => { return Object.entries(obj).flatMap(([key, value]) => { const fullKey = prefix ? `${prefix}.${key}` : key; return typeof value === 'object' ? getAllKeys(value, fullKey) : [fullKey]; }); };
it('has all English keys in German', () => { const enKeys = getAllKeys(en); const deKeys = getAllKeys(de); const missing = enKeys.filter(k => !deKeys.includes(k)); expect(missing).toEqual([]); });});Test Organization
Section titled “Test Organization”Co-locate tests with source:
src/features/planning/hooks/├── useDivePlan.ts├── useDivePlan.test.ts # Unit tests├── usePlanValidation.ts└── usePlanValidation.test.tsCI Integration
Section titled “CI Integration”All tests run on every PR:
jobs: test-typescript: steps: - run: npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3
test-rust: steps: - run: cargo test - run: cargo tarpaulin --out Xml # Coverage