Salesforce exige 75 % de couverture pour déployer. Mais il y a une différence entre la couverture et les tests qui détectent des bugs. La plupart des projets Apex ont largement la première et très peu du second. Voici comment écrire des tests qui comptent vraiment.
Prérequis
- Notions de base en Apex
- Compréhension de la structure des triggers et classes Apex
- Familiarité avec les opérations DML Salesforce
Le pattern TestDataFactory
Centralisez la création des données de test dans une classe utilitaire TestDataFactory plutôt que de la répéter dans chaque méthode — y compris pour les jeux propres à un test, comme les 200 enregistrements d'un test de masse :
@isTest
public class TestDataFactory {
public static Account createAccount(String name, Boolean doInsert) {
Account acc = new Account(
Name = name,
Phone = '0123456789',
BillingCountry = 'France'
);
if (doInsert) insert acc;
return acc;
}
public static List<Account> createAccounts(String namePrefix, Integer count, Boolean doInsert) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(createAccount(namePrefix + ' ' + i, false));
}
if (doInsert) insert accounts;
return accounts;
}
public static List<Contact> createContacts(Id accountId, Integer count, Boolean doInsert) {
List<Contact> contacts = new List<Contact>();
for (Integer i = 0; i < count; i++) {
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact ' + i,
AccountId = accountId,
Email = 'test' + i + '@example.com'
));
}
if (doInsert) insert contacts;
return contacts;
}
}Avantages : un seul endroit à modifier quand les règles de validation changent, réutilisable dans toutes les classes de test, vous force à penser aux champs obligatoires.
@TestSetup — Initialisation partagée sans surcoût
Utilisez @TestSetup pour créer les données une seule fois par classe au lieu de répéter le setup dans chaque méthode :
@isTest
private class AccountServiceTest {
@TestSetup
static void setup() {
Account acc = TestDataFactory.createAccount('Test Corp', true);
TestDataFactory.createContacts(acc.Id, 5, true);
}
@isTest
static void testGetActiveContacts() {
Account acc = [SELECT Id FROM Account LIMIT 1];
Test.startTest();
List<Contact> result = AccountService.getActiveContacts(acc.Id);
Test.stopTest();
System.assertEquals(5, result.size(), 'Devrait retourner 5 contacts');
}
@isTest
static void testDeactivateContacts() {
Account acc = [SELECT Id FROM Account LIMIT 1];
Test.startTest();
AccountService.deactivateContacts(acc.Id);
Test.stopTest();
List<Contact> active = [SELECT Id FROM Contact WHERE AccountId = :acc.Id AND IsActive__c = true];
System.assertEquals(0, active.size(), 'Tous les contacts devraient être désactivés');
}
}Important : les données
@TestSetupsont créées une seule fois par classe. Chaque méthode de test part de leur état initial : les modifications DML qu'une méthode leur apporte sont annulées avant la suivante.
Tester le code asynchrone avec Test.startTest() / stopTest()
Test.startTest() réinitialise les governor limits et force l'exécution asynchrone (future methods, queueable, batch) à s'exécuter de façon synchrone lors de l'appel à Test.stopTest() :
@isTest
static void testFutureMethod() {
Account acc = TestDataFactory.createAccount('Test Async', true);
Test.startTest();
AccountService.recalculateMetricsAsync(acc.Id); // méthode @future
Test.stopTest(); // Force l'exécution asynchrone ici
// Requête des résultats — le @future s'est exécuté
Account updated = [SELECT Metric_Score__c FROM Account WHERE Id = :acc.Id];
System.assertNotEquals(null, updated.Metric_Score__c, 'Le score doit être calculé');
}Simuler les callouts HTTP
Les tests qui effectuent de vrais appels HTTP échoueront. Utilisez HttpCalloutMock :
// Classe mock
@isTest
global class SalesforceApiMock implements HttpCalloutMock {
global HTTPResponse respond(HTTPRequest req) {
HTTPResponse res = new HTTPResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"id": "12345", "status": "success"}');
res.setStatusCode(200);
return res;
}
}
// Méthode de test
@isTest
static void testCalloutSuccess() {
Test.setMock(HttpCalloutMock.class, new SalesforceApiMock());
Test.startTest();
String result = ExternalApiService.createRecord('Données Test');
Test.stopTest();
System.assertEquals('12345', result, 'Devrait retourner l\'ID créé');
}Pour les scénarios d'erreur, créez un second mock qui retourne 4xx/5xx et testez aussi votre gestion des erreurs.
Assertions significatives
Les mauvaises assertions gonflent la couverture sans détecter de bugs :
// ❌ Passe toujours
System.assert(true);
// ❌ Trop faible — ne vérifie que l'existence d'un résultat, pas son contenu
System.assertNotEquals(null, result);
// ✅ Significatif — détecte les régressions
System.assertEquals(3, result.size(),
'Attendu 3 enregistrements après filtrage par statut actif');
System.assertEquals('ESCALATED', ticket.Status__c,
'Le ticket doit être escaladé quand le SLA est dépassé');Utilisez toujours le troisième paramètre (message) — il vous indique ce qui a échoué quand l'assertion plante.
Depuis Winter '23, la classe Assert (Assert.areEqual, Assert.isTrue, Assert.fail…) est l'API recommandée pour les nouveaux tests ; System.assertEquals reste valide, et l'ordre des paramètres (attendu, puis obtenu, puis message) est le même.
Anti-patterns à éviter
SeeAllData=true: expose vos tests aux données réelles de l'org, les rendant dépendants de l'environnement. À éviter par défaut, et à réserver aux rares cas où les données nécessaires ne peuvent pas être créées en test. Pour le catalogue de prix standard, il est inutile :Test.getStandardPricebookId()fonctionne sans.- Insertion dans chaque méthode : duplique le code et ralentit les exécutions (chaque méthode a ses propres limites, mais recrée toutes ses données). Utilisez
@TestSetupouTestDataFactory. - Tester uniquement le chemin heureux : écrivez au moins un test négatif.
- Ignorer les tests en masse : ajoutez un test avec 200 enregistrements pour vérifier la bulkification.
@isTest
static void testBulk200Records() {
// Volume propre à ce test, créé via la factory
List<Account> accounts = TestDataFactory.createAccounts('Bulk', 200, true);
Test.startTest();
AccountService.processAll(accounts);
Test.stopTest();
System.assertEquals(200, [SELECT COUNT() FROM Account WHERE Status__c = 'Processed'],
'Les 200 comptes doivent être traités en une seule transaction');
}Récapitulatif
| Pratique | Pourquoi c'est important |
|---|---|
TestDataFactory | Source unique pour les données de test |
@TestSetup | Évite les DML répétés, méthodes plus lisibles |
Test.startTest/stopTest | Force l'exécution async, réinitialise les limites |
HttpCalloutMock | Teste la logique de callout sans appels externes |
| Assertions significatives | Détecte vraiment les régressions |
| Test bulk (200 enregistrements) | Vérifie la bulkification |