Skip to content

Testing Strategy

DiveSuite is life-critical software. Our testing strategy reflects this: the deco engine requires significantly more rigorous testing than UI components.

/ E2E \ <- Few: critical user flows only
/---------\
/Integration \ <- Medium: DB, WASM bridge, services
/---------------\
/ Unit Tests \ <- Many: pure logic, calculations
/-------------------\
Code AreaTargetRationale
Deco Engine (Rust)95%+ branchLife-critical calculations
Core Services (TS)80%+ lineBusiness logic
UI ComponentsNo targetTest behavior, not rendering
Terminal window
# Run all tests
npm test
# With coverage
npm test -- --coverage
# Watch mode
npm test -- --watch
# Specific file
npm test -- dive-plan.test.ts
Terminal window
cd rust-engine
# Run all tests
cargo test
# With output
cargo test -- --nocapture
# Specific test
cargo test test_calculate_ndl

The deco engine is life-safety software. Testing must be significantly more rigorous.

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());
}
}

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);
}
}

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);
}
}

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);
}

Test pure functions and business logic:

gas-calculations.test.ts
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();
});
});

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();
});
});

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);
});
});

Use Detox or Maestro for critical user flows only:

e2e/plan-dive.test.ts
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();
});
});

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([]);
});
});

Co-locate tests with source:

src/features/planning/hooks/
├── useDivePlan.ts
├── useDivePlan.test.ts # Unit tests
├── usePlanValidation.ts
└── usePlanValidation.test.ts

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