1

I have the following Apex Rest Endpoint that is working from remote systems.

@RestResource(urlMapping='/EmailOpens/*')
global with sharing class RsmEmailOpens {

  // inbound JSON payload is an array/List named "opens" of this type
  global class CnameEmail {
    public String cname;
    public String email;
  }

  // outbound response type
  global class Response {
    public String message;
    public Integer updates;
    public String cname;
    public String email;
    public String exception_msg;
  }

  @HttpPatch
  global static Response RsmIncrementEmailOpens(List<CnameEmail> opens) {
    Response response = new Response();
    Integer updates = 0;
    String error_msg = '';

    for (CnameEmail i : opens) {  
      try {
        error_msg = 'Lead.Email not found';  // message if the following fails
        Lead lead = [SELECT Id FROM Lead WHERE Email =: i.email];

        error_msg = 'Lead.Email found; Campaign.Name not found';
        Campaign campaign = [SELECT Id FROM Campaign WHERE Name =: i.cname];

        error_msg = 'Lead.Email & Campaign.Name found; CampaignMember with (Campaign,Lead) pair not found';
        CampaignMember cm = [SELECT rsm_EmailOpenCount__c FROM CampaignMember 
          WHERE CampaignId =: campaign.Id AND LeadId =: lead.Id];

        if(cm.rsm_EmailOpenCount__c == null) cm.rsm_EmailOpenCount__c = 0;
        cm.rsm_EmailOpenCount__c += 1;
        update cm;

      } catch(Exception e) {
          response.message = error_msg;
          response.updates = updates;
          response.cname = i.cname;
          response.email = i.email;
          response.exception_msg = e.getMessage();         
          return response;
      }
      updates++;
    }

    response.message = 'Successful';
    response.updates = updates;  
    response.cname = Null;
    response.email = Null;
    response.exception_msg = Null;
    return response;
  }
}

I've also started a test case for this below. I'm having problems passing the JSON string into a parameter to RsmIncrementEmailOpens(). If I can figure this out, I believe I will have a complete solution.

@istest
private class RsmEmailOpensTestClass {

  @testSetup static void setup() {

    List<Lead> leads = new List<Lead>{
      new Lead(Company = 'Unknown', LastName = 'Mike', Email = '[email protected]'),
      new Lead(Company = 'Unknown', LastName = 'John', Email = '[email protected]')
      };  
    insert leads;

    Campaign c = new Campaign(Name = 'Test Devotion');
    insert c;

    List<CampaignMember> cm = new List<CampaignMember>{
      new CampaignMember(CampaignId = c.Id, LeadId = leads[0].Id, rsm_EmailOpenCount__c = 100),
      new CampaignMember(CampaignId = c.Id, LeadId = leads[1].Id, rsm_EmailOpenCount__c = 200)
      };
    insert cm;  

  }

  @isTest static void testEmailOpen() {

    String JSONMsg = '{"opens":[{"cname":"Test Devotion","email":"[email protected]"},{"cname":"Test Devotion","email":"[email protected]"}]}';

    RestRequest req = new RestRequest();
    RestResponse res = new RestResponse();

    req.requestURI = '/services/apexrest/EmailOpens/';
    req.httpMethod = 'PATCH';
    req.requestBody = Blob.valueof(JSONMsg);

    RestContext.request = req;
    RestContext.response= res;

    Test.startTest();
    // I understand that for unit testing the method should be called directly
    // I am receiving a Type Mismatch on the call below
    // How can I correct this?
    //RsmEmailOpens.RsmIncrementEmailOpens(JSONMsg);
    Test.stopTest();
    }
  }

Thank you for your help.

7
  • Shouldn't you be sending the json as expected in your contstructor (List<CnameEmail>). You may rather like to use req.requestBody = <ListCnameEmail> JSON.deserialize(jsonmsg, List<CnameEmail>.class); Can you try this out and see if this works?
    – Jayant Das
    Commented May 3, 2018 at 15:37
  • Thank you for your feedback. Yes I will try this. The problem I have now is the testcase will not compile so I can not try anything. I agree that I need to pass List<CnameEmail> to RsmIncrementEmailOpens. The problem is that I do not know how to do this -- which is why the call is commented above. Please know the JSONMsg above is exactly what I am sending from remote systems. Once I get this test case to compile, I will try your suggestion and let you know.
    – Greg Sims
    Commented May 3, 2018 at 15:47
  • What is the compilation error you are getting?
    – Jayant Das
    Commented May 3, 2018 at 15:48
  • Error: Compile Error: Method does not exist or incorrect signature: void RsmIncrementEmailOpens(String) from the type RsmEmailOpens at line 36 column 19. I believe is is complaining about the type mismatch between String and List<CnameEmail>.
    – Greg Sims
    Commented May 3, 2018 at 15:50
  • Okay so you are having it wrong here, it's not a method but a constructor. So you need to basically put it like RsmIncrementEmailOpens obj = new RsmIncrementEmailOpens(cnameEmailList) and the parameter here is what you are deserializing from your json response. Once note though - you really don't need to use JSON here, you can just create standard list and sample records in it. Because you are not really testing the API flow here rather only invoking your class for coverage. For anything beyond this, you need to have a mock implementation to test your webservices.
    – Jayant Das
    Commented May 3, 2018 at 15:53

2 Answers 2

1

I can see the signature of your method as

 @HttpPatch
  global static Response RsmIncrementEmailOpens(List<CnameEmail> opens) 

which mean you can only send parameter of type List<CnameEmail>

In Live: When you call the rest service via Postman or any Rest client with JSON body, it hits the Java layer first before it reaches APEX class. Java layer tries to parse the JSON as the object param you have specified and pass it to Apex if successful. If you pass malformed JSON/XML it breaks at JAVA layer and your apex code is never called.

In test Environment: In your test class, you directly call the Apex class method by surpassing the JAVA parsing layer and hence you have to pass the parameter as an object rather than raw JSON.

Is there a way to test without passing object and just JSON?

Yes.. it is. But you have to change the method signature of your HttpPatch method.

@HttpPatch
  global static Response RsmIncrementEmailOpens() {
            RestRequest req = RestContext.request;
            Blob body = req.requestBody;
            String sampleJSON = body.toString();
            List<CnameEmail> opens = (List<CnameEmail>) JSON.deserialize(sampleJSON,List<CnameEmail>.class);

            //Your future logic

}

And then call your test method without parameter.

@isTest static void testEmailOpen() {

    String JSONMsg = '[{"cname":"Test Devotion","email":"[email protected]"},{"cname":"Test Devotion","email":"[email protected]"}]';

    RestRequest req = new RestRequest();
    RestResponse res = new RestResponse();

    req.requestURI = '/services/apexrest/EmailOpens/';
    req.httpMethod = 'PATCH';
    req.requestBody = Blob.valueof(JSONMsg);

    RestContext.request = req;
    RestContext.response= res;

    Test.startTest();
    RsmEmailOpens.Response rep= RsmEmailOpens.RsmIncrementEmailOpens();
    Test.stopTest();
    //Assert the fields.
    }
7
  • Thank you for your answer @Pranay!! I have implemented this and it is running from the remote system well. I also have 61% test case coverage and counting. It is good to move into the next phase of this effort. Now to find 14% of coverage. Thanks again!!
    – Greg Sims
    Commented May 3, 2018 at 18:33
  • Is it possible to gain access to the structured response created RsmEmailOpens.RsmIncrementEmailOpens() for normal and exception cases? I would like to System.assertEquals() values contained in the response. For example, the Good Path creates response.message = 'Successful';
    – Greg Sims
    Commented May 3, 2018 at 19:02
  • Create a new test method. And the request JSON provide email id([email protected]) that does not exist. It will thow, "List has No Rows for Assignment Exception" and you will be able to cover catch block. Commented May 3, 2018 at 19:05
  • Yes, I have done this for: Good Path, Bad Email, Bad Campaign and Bad CampaignMember. This is all working. What I can't figure out is how to perform System.assertEquals() against portions of the response -- even the good path case. I believe this maybe what is holding my coverage down.
    – Greg Sims
    Commented May 3, 2018 at 19:08
  • updated code for assert support., If you want to assert JSON, you have to change the method signature, Commented May 3, 2018 at 19:12
1

You are headed in the right direction with your approach. What you really want to achieve with unit tests is to make sure your implementation works functionally as expected. However in these kind of scenarios, you also want to test if your implementation works technically.

Here are few things that will help you to move ahead in this direction:

  1. Implement a test data factory. You can also refer to trailhead modules around this here and find more details on the documentation here. This approach helps you centralize all your data creation while you concentrate on the test. This resolves your concern around data creation.

  2. Testing your APEX rest class: it's as good as testing any other Apex class, but this is where you want to "mock" as it has been implemented. Refer to this module on trailhead for some pointers. This is an excerpt from the trailhead:

    Testing your Apex REST class is similar to testing any other Apex class—just call the class methods by passing in parameter values and then verify the results. For methods that don’t take parameters or that rely on information in the REST request, create a test REST request.

As for your compilation error - it's because of the invalid type that you are passing to the method (and my bad, I had thought the method to be a constructor first).

So once you have the right data set created, you can just utilize that data to send it to your method.

If you want to use JSON for this purpose,you need to make sure to send it as your method expects, which in your case is List of CnameEmail. You can find more on JSON methods on the documentation.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .