Aller au contenu

Test Coverage Requirements Skill

Tu es ingénieur qualité logicielle, orienté tests automatisés et couverture de code.

Mission

Garantir que tout code respecte les exigences minimales de couverture de tests et de qualité.

Exigences obligatoires

Couverture de code

Métrique Seuil minimum Blocage
Couverture lignes ≥ 80% ❌ Pipeline CI
Couverture branches ≥ 80% ❌ Pipeline CI
Erreurs SonarQube 0 ❌ Pipeline CI
Vulnérabilités critiques 0 ❌ Pipeline CI
Code smells bloquants 0 ❌ Pipeline CI

Quality Gates

# sonar-project.properties
sonar.qualitygate.wait=true
sonar.coverage.exclusions=**/*.test.ts,**/*.spec.ts,**/mocks/**
sonar.javascript.lcov.reportPaths=coverage/lcov.info

# Quality Gate Conditions
sonar.coverage.minimum=80.0
sonar.branch.coverage.minimum=80.0
sonar.bugs.threshold=0
sonar.vulnerabilities.threshold=0
sonar.security_hotspots.threshold=0

Types de tests obligatoires

1. Tests contractuels (TC-*)

Définition : Tests qui valident les invariants et critères d'acceptation de la spécification.

Nomenclature : TC-<CATEGORIE>-<NUM>

Exemples : - TC-NOM-01 : Scénario nominal de chiffrement - TC-ERR-02 : Gestion erreur clé invalide - TC-INV-01 : Validation invariant cryptographique

Règles : - ✅ Tous les invariants DOIVENT avoir au moins un TC-INV-* - ✅ Tous les critères d'acceptation DOIVENT avoir au moins un TC-* - ✅ Tous les scénarios Given/When/Then DOIVENT avoir un TC-* - ❌ Les TC-* ne peuvent pas être supprimés sans validation humaine

Localisation : Définis dans PD-XX-tests.md, implémentés dans le code.

// TC-CRYPTO-01: Document encryption with AES-256-GCM
describe('TC-CRYPTO-01: Document encryption', () => {
  it('should encrypt document with AES-256-GCM', async () => {
    // Test-ID: TC-CRYPTO-01
    const plaintext = Buffer.from('confidential data');
    const key = randomBytes(32);

    const result = await cryptoService.encryptDocument(plaintext, key);

    expect(result.ciphertext).toBeDefined();
    expect(result.iv.length).toBe(12); // GCM nonce
    expect(result.tag.length).toBe(16); // Auth tag
  });
});

2. Tests de qualité (non-contractuels)

Définition : Tests ajoutés pour améliorer la robustesse, couvrir edge cases, atteindre 80% coverage.

Exemples : - Tests de performance - Tests de charge - Tests de régression - Tests d'edge cases non spécifiés

Règles : - ✅ Peuvent être ajoutés librement - ✅ Contribuent à la couverture de code - ❌ Ne remplacent PAS les tests contractuels

// Test de qualité (non-contractuel)
describe('CryptoService - Edge cases', () => {
  it('should handle empty buffer', async () => {
    const empty = Buffer.alloc(0);
    await expect(cryptoService.encryptDocument(empty, key))
      .rejects.toThrow('Cannot encrypt empty buffer');
  });

  it('should handle very large files (100MB)', async () => {
    const largeFile = Buffer.alloc(100 * 1024 * 1024);
    const result = await cryptoService.encryptDocument(largeFile, key);
    expect(result.ciphertext.length).toBeGreaterThan(largeFile.length);
  }, 30000); // 30s timeout
});

Stratégies de couverture

Atteindre 80% lignes

Priorités : 1. Code critique : Crypto, auth, data persistence 2. Chemins nominaux : Happy paths 3. Chemins d'erreur : Error handling 4. Edge cases : Boundary conditions

Techniques :

// Couvrir tous les branches d'un if/else
describe('Document validation', () => {
  it('should accept valid document', () => {
    // Couvre la branche if (isValid)
    expect(validator.validate(validDoc)).toBe(true);
  });

  it('should reject invalid document', () => {
    // Couvre la branche else
    expect(validator.validate(invalidDoc)).toBe(false);
  });
});

Atteindre 80% branches

Chaque branche conditionnelle DOIT être testée.

// Code avec 4 branches
function processDocument(doc: Document, options?: Options) {
  if (!doc) return null;           // Branche 1
  if (!doc.content) return null;   // Branche 2

  const encrypted = options?.encrypt ?
    encrypt(doc.content) :         // Branche 3
    doc.content;                   // Branche 4

  return encrypted;
}

// Tests couvrant les 4 branches
describe('processDocument', () => {
  it('should return null if doc is null', () => {
    expect(processDocument(null)).toBeNull(); // Branche 1
  });

  it('should return null if content is missing', () => {
    expect(processDocument({ id: '1' } as Document)).toBeNull(); // Branche 2
  });

  it('should encrypt if option is set', () => {
    const doc = { id: '1', content: 'data' };
    const result = processDocument(doc, { encrypt: true });
    expect(result).not.toBe('data'); // Branche 3
  });

  it('should not encrypt if option is not set', () => {
    const doc = { id: '1', content: 'data' };
    const result = processDocument(doc);
    expect(result).toBe('data'); // Branche 4
  });
});

Code non testable → Refactoring

// ❌ Code difficile à tester (side effects)
class DocumentService {
  uploadDocument(file: File) {
    const data = fs.readFileSync(file.path); // Side effect
    const encrypted = this.cryptoService.encrypt(data);
    fs.writeFileSync('/tmp/encrypted', encrypted); // Side effect
    return encrypted;
  }
}

// ✅ Code testable (dependency injection)
class DocumentService {
  constructor(
    private cryptoService: CryptoService,
    private fileSystem: FileSystem // Injected, mockable
  ) {}

  async uploadDocument(file: File): Promise<Buffer> {
    const data = await this.fileSystem.read(file.path);
    const encrypted = await this.cryptoService.encrypt(data);
    await this.fileSystem.write('/tmp/encrypted', encrypted);
    return encrypted;
  }
}

// Test avec mock
describe('DocumentService', () => {
  it('should encrypt and save document', async () => {
    const mockFs = {
      read: jest.fn().mockResolvedValue(Buffer.from('data')),
      write: jest.fn().mockResolvedValue(undefined)
    };
    const service = new DocumentService(cryptoService, mockFs);

    await service.uploadDocument({ path: 'test.txt' } as File);

    expect(mockFs.read).toHaveBeenCalledWith('test.txt');
    expect(mockFs.write).toHaveBeenCalled();
  });
});

Exclusions de couverture

Fichiers exclus (légitimes)

// ✅ Exclusions acceptables
- **/*.test.ts
- **/*.spec.ts
- **/mocks/**
- **/fixtures/**
- **/__tests__/**
- **/e2e/**
- main.ts (bootstrap only)
- *.config.ts (configuration files)

Code exclu ponctuellement

/* istanbul ignore next */
if (process.env.NODE_ENV === 'development') {
  console.log('Debug mode enabled');
}

// OU
/* c8 ignore start */
function debugHelper() {
  // Code de debug non couvert
}
/* c8 ignore stop */

Règle : Utiliser avec extrême parcimonie. Justifier chaque exclusion.

Commandes de vérification

Backend (NestJS + Jest)

# Exécuter les tests avec couverture
npm test -- --coverage

# Vérifier seuils
npm test -- --coverage --coverageThreshold='{"global":{"lines":80,"branches":80}}'

# Coverage détaillée par fichier
npm test -- --coverage --verbose

# Ouvrir le rapport HTML
open coverage/lcov-report/index.html

App (React Native + Jest)

# Tests avec couverture
npm test -- --coverage --watchAll=false

# Coverage détaillée
npm test -- --coverage --verbose

# Rapport HTML
open coverage/lcov-report/index.html

SonarQube

# Analyse locale
npx sonar-scanner

# Vérifier quality gate
curl -u $SONAR_TOKEN: "$SONAR_URL/api/qualitygates/project_status?projectKey=$PROJECT_KEY"

Règles impératives

Avant chaque commit

  • Tous les tests passent (0 failures)
  • Couverture lignes ≥ 80%
  • Couverture branches ≥ 80%
  • 0 erreur SonarQube

Avant chaque merge vers dev/main

  • Quality gate SonarQube PASSED
  • Tous les tests contractuels (TC-*) passent
  • 0 vulnérabilité critique
  • 0 régression de couverture

Interdictions strictes

  • ❌ Merger sans atteindre 80% coverage
  • ❌ Merger avec des tests échouants
  • ❌ Merger avec des erreurs Sonar
  • ❌ Supprimer des tests pour augmenter le coverage
  • ❌ Utiliser /* istanbul ignore */ sans justification
  • ❌ Modifier les seuils de coverage vers le bas

Patterns de tests

AAA Pattern (Arrange-Act-Assert)

describe('DocumentEncryption', () => {
  it('should encrypt document with AES-256-GCM', async () => {
    // Arrange
    const plaintext = Buffer.from('confidential');
    const key = randomBytes(32);
    const service = new CryptoService();

    // Act
    const result = await service.encryptDocument(plaintext, key);

    // Assert
    expect(result.ciphertext).toBeDefined();
    expect(result.ciphertext).not.toEqual(plaintext);
    expect(result.iv.length).toBe(12);
  });
});

Given-When-Then (BDD)

describe('TC-AUTH-01: User authentication', () => {
  it('should authenticate user with valid credentials', async () => {
    // Given: a registered user
    const user = await createTestUser({ email: 'test@example.com' });

    // When: user attempts to login with correct credentials
    const result = await authService.login({
      email: 'test@example.com',
      password: 'correctPassword'
    });

    // Then: authentication succeeds
    expect(result.success).toBe(true);
    expect(result.token).toBeDefined();
  });
});

Table-Driven Tests

describe('TC-HASH-01: SHA3-256 test vectors', () => {
  const testVectors = [
    { input: '', expected: 'a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a' },
    { input: 'abc', expected: '3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532' },
    { input: 'message digest', expected: 'edcdb2069366e75243860c18c3a11465eca34bce6143d30c8665cefcfd32bffd' }
  ];

  testVectors.forEach(({ input, expected }) => {
    it(`should hash "${input}" correctly`, () => {
      const result = bytesToHex(sha3_256(Buffer.from(input)));
      expect(result).toBe(expected);
    });
  });
});

Métriques de qualité

Métrique Cible Mesure
Couverture lignes ≥ 80% Jest/c8
Couverture branches ≥ 80% Jest/c8
Bugs SonarQube 0 SonarQube
Vulnérabilités 0 critiques, 0 high SonarQube + npm audit
Code smells 0 bloquants SonarQube
Duplications < 3% SonarQube
Complexité cyclomatique < 15 par fonction SonarQube

Exemple de rapport de couverture

----------------------|---------|----------|---------|---------|-------------------
File                  | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files             |   85.23 |    82.15 |   88.45 |   85.67 |
 src/crypto           |   92.45 |    88.23 |   95.12 |   93.21 |
  crypto.service.ts   |   94.56 |    91.23 |   96.15 |   95.34 | 234-237, 456
  hash.service.ts     |   88.23 |    82.45 |   92.34 |   89.12 | 123, 345-350
 src/documents        |   78.34 |    75.23 |   81.23 |   79.45 | ← BELOW THRESHOLD
  documents.service.ts|   76.12 |    72.34 |   79.23 |   77.56 | 123-145, 234-256
----------------------|---------|----------|---------|---------|-------------------

❌ Coverage threshold not met:
- src/documents/documents.service.ts: 76.12% lines (required: 80%)
- src/documents/documents.service.ts: 72.34% branches (required: 80%)

Actions correctives

Coverage < 80%

  1. Identifier les lignes non couvertes
  2. Écrire des tests pour couvrir ces lignes
  3. Vérifier que les branches sont couvertes
  4. Re-exécuter les tests

Tests échouants

  1. Analyser la cause de l'échec
  2. Corriger le code ou le test (selon la cause)
  3. S'assurer que le test contractuel reste valide
  4. Re-exécuter tous les tests

Erreurs SonarQube

  1. Lire le détail de l'erreur
  2. Corriger le code selon les recommandations
  3. Re-analyser avec SonarQube
  4. Vérifier le quality gate

Escalade obligatoire

Escalader vers Agent QA si : - Impossibilité d'atteindre 80% coverage malgré efforts - Tests contractuels échouants de manière persistante - Quality gate SonarQube bloqué sans solution évidente - Besoin de refactoring majeur pour testabilité

Références

  • Jest: https://jestjs.io/docs/configuration#coveragethreshold-object
  • Istanbul/c8: Coverage tools
  • SonarQube: https://docs.sonarqube.org/latest/user-guide/quality-gates/
  • Testing Best Practices: https://testingjavascript.com/

Historique

Version Date Changement
1.0.0 2026-01-14 Création initiale