Skip to main content

Hi Salesforce Community,

I have created an Apex class named SingleEmailLimitController to check the daily single-email limit in my Salesforce organization. The class retrieves the email limit using System.OrgLimits.getMap() and returns the daily limit, emails used, emails remaining, and a status message.

I am trying to write an Apex test class to achieve 100% code coverage, but I am facing difficulties covering all the conditional branches and the exception-handling block.

My main challenges are:

  1. Testing the scenario where limits.get('SingleEmail') returns null.
  2. Covering the condition where the daily email limit has been reached or exceeded.
  3. Covering the condition where 10 or fewer emails remain.
  4. Covering the condition where sufficient email capacity remains.
  5. Covering the catch (Exception e) block.
  6. Testing these scenarios without depending on the actual email usage and limits of the Salesforce organization.

Since System.OrgLimits.getMap() returns system-provided limit information, I am unsure how to simulate these different scenarios in an Apex test.

My Apex controller:

public with sharing class SingleEmailLimitController {

public class EmailLimitResponse {

@InvocableVariable(label='Daily Single Email Limit')

public Integer dailyLimit;

@InvocableVariable(label='Emails Used')

public Integer emailsUsed;

@InvocableVariable(label='Emails Remaining')

public Integer emailsRemaining;

@InvocableVariable(label='Limit Status Message')

public String statusMessage;

}

@InvocableMethod(

label='Check Single Email Limit'

description='Checks daily email usage and returns a status message.'

)

public static List<EmailLimitResponse> checkSingleEmailLimit(

List<String> requests

) {

EmailLimitResponse result = new EmailLimitResponse();

List<EmailLimitResponse> results =

new List<EmailLimitResponse>();

try {

Map<String, System.OrgLimit> limits =

System.OrgLimits.getMap();

System.OrgLimit emailLimit = limits.get('SingleEmail');

if (emailLimit == null) {

result.statusMessage =

'Unable to retrieve the daily email limit.';

} else {

result.dailyLimit = emailLimit.getLimit();

result.emailsUsed = emailLimit.getValue();

result.emailsRemaining =

Math.max(

0,

result.dailyLimit - result.emailsUsed

);

if (result.emailsUsed >= result.dailyLimit) {

result.statusMessage =

'Daily email limit exceeded. No email capacity remains.';

} else if (result.emailsRemaining <= 10) {

result.statusMessage =

'Daily email limit almost reached. Only ' +

result.emailsRemaining + ' emails remaining.';

} else {

result.statusMessage =

'Email capacity available. ' +

result.emailsRemaining +

' emails remaining out of ' +

result.dailyLimit + '.';

}

}

} catch (Exception e) {

result.statusMessage =

'Unable to check email limit: ' + e.getMessage();

}

results.add(result);

return results;

}

}

My questions:

  • Is it possible to achieve 100% code coverage for this controller without changing its implementation?
  • If not, what is the recommended approach for making the limit values testable?
  • How can I write a complete Apex test class that covers all the conditional branches and the exception-handling block?
  • Would using a @TestVisible helper method or an injectable service be a suitable solution?

Please share a working example of the controller changes, if necessary, and the corresponding Apex test class.

Thank you for your help! 

 

#Salesforce Developer  #Salesforce Admin  #Trailhead Challenges  #Trailhead

1 个回答
  1. 10月16日 15:07

    @HARIPRASAD Km I don't believe there is a way to achieve 100% without changing the class... first - emailLimit will never be null, so you will not get into that block. There is also no way to set org limits from a test, so the 'close to limit' and 'at limit' branches are not going to hit. I also don't see anyway you would end up in the catch block - nothing in here would fail.

     

    There are some things you can do to refactor that could help, but you'd essentially just be making the changes to get higher coverage, when you could easily get the 75% keeping the class as is. Regardless, if that is your goal, I'll try to help:

     

     The problem is this line:

    Map<String, System.OrgLimit> limits = System.OrgLimits.getMap(); 

     

    The class fetches those numbers itself, so a test can’t get in between. Flip it around - define a small interface with getDailyLimit() and getEmailsUsed(), put the System.OrgLimits call in one class that implements it, and hold that in a @TestVisible private static field on the controller. Production uses the real one by default, and your test reassigns the field to a mock. Your if/else logic doesn’t change at all. 

     

    From there 5000/4995 hits “almost reached”, 5000/5000 hits exceeded, 5000/100 hits the normal path, nulls hit “unable to retrieve”, and a mock whose getter throws hits the catch. Keep one test that doesn’t swap the provider so the real one gets covered too.

     

     

    Here are the classes: 

    public with sharing class SingleEmailLimitController {

    public class EmailLimitResponse {

    @InvocableVariable(label='Daily Single Email Limit')

    public Integer dailyLimit;

    @InvocableVariable(label='Emails Used')

    public Integer emailsUsed;

    @InvocableVariable(label='Emails Remaining')

    public Integer emailsRemaining;

    @InvocableVariable(label='Limit Status Message')

    public String statusMessage;

    }

    public interface LimitProvider {

    Integer getDailyLimit();

    Integer getEmailsUsed();

    }

    public class OrgLimitProvider implements LimitProvider {

    public Integer getDailyLimit() {

    System.OrgLimit l = System.OrgLimits.getMap().get('SingleEmail');

    return l == null ? null : l.getLimit();

    }

    public Integer getEmailsUsed() {

    System.OrgLimit l = System.OrgLimits.getMap().get('SingleEmail');

    return l == null ? null : l.getValue();

    }

    }

    @TestVisible

    private static LimitProvider provider = new OrgLimitProvider();

    @InvocableMethod(

    label='Check Single Email Limit'

    description='Checks daily email usage and returns a status message.'

    )

    public static List<EmailLimitResponse> checkSingleEmailLimit(List<String> requests) {

    List<EmailLimitResponse> results = new List<EmailLimitResponse>();

    if (requests == null || requests.isEmpty()) {

    return results;

    }

    EmailLimitResponse evaluated = evaluate();

    for (Integer i = 0; i < requests.size(); i++) {

    EmailLimitResponse r = new EmailLimitResponse();

    r.dailyLimit = evaluated.dailyLimit;

    r.emailsUsed = evaluated.emailsUsed;

    r.emailsRemaining = evaluated.emailsRemaining;

    r.statusMessage = evaluated.statusMessage;

    results.add(r);

    }

    return results;

    }

    @TestVisible

    private static EmailLimitResponse evaluate() {

    EmailLimitResponse result = new EmailLimitResponse();

    try {

    Integer dailyLimit = provider.getDailyLimit();

    Integer emailsUsed = provider.getEmailsUsed();

    if (dailyLimit == null || emailsUsed == null) {

    result.statusMessage = 'Unable to retrieve the daily email limit.';

    } else {

    result.dailyLimit = dailyLimit;

    result.emailsUsed = emailsUsed;

    result.emailsRemaining = Math.max(0, dailyLimit - emailsUsed);

    if (emailsUsed >= dailyLimit) {

    result.statusMessage =

    'Daily email limit exceeded. No email capacity remains.';

    } else if (result.emailsRemaining <= 10) {

    result.statusMessage =

    'Daily email limit almost reached. Only ' +

    result.emailsRemaining + ' emails remaining.';

    } else {

    result.statusMessage =

    'Email capacity available. ' + result.emailsRemaining +

    ' emails remaining out of ' + dailyLimit + '.';

    }

    }

    } catch (Exception e) {

    result.statusMessage = 'Unable to check email limit: ' + e.getMessage();

    }

    return result;

    }

    }

     

     

    And Test Class

    @IsTest

    private class SingleEmailLimitControllerTest {

    private class MockException extends Exception {}

    private class MockProvider implements SingleEmailLimitController.LimitProvider {

    private Integer dailyLimit;

    private Integer emailsUsed;

    private MockProvider(Integer dailyLimit, Integer emailsUsed) {

    this.dailyLimit = dailyLimit;

    this.emailsUsed = emailsUsed;

    }

    public Integer getDailyLimit() { return dailyLimit; }

    public Integer getEmailsUsed() { return emailsUsed; }

    }

    private class ThrowingProvider implements SingleEmailLimitController.LimitProvider {

    public Integer getDailyLimit() { throw new MockException('simulated failure'); }

    public Integer getEmailsUsed() { return null; }

    }

    private static List<SingleEmailLimitController.EmailLimitResponse> run() {

    return SingleEmailLimitController.checkSingleEmailLimit(new List<String>{ 'go' });

    }

    @IsTest

    static void limitUnavailableReturnsMessage() {

    SingleEmailLimitController.provider = new MockProvider(null, null);

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results = run();

    Test.stopTest();

    Assert.areEqual(

    'Unable to retrieve the daily email limit.',

    results[0].statusMessage

    );

    Assert.isNull(results[0].dailyLimit);

    }

    @IsTest

    static void limitExceededReturnsMessage() {

    SingleEmailLimitController.provider = new MockProvider(5000, 5000);

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results = run();

    Test.stopTest();

    Assert.areEqual(0, results[0].emailsRemaining);

    Assert.isTrue(results[0].statusMessage.contains('limit exceeded'));

    }

    @IsTest

    static void almostReachedReturnsMessage() {

    SingleEmailLimitController.provider = new MockProvider(5000, 4995);

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results = run();

    Test.stopTest();

    Assert.areEqual(5, results[0].emailsRemaining);

    Assert.isTrue(results[0].statusMessage.contains('almost reached'));

    }

    @IsTest

    static void capacityAvailableReturnsMessage() {

    SingleEmailLimitController.provider = new MockProvider(5000, 100);

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results = run();

    Test.stopTest();

    Assert.areEqual(4900, results[0].emailsRemaining);

    Assert.isTrue(results[0].statusMessage.contains('Email capacity available'));

    }

    @IsTest

    static void exceptionIsCaught() {

    SingleEmailLimitController.provider = new ThrowingProvider();

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results = run();

    Test.stopTest();

    Assert.isTrue(results[0].statusMessage.startsWith('Unable to check email limit:'));

    }

    @IsTest

    static void outputSizeMatchesInputSize() {

    SingleEmailLimitController.provider = new MockProvider(5000, 100);

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results =

    SingleEmailLimitController.checkSingleEmailLimit(

    new List<String>{ 'a', 'b', 'c' }

    );

    Test.stopTest();

    Assert.areEqual(3, results.size());

    Assert.areEqual(4900, results[2].emailsRemaining);

    }

    @IsTest

    static void emptyInputReturnsEmptyList() {

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results =

    SingleEmailLimitController.checkSingleEmailLimit(new List<String>());

    Test.stopTest();

    Assert.areEqual(0, results.size());

    }

    @IsTest

    static void realProviderReturnsAResult() {

    Test.startTest();

    List<SingleEmailLimitController.EmailLimitResponse> results = run();

    Test.stopTest();

    Assert.areEqual(1, results.size());

    Assert.isNotNull(results[0].statusMessage);

    }

    }

     

     

    Let me know if this all makes sense. Happy to talk through it more

0/9000