Files
iban-ex/docs/test_coverage_improvement_plan.md
2025-11-29 21:20:32 -05:00

45 KiB

IbanEx Test Coverage Improvement Plan

Comprehensive Merged Analysis

Executive Summary

This comprehensive test coverage improvement plan consolidates insights from detailed codebase analysis to identify critical testing gaps in the IbanEx library. The analysis reveals that while basic validation and parsing have initial coverage through doctests and happy-path tests, critical security and reliability functions remain completely untested, including error handling, data serialization, and negative test cases.

Key Findings

  • Current Coverage: ~40-50% (estimated)
  • Critical Risk: Core validation functions (violations/1, validate/1, checksum verification) are untested
  • Data Safety Gap: Serialization/deserialization protocols can silently accept malformed data
  • Missing Safety Nets: No negative tests, edge cases, or malformed input handling verification

Strategic Approach

  • 4-Phase Implementation: Prioritized by risk (Critical → High → Medium → Polish)
  • Expected Outcome: 90%+ coverage with comprehensive safety nets
  • Timeline: 8 weeks to complete all phases

1. Current Test Coverage Analysis

1.1 Existing Test Files

test/
├── iban_ex_test.exs           # Doctest aggregator (70+ countries)
├── iban_ex_parser_test.exs    # Parser validation (2 test cases)
├── iban_ex_validator_test.exs # Validator tests (6 test cases)
└── test_helper.exs             # Test configuration

1.2 Current Strengths

  • Basic IBAN validation for all 70+ supported countries
  • Parser functionality for valid and invalid country codes
  • Some BBAN component validation (account number, bank code, branch code, national check)
  • Length validation for 5 countries
  • Doctests ensure per-country formatting examples compile

1.3 Critical Weaknesses

Completely Untested Modules (0% Coverage)

  1. IbanEx.Validator.violations/1 - Core aggregation function
  2. IbanEx.Validator.validate/1 - Primary validation API
  3. IbanEx.Validator.iban_violates_checksum?/1 - Security-critical function
  4. IbanEx.Deserialize protocol - All implementations (String, Map, List)
  5. IbanEx.Serialize module - to_string/1 and to_map/1
  6. IbanEx.Formatter module - All formatting functions
  7. IbanEx.Iban struct - Delegates and structure
  8. IbanEx.Commons - Normalization utilities
  9. IbanEx.Error - Error message generation
  10. IbanEx.Country - Discovery and module lookup functions

Partially Tested (Only Happy Paths)

  1. IbanEx.Parser - Missing: incomplete: true, parse_bban/3, edge cases
  2. IbanEx.Validator - Missing: negative tests, all error branches
  3. Country modules - Only indirect doctest coverage

2. High-Risk Gaps (Critical Priority)

2.1 Validator Pipeline (lib/iban_ex/validator/validator.ex)

Risk Level: 🔴 CRITICAL - Primary security and data validation surface

Untested Functions

# Public API - COMPLETELY UNTESTED
validate/1              # Returns {:ok, iban} | {:error, reason}
violations/1            # Returns list of all violations

# Internal Guards - NO NEGATIVE TESTS
iban_violates_format?/1          # Format validation
iban_unsupported_country?/1      # Country code validation
iban_violates_length?/1          # Length validation
iban_violates_country_rule?/1    # BBAN structure validation
iban_violates_checksum?/1        # 🔐 SECURITY CRITICAL - mod 97 check

# BBAN Component Validators
check_iban_bank_code/1           # Only 5 countries tested
check_iban_account_number/1      # Only 5 countries tested
check_iban_branch_code/1         # Only 5 countries tested
check_iban_national_check/1      # Only 5 countries tested

Required Test Coverage

  1. violations/1 Tests:

    • Empty list for valid IBANs
    • All violations for completely invalid input
    • Specific violations for targeted errors (checksum only, length only, etc.)
    • Multiple simultaneous violations
    • Deterministic ordering of violation list
  2. validate/1 Tests:

    • All error tuple types: :invalid_format, :unsupported_country_code, :invalid_length, :invalid_format_for_country, :invalid_bank_code, :invalid_account_number, :invalid_branch_code, :invalid_national_check, :invalid_checksum
    • Success case returns correct {:ok, %Iban{}} structure
  3. Checksum Validation (iban_violates_checksum?/1):

    • Valid checksums (02-96, 98)
    • Invalid by algorithm (01, 00, 97, 99)
    • All-numeric BBANs
    • All-alphabetic BBANs
    • Mixed alphanumeric BBANs
    • Edge case countries (QA with heavy letter usage)
  4. Replacements Module (lib/iban_ex/validator/replacements.ex):

    • Verify replace/1 letter-to-number mapping
    • Confirm complete A-Z coverage
    • Validate fallback behavior

2.2 Parser Module (lib/iban_ex/parser.ex)

Risk Level: 🔴 CRITICAL - Entry point for all IBAN data

Untested Functions & Branches

parse/2                 # Missing: incomplete: true option
parse_bban/3           # Completely untested
country_code/1         # Missing: edge cases
check_digits/1         # Missing: edge cases
bban/1                 # Missing: edge cases

Required Test Coverage

  1. parse/2 Edge Cases:

    # Input validation
    - Empty strings
    - Nil values
    - Strings with only country code
    - Strings with country + check digits but no BBAN
    - Special characters (!@#$%^&*)
    - Extremely long strings (>1000 chars)
    - Non-ASCII / Unicode characters
    - Emoji in input
    
    # incomplete: true option
    - Partial IBAN strings
    - IBANs missing BBAN components
    - Malformed partial data
    
  2. parse_bban/3 Tests:

    • Empty BBAN strings
    • BBAN with invalid characters
    • BBAN length mismatches
    • Both incomplete: false (regex) and incomplete: true (rules) modes
    • Fallback to %{} when regex captures fail
  3. Helper Functions:

    • country_code/1: strings too short, lowercase, with whitespace
    • check_digits/1: non-numeric, single digit, letters
    • bban/1: strings shorter than 4 characters

2.3 Data Serialization & Deserialization

Risk Level: 🔴 CRITICAL - Silent data corruption risk

Deserialize Protocol (lib/iban_ex/deserialize.ex)

0% Coverage - All implementations untested:

# String/BitString implementation
- Valid IBAN strings
- Invalid IBAN strings
- Empty strings
- Nil handling
- Error tuple propagation

# Map implementation (atom keys)
- Valid maps with all required fields
- Maps with optional fields as nil
- Maps missing required fields (:country_code, :check_digits, etc.)
- Maps with extra fields
- Invalid field values

# Map implementation (string keys)
- Valid maps with string keys
- Partial maps
- Mixed atom/string keys

# Keyword List implementation
- Valid keyword lists
- Keyword lists with optional fields
- Empty lists
- Lists with invalid data

# Error handling
- {:can_not_parse_map, _} error cases
- Protocol not implemented for other types

Serialize Module (lib/iban_ex/serialize.ex)

0% Coverage:

# to_string/1
- Serialize valid IBANs
- Serialize IBANs with optional fields (nil branch_code, nil national_check)
- Output format verification (compact representation)

# to_map/1
- Convert Iban struct to map
- Verify all fields present
- Verify correct field types
- Handle nil optional fields (branch_code, national_check)

# Round-trip tests
- struct -> map -> struct preservation
- to_string/1 output can be parsed back

2.4 Formatter & User-Facing APIs

Risk Level: 🟡 HIGH - User experience and data presentation

Formatter Module (lib/iban_ex/formatter.ex)

0% Coverage:

# available_formats/0
- Returns [:compact, :pretty, :splitted]
- Complete list validation

# format/2 and convenience functions
- compact/1: removes all spacing
- pretty/1: country-specific formatting rules
- splitted/1: 4-character block grouping
- format/2: all three format types
- Default format behavior

# Edge cases
- IBANs with nil optional fields
- Shortest IBAN (Norway: 15 chars)
- Longest IBAN (Malta: 31 chars)
- Round-trip: parse -> format -> parse preservation

Iban Struct (lib/iban_ex/iban.ex)

0% Coverage:

# Struct validation
- Valid struct creation with all fields
- Struct with optional fields as nil
- Field types and defaults

# Delegated functions
- to_map/1 delegation to Serialize
- to_string/1 delegation to Serialize
- pretty/1, splitted/1, compact/1 delegations to Formatter

# Pattern matching
- Struct field access
- Pattern matching on struct

2.5 Country Modules Infrastructure

Risk Level: 🟡 HIGH - Foundation for all validations

Country Module (lib/iban_ex/country.ex)

Minimal Coverage (doctests only):

# Discovery functions
supported_country_codes/0    # Returns all 70+ codes
supported_country_modules/0  # Returns all module atoms
country_module/1             # Lookup by code
is_country_code_supported?/1 # Boolean check

# Test requirements
- All functions with valid inputs
- Invalid country codes
- Case insensitivity (DE, de, De, dE)
- Atom vs string inputs
- Edge cases: nil, empty string, numbers, special chars

Country Implementation Modules (70+ files)

Indirect Coverage Only (via doctests):

Each module in lib/iban_ex/country/*.ex needs explicit tests:

# Per-country validation
- size/0 returns correct length
- rule/0 returns valid regex
- rules/0 returns all BBAN part rules
- rules_map/0 field mappings are correct
- to_string/1 formats correctly
- BBAN structure validation
- Optional fields handling (branch_code, national_check)

# Shared test suite approach
- Iterate over Country.supported_country_modules/0
- Assert size/0 == regex length + 4
- Assert rules/0 and rules_map/0 produce contiguous ranges
- Assert ranges cover bban_size/0
- Negative tests: malformed BBANs per region

High-priority countries for explicit tests:

  • DE (Germany) - Most common
  • FR (France) - Complex format
  • GB (United Kingdom) - Custom to_string/1 override
  • IT (Italy) - Has branch code and national check
  • BR (Brazil) - Different character set
  • NO (Norway) - Shortest IBAN (15 chars)
  • MT (Malta) - Longest IBAN (31 chars)

2.6 Utility Modules

Risk Level: 🟡 HIGH - Shared dependencies

Commons Module (lib/iban_ex/commons/commons.ex)

0% Coverage:

# blank/1
- Empty strings
- Whitespace-only strings
- Nil values
- Non-empty strings

# normalize/1
- Removes spaces and hyphens
- Converts to uppercase
- Handles nil
- Handles already normalized strings
- Unicode handling

# normalize_and_slice/2
- Correct slicing after normalization
- Edge cases with ranges
- Empty results
- Out-of-bounds ranges
- Negative indices

Error Module (lib/iban_ex/error.ex)

0% Coverage:

# message/1
- All declared error atoms return messages
- Messages are human-readable
- Unknown atoms fall back to "Undefined error"
- Consistent error message format

3. Integration & End-to-End Testing Gaps

3.1 End-to-End Workflows

Currently Missing:

# Complete IBAN lifecycle
String -> Parse -> Validate -> Format -> String (round-trip)
Map -> Deserialize -> Serialize -> Map (round-trip)
Invalid input -> Error handling -> Error messages

# Cross-module integration
Parser + Validator integration
Formatter + Parser integration
Deserialize + Serialize integration
Country module + Validator integration

3.2 Real-World Scenarios

Banking Use Cases:

# Bulk operations
- Validate 1000+ IBANs (performance)
- IBAN conversion between formats in batch
- Database serialization/deserialization patterns

# API integration
- JSON request/response handling (via Deserialize protocol)
- Multi-country IBAN processing
- Error propagation through application layers

# Error scenarios
- Graceful degradation with partial data
- Error recovery strategies
- User-friendly error messages

4. Edge Cases & Boundary Testing

4.1 Input Validation Edge Cases

# Null and empty handling
- nil inputs to all public functions
- Empty strings ("")
- Whitespace-only strings ("   ", "\t", "\n")

# Character encoding
- UTF-8 edge cases
- Non-ASCII characters (ñ, ö, ü, etc.)
- Emoji in IBANs (should fail gracefully: 💰, 🏦)
- Different whitespace characters (\t, \n, \r, non-breaking space)

# Size boundaries
- Minimum valid IBAN length (15 chars - Norway)
- Maximum valid IBAN length (34 chars - Malta theoretical max, 31 practical)
- Off-by-one errors (length ± 1)
- Extremely short (<15)
- Extremely long (>34)

# Type boundaries
- Integer inputs (should fail with proper errors)
- Float inputs
- Boolean inputs
- List inputs (where not expected)
- Atom inputs (where not expected)
- Tuple inputs

4.2 Country-Specific Edge Cases

# Special characteristics
- NO (Norway): Shortest IBAN (15 chars)
- MT (Malta): Longest IBAN (31 chars)
- BR (Brazil): Uses letters in account number
- Countries with optional fields (branch_code, national_check)

# BBAN part validation per country
- Bank code format and ranges
- Account number format and ranges
- Branch code presence/absence
- National check digit algorithms
- Minimum/maximum values for numeric parts
- Alpha/numeric/alphanumeric field types

4.3 Checksum Edge Cases

# Algorithmic edge cases
- Valid checksums: 02-96, 98
- Algorithmically invalid: 01, 00, 97, 99
- Check digits as letters (should fail)
- Single digit check digits
- Missing check digits

# BBAN composition
- All-numeric BBANs
- All-alphabetic BBANs (e.g., QA)
- Mixed alphanumeric BBANs
- Maximum letter density (QA: "QAAA")

5. Property-Based Testing Opportunities

# Invariants to test:

1. Round-trip preservation:
   valid_iban |> parse |> format(:compact) |> parse == valid_iban

2. Checksum determinism:
   iban_violates_checksum?(iban) == iban_violates_checksum?(iban)

3. Country code extraction:
   country_code(iban) always returns 2 uppercase characters

4. BBAN length:
   byte_size(bban(iban)) == size(country) - 4

5. Normalization idempotence:
   normalize(normalize(x)) == normalize(x)

6. Format preservation:
   format(parse(iban), :compact) preserves check digits

7. Serialization idempotence:
   to_map(from_map(map)) == map (for valid maps)

# Generators needed:
- Valid IBAN generator (per country, respecting rules)
- Invalid IBAN generator (various violation types)
- Edge case generator (boundary lengths, special chars)
- Mutation generator (valid IBAN with single-bit errors)

6. Test Data Management Strategy

6.1 Test Data Organization

test/support/
├── fixtures/
│   ├── valid_ibans.exs        # 200+ valid IBANs (3-5 per country)
│   ├── invalid_ibans.exs      # 500+ invalid IBANs by violation type
│   └── edge_cases.exs         # 100+ boundary cases
├── factories/
│   └── iban_factory.ex        # Build Iban structs with overrides
└── countries/
    └── country_test_suite.ex  # Shared country conformance tests

6.2 Test Data Sets Needed

# 1. Valid IBANs Dataset (200+ entries)
# Organized by country, 3-5 per country
%{
  de: [
    "DE89370400440532013000",
    "DE44500105175407324931",
    # ... 3 more
  ],
  fr: [
    "FR1420041010050500013M02606",
    "FR7630006000011234567890189",
    # ... 3 more
  ],
  # ... all 70+ countries
}

# 2. Invalid IBANs Dataset (500+ entries)
# Organized by violation type
%{
  wrong_checksum: [
    "DE89370400440532013001",  # Valid format, wrong checksum
    "FR1420041010050500013M02607",
    # ... 100+ more
  ],
  wrong_length: [
    "DE8937040044053201300",   # Too short
    "DE893704004405320130000",  # Too long
    # ... 100+ more
  ],
  invalid_format: [
    "DEXXX70400440532013000",   # Invalid chars in BBAN
    "de89370400440532013000",   # Lowercase (should fail after normalize)
    # ... 100+ more
  ],
  unsupported_country: [
    "XX8937040044053201300",
    "ZZ1234567890123456",
    # ... 50+ more
  ],
  invalid_bank_code: [...],
  invalid_account_number: [...],
  invalid_branch_code: [...],
  invalid_national_check: [...],
}

# 3. Edge Cases Dataset (100+ entries)
%{
  shortest: "NO9386011117947",        # Norway: 15 chars
  longest: "MT84MALT011000012345MTLCAST001S", # Malta: 31 chars
  all_numeric_bban: [...],
  all_alpha_bban: [...],
  mixed_case: [...],
  with_spaces: [...],
  with_hyphens: [...],
  unicode: [...],
}

6.3 Test Support Modules

# test/support/test_data.ex
defmodule IbanEx.TestData do
  @valid_ibans ... # load from fixtures/valid_ibans.exs
  @invalid_ibans ... # load from fixtures/invalid_ibans.exs
  @edge_cases ... # load from fixtures/edge_cases.exs
  
  def valid_ibans(country \\ :all) do
    case country do
      :all -> @valid_ibans |> Map.values() |> List.flatten()
      code -> Map.get(@valid_ibans, code, [])
    end
  end
  
  def invalid_ibans(violation_type \\ :all) do
    case violation_type do
      :all -> @invalid_ibans |> Map.values() |> List.flatten()
      type -> Map.get(@invalid_ibans, type, [])
    end
  end
  
  def edge_cases, do: @edge_cases
  
  def random_valid_iban, do: Enum.random(valid_ibans())
  def random_invalid_iban, do: Enum.random(invalid_ibans())
end

# test/support/factories/iban_factory.ex
defmodule IbanEx.IbanFactory do
  @doc "Build valid Iban struct with optional overrides"
  def build(attrs \\ %{}) do
    default = %IbanEx.Iban{
      country_code: "DE",
      check_digits: "89",
      bban: "370400440532013000",
      bank_code: "37040044",
      account_number: "0532013000",
      branch_code: nil,
      national_check: nil
    }
    
    struct!(default, attrs)
  end
  
  def build_with_invalid_checksum(attrs \\ %{}) do
    build(Map.put(attrs, :check_digits, "00"))
  end
end

7. Implementation Roadmap

Phase 1: Critical Coverage (Weeks 1-2)

Priority: 🔴 CRITICAL - Security and data safety

Focus: Core validation, data conversion, parser safety

Week 1 Deliverables

  1. Validator Tests (test/iban_ex_validator_test.exs expansion)

    • violations/1 complete coverage (~100 lines)
    • validate/1 all error paths (~80 lines)
    • iban_violates_checksum?/1 comprehensive tests (~120 lines)
    • BBAN component validators negative tests (~150 lines)
  2. Deserialize Protocol Tests (test/iban_ex_deserialize_test.exs - NEW)

    • String/BitString implementation (~80 lines)
    • Map implementation (atom keys) (~100 lines)
    • Map implementation (string keys) (~60 lines)
    • Keyword list implementation (~50 lines)
    • Error handling (~40 lines)

Week 2 Deliverables

  1. Parser Edge Cases (test/iban_ex_parser_test.exs expansion)

    • parse/2 edge cases (~150 lines)
    • parse/2 with incomplete: true (~100 lines)
    • parse_bban/3 tests (~80 lines)
    • Helper functions edge cases (~70 lines)
  2. Serialize Tests (test/iban_ex_serialize_test.exs - NEW)

    • to_string/1 tests (~60 lines)
    • to_map/1 tests (~60 lines)
    • Round-trip tests (~50 lines)

Estimated Lines of Test Code: ~1,200 lines
Expected Coverage Increase: 40% → 70%


Phase 2: High Priority Coverage (Weeks 3-4)

Priority: 🟡 HIGH - User-facing APIs and utilities

Week 3 Deliverables

  1. Formatter Tests (test/iban_ex_formatter_test.exs - NEW)

    • available_formats/0 (~20 lines)
    • format/2 all types (~120 lines)
    • Convenience functions (~60 lines)
    • Edge cases (~80 lines)
    • Round-trip preservation (~40 lines)
  2. Iban Struct Tests (test/iban_ex_iban_test.exs - NEW)

    • Struct creation and validation (~80 lines)
    • Delegated functions (~100 lines)
    • Pattern matching (~40 lines)
  3. Commons Tests (test/iban_ex_commons_test.exs - NEW)

    • blank/1 (~40 lines)
    • normalize/1 (~80 lines)
    • normalize_and_slice/2 (~60 lines)

Week 4 Deliverables

  1. Country Module Tests (test/iban_ex_country_test.exs - NEW)

    • Discovery functions (~100 lines)
    • Edge cases for lookup (~80 lines)
  2. Integration Tests (test/integration/ - NEW directory)

    • End-to-end workflows (~200 lines)
    • Cross-module integration (~150 lines)
    • Real-world scenarios (~100 lines)
  3. Test Infrastructure

    • Fixtures: test/support/fixtures/*.exs (~300 lines)
    • Factory: test/support/factories/iban_factory.ex (~100 lines)
    • TestData: test/support/test_data.ex (~150 lines)

Estimated Lines of Test Code: ~1,700 lines
Expected Coverage Increase: 70% → 85%


Phase 3: Comprehensive Coverage (Weeks 5-6)

Priority: 🟢 MEDIUM - Country modules and regression

Week 5 Deliverables

  1. Country Implementation Tests (test/country/ - NEW directory)

    • Shared country test suite (~200 lines)
    • High-priority country tests (~400 lines):
      • DE, FR, GB, IT, BR, NO, MT
    • Template validation (~100 lines)
  2. Error Module Tests (test/iban_ex_error_test.exs - NEW)

    • All error atoms (~60 lines)
    • Message consistency (~40 lines)
    • Fallback behavior (~20 lines)

Week 6 Deliverables

  1. Validator Replacements Tests (test/iban_ex_validator_replacements_test.exs - NEW)

    • replace/1 letter mapping (~100 lines)
    • Complete A-Z coverage (~50 lines)
    • Fallback behavior (~30 lines)
  2. Comprehensive Country Coverage

    • Apply shared test suite to all 70+ countries (~100 lines)
    • Ensure async: true for performance
  3. Regression Test Suite (test/regression/ - NEW directory)

    • Violation type regression (~150 lines)
    • Known bug scenarios (~100 lines)

Estimated Lines of Test Code: ~1,350 lines
Expected Coverage Increase: 85% → 92%


Phase 4: Polish & Performance (Weeks 7-8)

Priority: 🟢 NICE-TO-HAVE - Property tests and performance

Week 7 Deliverables

  1. Property-Based Tests (test/property/iban_properties_test.exs - NEW)

    • Round-trip invariants (~100 lines)
    • Checksum determinism (~60 lines)
    • Normalization idempotence (~60 lines)
    • Format preservation (~60 lines)
    • Generators (~150 lines)
  2. Performance Tests (test/performance/benchmarks_test.exs - NEW)

    • Validation performance (~80 lines)
    • Parsing performance (~60 lines)
    • Bulk operations (~100 lines)

Week 8 Deliverables

  1. Documentation Enhancement

    • README examples expansion
    • Module documentation examples
    • Error handling examples
  2. CI/CD Integration

    • Coverage reporting setup (ExCoveralls)
    • Coverage gates configuration
    • Performance regression tests
  3. Final Audit

    • Code coverage report generation
    • Identify remaining gaps
    • Document coverage achievements

Estimated Lines of Test Code: ~670 lines
Expected Coverage Increase: 92% → 95%+


test/
├── iban_ex_test.exs                    # Existing - doctests
├── iban_ex_parser_test.exs             # Existing - expand
├── iban_ex_validator_test.exs          # Existing - expand
├── iban_ex_formatter_test.exs          # NEW - Phase 2
├── iban_ex_iban_test.exs               # NEW - Phase 2
├── iban_ex_serialize_test.exs          # NEW - Phase 1
├── iban_ex_deserialize_test.exs        # NEW - Phase 1
├── iban_ex_country_test.exs            # NEW - Phase 2
├── iban_ex_commons_test.exs            # NEW - Phase 2
├── iban_ex_error_test.exs              # NEW - Phase 3
├── iban_ex_validator_replacements_test.exs # NEW - Phase 3
├── integration/                         # NEW - Phase 2
│   ├── iban_lifecycle_test.exs
│   ├── cross_module_test.exs
│   └── real_world_scenarios_test.exs
├── country/                             # NEW - Phase 3
│   ├── country_test_suite.ex           # Shared suite
│   ├── de_test.exs
│   ├── fr_test.exs
│   ├── gb_test.exs
│   ├── it_test.exs
│   ├── br_test.exs
│   ├── no_test.exs
│   ├── mt_test.exs
│   └── ...
├── regression/                          # NEW - Phase 3
│   ├── violation_regression_test.exs
│   └── known_bugs_test.exs
├── property/                            # NEW - Phase 4
│   └── iban_properties_test.exs
├── performance/                         # NEW - Phase 4
│   └── benchmarks_test.exs
├── support/                             # NEW - Phase 2
│   ├── fixtures/
│   │   ├── valid_ibans.exs
│   │   ├── invalid_ibans.exs
│   │   └── edge_cases.exs
│   ├── factories/
│   │   └── iban_factory.ex
│   ├── countries/
│   │   └── country_test_suite.ex
│   └── test_data.ex
└── test_helper.exs                      # Existing

9. Testing Tools and Infrastructure

# mix.exs
defp deps do
  [
    # Existing production dependencies
    {:req, "~> 0.5.0"},
    
    # Testing dependencies
    {:ex_unit, "~> 1.18", only: :test},                              # Built-in
    {:stream_data, "~> 1.1", only: :test},                           # Property-based testing
    {:excoveralls, "~> 0.18", only: :test},                          # Coverage reporting
    {:benchee, "~> 1.3", only: [:dev, :test]},                       # Performance benchmarking
    {:mix_test_watch, "~> 1.0", only: :dev, runtime: false},         # Auto-test on file change
    
    # Code quality (already available via mix check)
    {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},      # Type checking
    {:credo, "~> 1.7", only: [:dev, :test], runtime: false},         # Code quality
    {:ex_doc, "~> 0.34", only: :dev, runtime: false},                # Documentation
  ]
end

9.2 CI/CD Integration

# .github/workflows/ci.yml (example)
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Elixir
        uses: erlef/setup-beam@v1
        with:
          elixir-version: '1.18'
          otp-version: '27'
      
      - name: Restore dependencies cache
        uses: actions/cache@v4
        with:
          path: deps
          key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: ${{ runner.os }}-mix-
      
      - name: Install dependencies
        run: mix deps.get
      
      - name: Run tests with coverage
        run: mix coveralls.json
        env:
          MIX_ENV: test
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          file: ./cover/excoveralls.json
          fail_ci_if_error: true
      
      - name: Run code quality checks
        run: mix check
      
      - name: Check coverage threshold
        run: |
          COVERAGE=$(jq '.total' ./cover/excoveralls.json)
          if (( $(echo "$COVERAGE < 90" | bc -l) )); then
            echo "Coverage $COVERAGE% is below 90% threshold"
            exit 1
          fi

9.3 Coverage Configuration

# mix.exs
def project do
  [
    # ...
    test_coverage: [tool: ExCoveralls],
    preferred_cli_env: [
      coveralls: :test,
      "coveralls.detail": :test,
      "coveralls.post": :test,
      "coveralls.html": :test,
      "coveralls.json": :test
    ],
  ]
end

9.4 Test Helper Configuration

# test/test_helper.exs
ExUnit.start()

# Configure test output
ExUnit.configure(
  exclude: [slow: true],           # Exclude slow tests by default
  formatters: [ExUnit.CLIFormatter],
  max_cases: System.schedulers_online() * 2,  # Parallel test execution
  timeout: 60_000                   # 60s timeout for long tests
)

# Load test support files
Code.require_file("support/test_data.ex", __DIR__)
Code.require_file("support/factories/iban_factory.ex", __DIR__)

# Seed random number generator for consistent property tests
:rand.seed(:exsss, {1, 2, 3})

10. Quality Metrics and Success Criteria

10.1 Coverage Targets

Phase Line Coverage Branch Coverage Function Coverage
Current ~40-50% ~30-40% ~35-45%
Phase 1 70% 60% 75%
Phase 2 85% 75% 90%
Phase 3 92% 85% 95%
Phase 4 95%+ 90%+ 98%+

10.2 Quality Gates

# Required for all PRs
minimum_coverage: 90%
coverage_regression_tolerance: -1%  # Max allowed decrease
test_execution_time: < 5 minutes
zero_test_failures: true
dialyzer_warnings: 0
credo_warnings: 0

# Required for releases
minimum_coverage: 95%
property_tests_passing: true
performance_benchmarks: no regression
documentation_coverage: 100%

10.3 Success Metrics

Short-term (1 month - End of Phase 2):

  • All Phase 1 & 2 tests implemented
  • Coverage ≥ 85%
  • No untested critical functions (violations/1, validate/1, iban_violates_checksum?/1)
  • All serialization/deserialization paths tested
  • Integration tests operational

Medium-term (3 months - End of Phase 4):

  • All phases completed
  • Coverage ≥ 95%
  • Property tests catching edge cases
  • CI/CD integration with coverage gates
  • Performance benchmarks established
  • Zero production bugs from untested code paths

Long-term (6 months):

  • Coverage maintained at 95%+
  • < 5 minute test suite execution
  • Quarterly test audits performed
  • All new features have tests before merge
  • Comprehensive documentation with examples

11. Appendix A: Example Test Implementations

Example 1: Validator.violations/1 Complete Test

# test/iban_ex_validator_test.exs (expansion)

describe "violations/1" do
  test "returns empty list for valid IBAN" do
    valid_ibans = IbanEx.TestData.valid_ibans()
    
    for iban <- valid_ibans do
      assert Validator.violations(iban) == [],
             "Expected no violations for valid IBAN: #{iban}"
    end
  end

  test "returns all violations for completely invalid IBAN" do
    violations = Validator.violations("INVALID")
    
    # Should have multiple violations
    assert :invalid_format in violations
    assert :unsupported_country_code in violations
    assert :invalid_length in violations
    
    # Should be deterministic
    assert violations == Validator.violations("INVALID")
  end

  test "returns specific violations for IBAN with wrong checksum" do
    # Valid format but wrong checksum (00 is algorithmically invalid)
    violations = Validator.violations("DE00370400440532013000")
    
    assert :invalid_checksum in violations
    refute :invalid_format in violations
    refute :unsupported_country_code in violations
  end

  test "returns multiple BBAN violations" do
    # Create IBAN with invalid BBAN structure
    violations = Validator.violations("DE89XXXXXXXXXXXX0000")
    
    # Should include BBAN format violations
    assert length(violations) > 0
    # Specific violations depend on DE country rules
  end
  
  test "returns violations for wrong length" do
    # German IBAN should be 22 chars, this is 21
    violations = Validator.violations("DE8937040044053201300")
    
    assert :invalid_length in violations
  end
  
  test "violations are deterministically ordered" do
    iban = "INVALID"
    violations1 = Validator.violations(iban)
    violations2 = Validator.violations(iban)
    
    assert violations1 == violations2
  end
end

Example 2: Deserialize Protocol Complete Test

# test/iban_ex_deserialize_test.exs (NEW)

defmodule IbanEx.DeserializeTest do
  use ExUnit.Case, async: true
  
  alias IbanEx.{Deserialize, Iban}

  describe "Deserialize.from/1 - String implementation" do
    test "deserializes valid IBAN string" do
      iban_string = "DE89370400440532013000"
      
      assert {:ok, %Iban{} = iban} = Deserialize.from(iban_string)
      assert iban.country_code == "DE"
      assert iban.check_digits == "89"
      assert iban.bban == "370400440532013000"
    end
    
    test "returns error for invalid IBAN string" do
      assert {:error, _reason} = Deserialize.from("INVALID")
    end
    
    test "handles empty string" do
      assert {:error, _reason} = Deserialize.from("")
    end
    
    test "handles bitstring input" do
      bitstring = <<"DE89370400440532013000">>
      
      assert {:ok, %Iban{}} = Deserialize.from(bitstring)
    end
  end

  describe "Deserialize.from/1 - Map implementation (atom keys)" do
    test "deserializes valid map with all required fields" do
      map = %{
        country_code: "DE",
        check_digits: "89",
        bban: "370400440532013000",
        bank_code: "37040044",
        account_number: "0532013000",
        branch_code: nil,
        national_check: nil
      }
      
      assert {:ok, %Iban{} = iban} = Deserialize.from(map)
      assert iban.country_code == "DE"
      assert iban.check_digits == "89"
    end
    
    test "deserializes map with optional fields as nil" do
      map = %{
        country_code: "DE",
        check_digits: "89",
        bban: "370400440532013000",
        bank_code: "37040044",
        account_number: "0532013000"
        # branch_code and national_check missing
      }
      
      assert {:ok, %Iban{} = iban} = Deserialize.from(map)
      assert iban.branch_code == nil
      assert iban.national_check == nil
    end
    
    test "returns error for map missing required fields" do
      map = %{
        country_code: "DE"
        # Missing other required fields
      }
      
      assert {:error, {:can_not_parse_map, _}} = Deserialize.from(map)
    end
    
    test "handles map with extra fields" do
      map = %{
        country_code: "DE",
        check_digits: "89",
        bban: "370400440532013000",
        bank_code: "37040044",
        account_number: "0532013000",
        extra_field: "should be ignored"
      }
      
      assert {:ok, %Iban{}} = Deserialize.from(map)
    end
  end

  describe "Deserialize.from/1 - Map implementation (string keys)" do
    test "deserializes map with string keys" do
      map = %{
        "country_code" => "DE",
        "check_digits" => "89",
        "bban" => "370400440532013000",
        "bank_code" => "37040044",
        "account_number" => "0532013000"
      }
      
      assert {:ok, %Iban{} = iban} = Deserialize.from(map)
      assert iban.country_code == "DE"
    end
    
    test "handles mixed atom and string keys" do
      map = %{
        "country_code" => "DE",
        check_digits: "89",  # atom key
        "bban" => "370400440532013000"
      }
      
      # Behavior depends on implementation
      # Should either work or return consistent error
      result = Deserialize.from(map)
      assert match?({:ok, _} | {:error, _}, result)
    end
  end

  describe "Deserialize.from/1 - Keyword list implementation" do
    test "deserializes valid keyword list" do
      list = [
        country_code: "DE",
        check_digits: "89",
        bban: "370400440532013000",
        bank_code: "37040044",
        account_number: "0532013000"
      ]
      
      assert {:ok, %Iban{}} = Deserialize.from(list)
    end
    
    test "returns error for empty list" do
      assert {:error, _} = Deserialize.from([])
    end
  end

  describe "Deserialize.from/1 - Error handling" do
    test "protocol not implemented for integers" do
      assert_raise Protocol.UndefinedError, fn ->
        Deserialize.from(12345)
      end
    end
    
    test "protocol not implemented for atoms" do
      assert_raise Protocol.UndefinedError, fn ->
        Deserialize.from(:invalid)
      end
    end
  end
end

Example 3: Property-Based Test

# test/property/iban_properties_test.exs (NEW)

defmodule IbanEx.PropertyTest do
  use ExUnit.Case
  use ExUnitProperties

  alias IbanEx.{Parser, Formatter, Validator, Commons}

  @tag :property
  property "valid IBAN round-trips through parse and format" do
    check all iban_string <- valid_iban_generator() do
      {:ok, iban} = Parser.parse(iban_string)
      formatted = Formatter.format(iban, :compact)
      {:ok, reparsed} = Parser.parse(formatted)
      
      assert iban == reparsed,
             "Round-trip failed: #{iban_string} -> #{formatted}"
    end
  end

  @tag :property
  property "checksum validation is deterministic" do
    check all iban_string <- StreamData.string(:alphanumeric, min_length: 15, max_length: 34) do
      result1 = Validator.iban_violates_checksum?(iban_string)
      result2 = Validator.iban_violates_checksum?(iban_string)
      
      assert result1 == result2,
             "Checksum validation non-deterministic for: #{iban_string}"
    end
  end

  @tag :property
  property "country_code always returns 2 uppercase chars for normalized input" do
    check all iban_string <- StreamData.string(:alphanumeric, min_length: 15, max_length: 34) do
      normalized = Commons.normalize(iban_string)
      
      if byte_size(normalized) >= 2 do
        code = Parser.country_code(normalized)
        assert byte_size(code) == 2
        assert code == String.upcase(code)
      end
    end
  end

  @tag :property
  property "normalization is idempotent" do
    check all input <- StreamData.string(:printable) do
      normalized_once = Commons.normalize(input)
      normalized_twice = Commons.normalize(normalized_once)
      
      assert normalized_once == normalized_twice,
             "Normalization not idempotent for: #{inspect(input)}"
    end
  end

  @tag :property
  property "formatting preserves check digits" do
    check all iban_string <- valid_iban_generator(),
              format <- StreamData.member_of([:compact, :pretty, :splitted]) do
      {:ok, iban} = Parser.parse(iban_string)
      formatted = Formatter.format(iban, format)
      {:ok, reparsed} = Parser.parse(formatted)
      
      assert iban.check_digits == reparsed.check_digits,
             "Format #{format} altered check digits: #{iban_string}"
    end
  end

  # Generators
  
  defp valid_iban_generator do
    # Use test fixtures as base
    IbanEx.TestData.valid_ibans()
    |> StreamData.member_of()
  end
  
  defp invalid_iban_generator do
    # Generate invalid IBANs by mutating valid ones
    gen all iban <- valid_iban_generator(),
            mutation <- mutation_generator() do
      apply_mutation(iban, mutation)
    end
  end
  
  defp mutation_generator do
    StreamData.member_of([
      :flip_checksum,
      :change_length,
      :invalid_char,
      :wrong_country
    ])
  end
  
  defp apply_mutation(iban, :flip_checksum) do
    # Flip check digits to create invalid checksum
    <<country::binary-size(2), check::binary-size(2), rest::binary>> = iban
    flipped = if check == "00", do: "01", else: "00"
    country <> flipped <> rest
  end
  
  defp apply_mutation(iban, :change_length) do
    # Add or remove character
    if :rand.uniform(2) == 1 do
      iban <> "0"
    else
      String.slice(iban, 0..-2//1)
    end
  end
  
  defp apply_mutation(iban, :invalid_char) do
    # Insert invalid character
    pos = :rand.uniform(byte_size(iban)) - 1
    <<before::binary-size(pos), _::binary-size(1), after_::binary>> = iban
    before <> "!" <> after_
  end
  
  defp apply_mutation(iban, :wrong_country) do
    # Replace with unsupported country
    <<"XX", rest::binary>> = String.slice(iban, 2..-1//1)
    "XX" <> rest
  end
end

12. Monitoring and Maintenance

12.1 Ongoing Test Maintenance

New Country Support:

# Template for adding new country tests
# test/country/{code}_test.exs

defmodule IbanEx.Country.XXTest do
  use ExUnit.Case, async: true
  use IbanEx.CountryTestSuite  # Shared suite
  
  @country_code "XX"
  @module IbanEx.Country.XX
  
  # Shared suite runs automatically:
  # - size/0 validation
  # - rules/0 and rules_map/0 consistency
  # - to_string/1 formatting
  # - BBAN regex validation
  
  # Add country-specific edge cases here
  test "XX-specific edge case" do
    # ...
  end
end

Regression Prevention:

# For every bug fix, add to test/regression/known_bugs_test.exs

test "issue #42: checksum validation for all-letter BBANs" do
  # Reproduce the bug scenario
  # Assert it's now fixed
end

Quarterly Audit Checklist:

  • Run mix coveralls.html and review uncovered lines
  • Check for new untested public functions
  • Review property test failure logs
  • Update test data fixtures with new edge cases
  • Benchmark test suite execution time
  • Update CI/CD coverage thresholds if needed

12.2 Code Review Process

PR Test Requirements:

  • All new functions must have tests in same PR
  • Coverage must not decrease (enforced by CI)
  • Property tests for new algorithms
  • Integration tests for cross-module changes
  • Documentation examples must have doctests

13. Summary and Next Steps

13.1 Immediate Actions (This Week)

  1. Set up coverage tooling

    # Add to mix.exs
    {:excoveralls, "~> 0.18", only: :test}
    
    # Run baseline
    mix coveralls.html
    
  2. Create test infrastructure

    mkdir -p test/support/fixtures
    mkdir -p test/support/factories
    touch test/support/test_data.ex
    touch test/support/factories/iban_factory.ex
    
  3. Implement Phase 1, Priority 1-2

    • test/iban_ex_validator_test.exs: Add violations/1 and validate/1 tests
    • test/iban_ex_deserialize_test.exs: NEW file with protocol tests
  4. Baseline measurement

    mix test --cover
    mix coveralls.detail
    # Document current coverage
    

13.2 Success Criteria Summary

Timeframe Goal Coverage Target Key Deliverables
Week 2 Phase 1 Complete 70% Critical validators, deserialize, parser safety
Week 4 Phase 2 Complete 85% Formatter, integration, test infrastructure
Week 6 Phase 3 Complete 92% All countries, regression, utilities
Week 8 Phase 4 Complete 95%+ Properties, performance, CI/CD

13.3 Long-Term Vision

6 Months:

  • Zero production bugs from untested code paths
  • < 5 minute test suite execution time
  • 95%+ code coverage maintained automatically
  • All public APIs have documented examples with doctests
  • Property tests catch edge cases before manual testing
  • Performance benchmarks prevent regression
  • Automated coverage gates in CI/CD prevent coverage decrease

12 Months:

  • Test-driven development culture for all changes
  • Comprehensive test data covering all IBAN edge cases globally
  • Mutation testing to verify test suite quality
  • Automated security scanning for IBAN handling vulnerabilities
  • Performance monitoring tracking validation latency over time

Appendix B: Coverage Report Template

# Coverage Report - [Date]

## Overall Metrics
- **Line Coverage:** X%
- **Branch Coverage:** Y%
- **Function Coverage:** Z%
- **Test Execution Time:** X.XXs

## Module Breakdown
| Module | Line % | Branch % | Function % | Status |
|--------|--------|----------|------------|--------|
| IbanEx | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Parser | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Validator | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Formatter | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Iban | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Serialize | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Deserialize | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Country | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Commons | X% | Y% | Z% | ✅/⚠️/❌ |
| IbanEx.Error | X% | Y% | Z% | ✅/⚠️/❌ |

## Uncovered Critical Paths
1. **[Module].[Function]:** [Reason not covered / Plan to cover]
2. ...

## Test Suite Statistics
- **Total Tests:** X
- **Total Assertions:** Y
- **Property Tests:** Z
- **Test Files:** N

## Next Phase Target
- **Target Completion:** [Date]
- **Expected Coverage Increase:** +X%
- **Focus Areas:**
  - [ ] Module A
  - [ ] Module B
  - [ ] Integration scenarios

## Notable Improvements This Phase
- Added X tests covering Y functions
- Improved coverage from A% to B%
- Discovered and fixed Z edge cases

Document Version: 2.0 (Merged & Enhanced)
Created: 2025-01-29
Last Updated: 2025-01-29
Authors: Comprehensive analysis combining multiple perspectives
Tooling Used: Tree Sitter, Cicada MCP, Manual Source Review


Quick Reference: Priority Matrix

Priority Modules Why Critical When to Implement
🔴 CRITICAL Validator (violations/1, validate/1, checksum), Deserialize, Parser edge cases Security, data safety, silent failures Week 1-2 (Phase 1)
🟡 HIGH Formatter, Serialize, Iban struct, Commons, Country lookup User-facing APIs, utilities Week 3-4 (Phase 2)
🟢 MEDIUM Country implementations, Error, Replacements, Regression Comprehensive coverage, edge cases Week 5-6 (Phase 3)
🟢 NICE Property tests, Performance, Documentation Polish, optimization, prevention Week 7-8 (Phase 4)

Remember: The goal is not just coverage percentage, but confidence that every code path behaves correctly under all conditions.