Garanties de qualité Salesforce Apex
Appliquez ces vérifications à chaque classe Apex, trigger et fichier de test que vous écrivez ou examinez.
Étape 1 — Vérification de la sécurité des limites de gouvernance
Scannez ces patterns avant de déclarer un fichier Apex acceptable :
SOQL et DML dans les boucles — Échec automatique
// ❌ JAMAIS — provoque LimitException à l'échelle
for (Account a : accounts) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL dans la boucle
update a; // DML dans la boucle
}
// ✅ TOUJOURS — collecter, puis interroger/mettre à jour une seule fois
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
update accounts; // DML une seule fois, en dehors de la boucle
Règle : si vous voyez [SELECT ou Database.query, insert, update, delete, upsert, merge à l'intérieur du corps d'une boucle for — arrêtez et refactorisez avant de continuer.
Étape 2 — Vérification du modèle de partage
Chaque classe doit déclarer explicitement son intention de partage. Le partage non déclaré hérite de l'appelant — comportement imprévisible.
| Déclaration | Quand l'utiliser |
|---|---|
public with sharing class Foo |
Par défaut pour tous les service, handler, selector et controller classes |
public without sharing class Foo |
Uniquement quand la classe doit s'exécuter avec des droits élevés (ex. journalisation système, bypass de trigger). Nécessite un commentaire de code expliquant pourquoi. |
public inherited sharing class Foo |
Points d'entrée du framework qui doivent respecter le contexte de partage de l'appelant |
Si une classe n'a pas l'une de ces trois déclarations, ajoutez-la avant d'écrire autre chose.
Étape 3 — Application de la sécurité CRUD / FLS
Le code Apex qui lit ou écrit des enregistrements au nom d'un utilisateur doit vérifier l'accès aux objets et aux champs. La plateforme n'applique pas automatiquement FLS ou CRUD dans Apex.
// Vérifier avant de interroger un champ
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
throw new System.NoAccessException();
}
// Ou utiliser WITH USER_MODE dans SOQL (API 56.0+)
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];
// Ou utiliser Database.query avec AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);
Règle : toute méthode Apex appelable à partir d'un composant UI, d'un endpoint REST, ou d'une @InvocableMethod doit appliquer CRUD/FLS. Les méthodes de service internes appelées uniquement à partir de contextes de confiance peuvent utiliser with sharing à la place.
Étape 4 — Prévention de l'injection SOQL
// ❌ JAMAIS — concatène l'entrée utilisateur dans la chaîne SOQL
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
// ✅ TOUJOURS — variable de liaison
String soql = [SELECT Id FROM Account WHERE Name = :userInput];
// ✅ Pour SOQL dynamique avec des noms de champs contrôlés par l'utilisateur — valider par rapport à une whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
throw new IllegalArgumentException('Field not permitted: ' + userInput);
}
Étape 5 — Idiomes Apex modernes
Préférez les fonctionnalités de langage actuelles (API 62.0 / Winter '25+) :
| Ancien pattern | Remplacement moderne |
|---|---|
if (obj != null) { x = obj.Field__c; } |
x = obj?.Field__c; |
x = (y != null) ? y : defaultVal; |
x = y ?? defaultVal; |
System.assertEquals(expected, actual) |
Assert.areEqual(expected, actual) |
System.assert(condition) |
Assert.isTrue(condition) |
[SELECT ... WHERE ...] sans contexte de partage |
[SELECT ... WHERE ... WITH USER_MODE] |
Étape 6 — Liste de contrôle des tests PNB
Chaque fonctionnalité doit être testée sur les trois chemins. En manquer un seul constitue une défaillance de qualité :
Chemin positif
- Entrée attendue → sortie attendue.
- Affirmer les valeurs de champ exactes, les nombres d'enregistrements ou les valeurs retournées — pas seulement qu'aucune exception n'a été levée.
Chemin négatif
- Entrée invalide, valeurs null, collections vides et conditions d'erreur.
- Affirmer que les exceptions sont levées avec le type et le message corrects.
- Affirmer qu'aucun enregistrement n'a été muté quand l'opération aurait dû échouer proprement.
Chemin en masse
- Insérer/mettre à jour/supprimer 200–251 enregistrements dans une seule transaction de test.
- Affirmer que tous les enregistrements ont été traités correctement — aucun échec partiel dû aux limites de gouvernance.
- Utiliser
Test.startTest()/Test.stopTest()pour isoler les compteurs de limites de gouvernance pour le travail asynchrone.
Règles de classe de test
@isTest(SeeAllData=false) // Obligatoire — aucune exception sans raison documentée
private class AccountServiceTest {
@TestSetup
static void makeData() {
// Créer toutes les données de test ici — utiliser une factory si elle existe dans le projet
}
@isTest
static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
// Chemin positif
List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
Test.startTest();
AccountService.processAccounts(accounts);
Test.stopTest();
// Affirmer des résultats significatifs — pas seulement aucune exception
List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
}
}
Étape 7 — Liste de contrôle de l'architecture des triggers
- [ ] Un trigger par objet. Si un deuxième trigger existe, le consolider dans le handler.
- [ ] Le corps du trigger contient uniquement : vérifications de contexte, invocation du handler et logique de routage.
- [ ] Aucune logique métier, SOQL ou DML directement dans le corps du trigger.
- [ ] Si un framework de trigger (Trigger Actions Framework, ff-apex-common, classe de base personnalisée) est déjà en use — l'étendre. Ne pas créer un pattern parallèle.
- [ ] La classe handler est
with sharingsauf si le trigger nécessite un accès élevé.
Référence rapide — Résumé des anti-patterns codés en dur
| Pattern | Action |
|---|---|
SOQL à l'intérieur de la boucle for |
Refactoriser : interroger avant la boucle, opérer sur les collections |
DML à l'intérieur de la boucle for |
Refactoriser : collecter les mutations, DML une seule fois après la boucle |
| Classe sans déclaration de partage | Ajouter with sharing (ou documenter pourquoi without sharing) |
escape="false" sur les données utilisateur (VF) |
Supprimer — l'auto-échappement applique la prévention XSS |
Bloc catch vide |
Ajouter la journalisation et re-lancer ou gérer l'erreur de manière appropriée |
| SOQL concaténé en chaîne avec entrée utilisateur | Remplacer par une variable de liaison ou une validation whitelist |
| Test sans assertion | Ajouter un appel Assert.* significatif |
Style System.assert / System.assertEquals |
Mettre à niveau vers Assert.isTrue / Assert.areEqual |
ID d'enregistrement codé en dur ('001...') |
Remplacer par un ID d'enregistrement de test interrogé ou inséré |