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%¶
- Identifier les lignes non couvertes
- Écrire des tests pour couvrir ces lignes
- Vérifier que les branches sont couvertes
- Re-exécuter les tests
Tests échouants¶
- Analyser la cause de l'échec
- Corriger le code ou le test (selon la cause)
- S'assurer que le test contractuel reste valide
- Re-exécuter tous les tests
Erreurs SonarQube¶
- Lire le détail de l'erreur
- Corriger le code selon les recommandations
- Re-analyser avec SonarQube
- 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 |