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:
- Why does the Name property get populated in Aura during deserialization from Apex and assignment to the attribute - I am glad it does
- 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:
- Create a scratch org
- Clone the repo and push to scratch org
- Open scratch org
- hit
/apex/vfTestPage
- look at the console and verify that name is present
- Click "Do It" to call the Apex Controller method
- 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