Salesforce exige un 75 % de cobertura de código para poder desplegar. Pero hay una diferencia entre cobertura y tests que detectan bugs. La mayoría de los proyectos Apex tienen mucho de lo primero y muy poco de lo segundo. Aquí tienes cómo escribir tests que realmente importan.
Requisitos previos
- Conocimientos básicos de Apex
- Comprensión de la estructura de triggers/clases Apex
- Familiaridad con las operaciones DML de Salesforce
El patrón TestDataFactory
Centraliza la creación de datos de prueba en una clase de utilidad TestDataFactory en lugar de repetirla en cada método — también para los conjuntos propios de un test, como los 200 registros de un test masivo:
@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;
}
}Ventajas: un único lugar que actualizar cuando cambien las reglas de validación, reutilizable en todas las clases de test, te obliga a pensar en los campos obligatorios.
@TestSetup — configuración compartida sin sobrecarga
Usa @TestSetup para crear los datos una sola vez por clase en lugar de repetir la configuración en cada método:
@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(), 'Should return 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(), 'All contacts should be deactivated');
}
}Importante: los datos de
@TestSetupse crean una sola vez por clase. Cada método de test parte de su estado inicial: los cambios DML que un método les aplica se revierten antes del siguiente.
Probar código asíncrono con Test.startTest() / stopTest()
Test.startTest() reinicia los governor limits y fuerza a que la ejecución asíncrona (métodos future, queueable, batch) se ejecute de forma síncrona cuando se llama a Test.stopTest():
@isTest
static void testFutureMethod() {
Account acc = TestDataFactory.createAccount('Async Test', true);
Test.startTest();
AccountService.recalculateMetricsAsync(acc.Id); // método @future
Test.stopTest(); // Fuerza la ejecución asíncrona aquí
// Ahora consulta los resultados — el @future ya se ha ejecutado
Account updated = [SELECT Metric_Score__c FROM Account WHERE Id = :acc.Id];
System.assertNotEquals(null, updated.Metric_Score__c, 'Score should be calculated');
}Mockear callouts HTTP
Los tests que hacen callouts HTTP reales fallarán. Usa HttpCalloutMock:
// Clase 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étodo de test
@isTest
static void testCalloutSuccess() {
Test.setMock(HttpCalloutMock.class, new SalesforceApiMock());
Test.startTest();
String result = ExternalApiService.createRecord('Test Data');
Test.stopTest();
System.assertEquals('12345', result, 'Should return the created ID');
}Para los escenarios de error, crea un segundo mock que devuelva 4xx/5xx y prueba también tu ruta de manejo de errores.
Escribir aserciones significativas
Las aserciones deficientes inflan la cobertura sin detectar bugs:
// ❌ Siempre pasa
System.assert(true);
// ❌ Demasiado débil — solo comprueba que hay un resultado, no su contenido
System.assertNotEquals(null, result);
// ✅ Significativa — detectará regresiones
System.assertEquals(3, result.size(),
'Expected 3 records after filtering by active status');
System.assertEquals('ESCALATED', ticket.Status__c,
'Ticket should be escalated when SLA is breached');
System.assert(result.contains(expectedId),
'Result set must include the account we just created');Usa siempre el tercer parámetro (el mensaje) — te indica qué falló cuando la aserción se rompe.
Desde Winter '23, la clase Assert (Assert.areEqual, Assert.isTrue, Assert.fail…) es la API recomendada para los nuevos tests; System.assertEquals sigue siendo válido, con el mismo orden de parámetros (esperado, obtenido, mensaje).
Antipatrones a evitar
SeeAllData=true: expone tus tests a los datos reales de la org, haciéndolos dependientes del entorno y poco fiables. Evítalo por defecto y resérvalo para los raros casos en que los datos necesarios no se pueden crear en un test. Para el catálogo de precios estándar no hace falta:Test.getStandardPricebookId()funciona sin él.- Insertar en cada método de test: duplica el código y ralentiza las ejecuciones (cada método tiene sus propios límites, pero recrea todos sus datos). Usa
@TestSetupoTestDataFactory. - Probar solo el camino feliz: escribe al menos un test negativo — ¿qué ocurre con una entrada nula, campos faltantes o datos inválidos?
- Ignorar los tests masivos: añade un test con 200 registros para verificar que tu código está correctamente "bulkificado".
@isTest
static void testBulk200Records() {
// Volumen propio de este test, creado mediante 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'],
'Las 200 cuentas deben procesarse en una sola transacción');
}Resumen
| Práctica | Por qué importa |
|---|---|
TestDataFactory | Fuente única de verdad para los datos de test |
@TestSetup | Evita DML repetidos, métodos de test más limpios |
Test.startTest/stopTest | Fuerza la ejecución asíncrona, reinicia los límites |
HttpCalloutMock | Prueba la lógica de callout sin llamadas externas |
| Aserciones significativas | Realmente detecta regresiones |
| Test masivo (200 registros) | Verifica la "bulkificación" |