19

Question rewritten and complete example code provided

I am trying to write test methods for an aura implementation but am running into a roadblock where the JSON serialization/deserialization between Aura and Apex are producing different results.

I am curious to know if my aura implementation is wrong of if there is something else I am doing wrong.

The interface

public interface theInterface {
    void anImplementedMethod();
    String getName();
}

The Class that implements said interface

public with sharing class classThatImplements implements theInterface{
    @AuraEnabled public string getName(){
        return 'classThatImplements';
    }

    @AuraEnabled public String somethingPreset {get { return 'preset'; }set;}

    public static void anImplementedMethod(){
        System.debug('I am doing something');
    }

}

The Lightning Component

<aura:component description="testComponent" implements="force:hasRecordId" controller="auraController">

    <aura:handler name="init" value="{!this}" action="{!c.init}"/>
    <aura:attribute name="returnedObject" type="theInterface" description="the returned interface implementation"/>
    <aura:attribute name="result" type="String"/>


    <lightning:button variant="brand" label="Do it" onclick="{!c.callController}"/>

    {!v.result}
</aura:component>

The Lightning Controller

({
    init: function (component, event, helper) {

        var response = component.get("c.getClassThatImplements");

        response.setCallback(this, function (returnValue) {
            if (component.isValid() && returnValue.getState() === 'SUCCESS') {
                var returnedData = returnValue.getReturnValue();

                //This will show that the name property has been set
                console.log(JSON.stringify(returnedData));

                component.set("v.returnedObject", returnedData);


            } else if (component.isValid() && returnValue.getState() === 'ERROR') {
                alert(returnValue.getError()[0].message);
            }

        });
        $A.enqueueAction(response);

    },
    callController: function(component, event, helper){
        var doIt = component.get("c.consumeClassThatImplements");

        doIt.setParams({
            "objectJSON": JSON.stringify(component.get("v.returnedObject"))
        });

        //Do the initial update to the profile to ready for Chargent Use
        doIt.setCallback(this, function (rtn) {
            if (component.isValid() && rtn.getState() === 'SUCCESS') {
                component.set("v.result",rtn.getReturnValue());
            } else if (component.isValid() && rtn.getState() === 'ERROR') {
                alert(rtn.getError()[0].message);
            }

        });
        $A.enqueueAction(doIt);

    },

})

During init of the aura component I call to the controller and get the results. The results are output to the console and assigned to the attribute..

Console Output - NOTE: name is populated

{"name":"classThatImplements","somethingPreset":"preset"}

This is what I want, seems like it is using the aura enabled property to get the name and populate it as a property.

Now comes the apex part

Apex Controller

public class auraController {

    /**
     * This will return a class that implements the interface.
     * It is meant to determine which class to instantiate as there would be several classes that implement the interface
     * Which one is determined by the record type of an object that would be passed into this method
     *
     * @return Object A generic Object that can be return to Aura.
     */
    @AuraEnabled public static Object getClassThatImplements() {
        try {
            return (theInterface)New classThatImplements();
        }catch(Exception e){
            AuraHandledException aex = New AuraHandledException(e.getMessage());
            aex.setMessage(e.getMessage());
            throw aex;
        }

    }

    /**
     * Takes the JSON representation of the implemented Interface Class
     *
     * @param objectJSON
     *
     * @return
     */
    @AuraEnabled public static String consumeClassThatImplements(String objectJSON) {
        try {
            //Since we cannot use an interface for the Apex Type need to figure out the class name and deserialze casting to interface type
            theInterface controller = (theInterface) json.deserialize(objectJSON, Type.forName(getClassName(objectJSON)));
            controller.anImplementedMethod();
            return 'Look ma no errors';
        }catch(Exception e){
//            We Don't expect any aura errors for this demo
//            AuraHandledException aex = New AuraHandledException(e.getMessage());
//            aex.setMessage(e.getMessage());
//            throw aex;
            throw new auraController_Exception(e.getMessage()); //So we actually get a message in the apex demo
        }
    }

    /**
     * Gets the Name property from a class
     *
     * @param objectJSON
     *
     * @return
     */
    @TestVisible private static String getClassName(String objectJSON){
        Map<String,Object> tmp = (Map<String, Object>) JSON.deserializeUntyped(objectJSON);
        //Just for debugging purposes
        for(String key : tmp.keySet()){
            System.debug('Key: ' + key + ' - Value: ' + tmp.get(key));
        }

        String typeName = (String) ( (Map<String, Object>) JSON.deserializeUntyped(objectJSON) ).get('name');

        System.debug('Type Name:' + typeName);

        if(String.isBlank(typeName)){
            throw new auraController_Exception('Well, this did not work as the name property was blank');
        }

        return typeName;


    }

    public class auraController_Exception extends Exception{}

}

From the Aura component I pass the JSON of the attribute to the method consumeClassThatImplements which then deserializes it. In order to do so we need to know the concrete class name, not the interface so the helper method getClassName is called to get the name (remember in aura is was set and output so we know we have it in the JSON)

Doing this during the Aura lifecycle works just fine

Apex Failure

Running the same code path in Apex during tests always returns NULL for the name when I try to get it. Here is the code to use to show that doing the same thing (minus the Apex <=> Aura communication)

String tmp = json.serialize(auraController.getClassThatImplements());
System.debug(auraController.consumeClassThatImplements(tmp));

The above results in:

Error on line 41, column 1: auraController.auraController_Exception: Well, this did not work as the name property was blank

So my question is:

  1. Why does the Name property get populated in Aura during deserialization from Apex and assignment to the attribute - I am glad it does
  2. Why during apex outside of Aura does the Name property always return null?

Is one a bug, are they both working as expected?

I took inspiration from here: Cannot deserialize JSON as abstract type?

But the fact that during Apex tests it does not behave as it does in aura concerns me.

I have shared the project on GitHub so all you need to do to test is:

  1. Create a scratch org
  2. Clone the repo and push to scratch org
  3. Open scratch org
  4. hit /apex/vfTestPage
  5. look at the console and verify that name is present
  6. Click "Do It" to call the Apex Controller method
  7. Observe that "Look ma no errors" is presented (successfully ran)

Then

In dev console execute

String tmp = json.serialize(auraController.getClassThatImplements());
System.debug(auraController.consumeClassThatImplements(tmp));

An you will get an error thrown which will show that in apex only JSON serialization.deserialization the name property is not available

  • Do you use exact same json string in your test that you get from Lightning Component? Can you provide an example? – steals Nov 18 '17 at 6:21
  • @steals - I use the exact same methods yes. The only difference is in Aura when assigning the results to the attribute it sets the name property and when passed back to the controller name is populated. In Apex, the name property never gets set on serialization. I'll try to work up an example that can be installed independently but it seems to be Aura is firing the getName() method and setting the property but Apex JSON.serialize does not.... – Eric Nov 18 '17 at 6:24
  • 1
    Because you're creating an instance, try declaring your variable as static to see if that doesn't resolve the issue you're having. Similar issue happens with chronTrigger when using instance methods to schedule jobs if you don't use a static variable. The original string doesn't persist in the instance class when tested in unit tests. – crmprogdev Nov 18 '17 at 13:06
  • @crmprogdev - I have rewritten the question will a git repo as well. Static has nothing to do with it (and did not change anything when I tried it). The question was difficult to follow. Hope it is clearer now – Eric Nov 19 '17 at 19:05
  • I would be interested in the reason for the downvote.....so whomever did so I am all ears – Eric Nov 23 '17 at 20:39
10
+200

There are four different things that work different and should not be mixed up:

  1. Attributes visible to Apex (System.debug(object))
  2. Attributes visible to JSON.serialize
  3. Attributes visible to Aura
  4. Attributes visible to VisualForce

At the end of this post, you will find the class with different types of attributes (public; getter; getMethod; auraEnabled; transient). But first I want to point out the differences between these four possible outputs.

1) Apex Debug: Only declared attributes are printed. getAttribute methods aren't visible if we debug the object itself, not even {get{return x;}} is executed (null). They only get executed, when we access them directly.

Thing:[auraEnable=x, getter=null, publc=x, transi=x]

2) JSON: JSON.serialize uses the same information, so it only sees declared attributes, but also knows how to call their getters get{return x;}, so we are one step further. (You can use transient to hide it from JSON).

{"publc":"x","getter":"x","auraEnable":"x"}

3) Aura: Aura sees everything, that is @auraEnabled and knows how to call the getters to get its information. It is not using JSON.serialize()!

Object { method: "x", auraEnable: "x", getter: "x" }

4) Visualforce: sees all attributes including transient and (oddly) also auraEnabled (but doesn't know how to execute getters without direct calls.) In fact VF sees everything that is also outputted in debug(thing);

Thing:[auraEnable=x, getter=null, publc=x, transi=x] 

The Answer:

Aura seems to be the smartest of all these possibilities to output an object. It is the only one that executed both getter methods. JSON.serialize only executed the public get{}.

So in your case, I would suggest replacing your interface with an abstract class using the template method pattern:

public abstract class theInterface{
    public String className { public get{ return getClassName(); } set; }

    public abstract String getClassName();
}

public class ImplementingThing extends theInterface{
    @auraEnabled
    public override String getClassName() {
        return ImplementingThing.class.getName();
    }
}

This would work in Aura and in your Test. Be aware that there are some issues about abstract classes and their properties in Lightning. Here are some more ways to get the classes name.


This is the code to verify all this:

public class ObjectCtrl {

    @auraEnabled
    public static Thing getThing() {
        Thing result = new Thing();

        System.debug(result);
        System.debug(JSON.serialize(result));

        return result;
    }

    public class Thing {
        public String publc = 'x';

        @auraEnabled
        public String getter { public get{ return 'x'; } set; }

        @auraEnabled
        public String getMethod() {
            return 'x';
        }

        @auraEnabled
        public String auraEnable = 'x';

        transient
        public String transi = 'x';
    }
}

Visual Force:

<apex:page controller="ObjectCtrl">
    {!thing}
</apex:page>

Lightning App (Aura):

<aura:application controller="ObjectCtrl">
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
</aura:application>

({
    onInit: function(cmp, evt, helper) {
        var action = cmp.get("c.getThing");

        action.setCallback(this, function(response) {
            console.log(response.getReturnValue());
        });

        $A.enqueueAction(action);
    },
})
| improve this answer | |
  • So basically you are saying that what I am seeing is expected. The Aura will be able to get the "Name" property by executing (or whatever) the get method on return of the "Class Object" but Apex cannot see it. I made it static to be more correct but it did not change the outcome of the test which takes Aura out of the equation. Only when the entry point is Aura does the Name property get passed to apex... – Eric Nov 23 '17 at 19:32
  • Yes, I think so. So in allignment with the finding I would suggest making the getName Method a getter. I included it in the answer. – Basti Nov 23 '17 at 20:06
  • I cannot make it a getter because I cannot enforce that via the interface....Stuck with method only....I could do the get; set; but it breaks the whole reason for the interface in this use case. – Eric Nov 23 '17 at 20:09
  • How about replacing the interface with an abstract class implementing the given getter? – Basti Nov 23 '17 at 20:11
  • 1
    Thanks for the time you put into the answer. I think it has played out and we are just at it is what it is........I started a new question to get and the next phase taking into account that JSON simply cannot do what Aura does when passing objects that are not concrete – Eric Nov 23 '17 at 20:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.