I have an apex trigger which updates the description of cases with the concatenation of its child cases, if it has. If not, it fills the Description field with a default phrase. I'm trying to code the test class for this trigger and i don't know how to continue.
Apex trigger:
trigger CaseTrigger on Case (before update) {
Map<Id, List<String>> idPadreYSusHijos = new Map<Id, List<String>>();
for(Case c :[SELECT Id, ParentId, CaseNumber FROM Case WHERE ParentId IN : Trigger.newMap.keySet()]){
// Comprobar si los Id ya están en el mapa, si no están se añaden.
// De esta forma se evitan duplicados
if(!idPadreYSusHijos.containsKey(c.ParentId)){
idPadreYSusHijos.put(c.ParentId, new List<String>());
}
// Añadir los casosHijos a sus respectivos padres mediante Id en el mapa
idPadreYSusHijos.get(c.ParentId).add(c.CaseNumber);
}
for(Case caso : Trigger.New){
List<String> numeroDeCasosHijos = idPadreYSusHijos.get(caso.Id);
if(numeroDeCasosHijos != null){
caso.Description = String.join(numeroDeCasosHijos, ', ');
}else{
// Lista vacía, no hay hijos, asignamos descripción por defecto
caso.Description = 'Caso sin hijos';
}
}
}
Test class:
@isTest
public class CaseTriggerTest {
@testSetup
static void dataCreation() {
Case padreConHijos = new Case(Status = 'New',Origin='Phone');
Case padreSinHijos = new Case(Status = 'New',Origin='Phone');
Case hijo1 = new Case(Status = 'New',Origin='Phone',ParentId=padreConHijos.Id, Description='hijo1');
Case hijo2 = new Case(Status = 'New',Origin='Phone',ParentId=padreConHijos.Id, Description='hijo2');
insert padreConHijos;
insert padreSinHijos;
insert hijo1;
insert hijo2;
}
@isTest
static void testCaseTrigger() {
List<Case> casosHijos = new List<Case>();
List<Case> casosPadres = new List<Case>();
for(Case c : [SELECT Id,CaseNumber FROM Case WHERE ParentId = null]) {
casosPadres.add(c);
}
//List<String> nombresHijos = new List<String>([SELECT CaseNumber FROM Case WHERE Id IN :casosHijos]);
update casosPadres;
for(Case c : [SELECT Id,CaseNumber FROM Case WHERE ParentId != null]) {
casosHijos.add(c);
}
update casosHijos;
Test.startTest();
for(Case caso : casosHijos){
System.assertEquals(caso.Description, 'Caso sin hijos');
}
Test.stopTest();
/*Test.startTest();
for(Case caso : casosHijos){
System.assertEquals(caso.Description, String.join(casosHijos, ', '));
}
Test.stopTest();*/
}
@isTest
static void testMapa() {
Case hijo1;
Case padreConHijos;
Test.startTest();
Map<Id,List<String>> lista = new Map<Id,List<String>>();
lista.put(padreConHijos.Id,new List<String>());
lista.get(padreConHijos.Id).add(hijo1.CaseNumber);
Test.stopTest();
}
}
With the test class i've made i cover 60% of the trigger. I don't know how to test the case where numeroDeCasosHijos !=null and the following lines :
if(!idPadreYSusHijos.containsKey(c.ParentId)){
idPadreYSusHijos.put(c.ParentId, new List<String>());
}
Best Answer
Part of the issue is in your
@testSetup
method.You need to perform a dml insert on the
padreConHijos
case (at the very least) before trying to set theParentId
onhijo1
andhijo2
. Records don't have an Id before they're inserted. There are a few situations where you can manually set an Id on a test record, but this is not one of them (because you're using a query in your trigger).You only get coverage for code executed as part of a unit test
So if you expect a certain piece of code to be covered, and it isn't, then you know something is wrong.
In
testCaseTrigger()
, the query infor(Case c : [SELECT Id,CaseNumber FROM Case WHERE ParentId = null])
was returning 4 cases (both parents, and both children) instead of 2 cases (both parents) because your test setup ended up populatingParentId
on the child cases beforepadreConHijos
had an Id.When you got to the first query in your trigger, in
for(Case c :[SELECT Id, ParentId, CaseNumber FROM Case WHERE ParentId IN : Trigger.newMap.keySet()]){
, it returned no results, so the body of thefor
loop was not executed.Fixing the test setup should fix that issue, and get you coverage for the body of that for loop. It'll also ensure that you have a case where
numeroDeCasosHijos
in your second loop is not null (thus gaining coverage for that as well).Code coverage should not be the focus of unit tests
Code coverage is the metric that Salesforce uses, and we as programmers need to care about it to some degree, but coverage isn't what makes a test useful to us.
Instead, you want to focus on whether or not the your code behaves as intended. That's what assertions help us do. A method that returns the result of 2 + 2 could have 100% coverage, but if it returns 5 as a result then the method is doing something wrong.
As for what you should be writing assertions for, you should look at the results, the output of running your code. Things like
The main thing you're concerned about in this case is if the case Description is updated or not.
Try to write more, small tests instead of fewer, large tests
Smaller tests are generally easier to write, and having multiple tests makes it easier to see where issues are.
If you have a single, monolithic test method and it fails partway through, you'll only get a single error message. If your code has more than one issue, you'll need to fix the first error before you can see the second error.
With multiple tests, you have a chance to see multiple errors at once. You'll still probably end up fixing them one at a time, but having a better idea of the overall state of your code can help track down issues and make better decisions.
One of the other important reasons to write many, smaller tests is that if you test a wide range of scenarios, your code coverage will naturally be high.
What tests would I write for this?
For that first test, I'd write it along these lines
Parting advice
Once you're comfortable with writing triggers like this (and writing tests), then the next step you should take is to use a trigger framework.
Best practice here is to have only one trigger per object (you can have multiple triggers, but they are not guaranteed to be run in any particular order), and to keep the trigger logic-free.
Triggers are harder to test than Apex classes. Triggers can only be executed by performing DML. As you continue to customize your org, the number of requirements that you have to satisfy to create your test data will increase.
Ideally, your trigger would look something like this
with
MyFramework.run()
using the trigger operation type to decide which handler method to call, and the handler method dictating the code to be run (or classes to be executed).Calling a method in an Apex class is more flexible. If you have the handler methods take lists and/or maps instead of directly using trigger context variables, then your code is independent of the trigger (which can really help if you need to set up a situation for a test that would otherwise be difficult or impossible to do).