Reputation: 11
I am looking to write an Aspect which can be used to mock data depending on some value passed in the method. This mock will replace the actual REST call. If the value doesn't match, go back to the actual method and make the call to the REST endpoint.
I already have an aspect written that works in production but is very messy and had to implement multiple different methods in there to accommodate the need to return different objects and handle different method arguments. Is there a way I could generalise this so that I could have like a single method to perform the work and able to use if for future cases where I do not have to worry about object types and method signatures.
Current implementation as follows. How could I amend this to work as a single method which can cater to future methods and response types without having to change my Aspect?
To clarify I have the following 3 methods which I want to mock by using Aspect. as you can see I have a custom annotation that takes in a name. None of the method's arguments line up.
@MyMock(name = "ABC")
public AResponse getAResponse(ARequest aRequest){
// make a REST call
}
@MyMock(name = "DEF")
public BResponse getBResponse(String fruit, BRequest bRequest){
// make a REST call
}
@MyMock(name = "GHJ")
public CResponse getCResponse(String vehicle, CRequest cRequest, int id){
// make a REST call
}
The Aspect Class currently is as follows. I use the name value in the annotation to determine which method to call and which value type to return. As you can see this is not very scalable. I need to write a new method and logic every single time I implement new mocks.
@Around("@annotation(myMock)")
public Object getMockedData(ProceedingJoinPoint pjp, MyMock myMock) throws Throwable {
String servName = performMocking.name();
Object[] methodArguments = pjp.getArgs();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Class returnType = signature.getReturnType();
switch (myMock.name()) {
case "ABC":
getA(methodArguments[0]);
break;
case "DEF":
getB(methodArguments[0]);
break;
case "GHJ":
getC(methodArguments[2]);
break;
}
return pjp.proceed(methodArguments);
}
private AResponse getA(ARequest aRequest){
// I use methodArguments[0] (which is aRequest) to decide what value to return as a mock response here.
// There is a getName value in that object which I use to reference
}
private BResponse getB(BRequest bRequest){
// I use methodArguments[0] (which is a String) to decide what value to return as a mock response here.
}
private CResponse getC(CRequest cRequest){
// I use methodArguments[2] (which is an int) to decide what value to return as a mock response here.
}
All the above get methods makes a call to an external JSON file to fetch the mock data. The files content as follows. If the keys match, a mock response would be given else go back to making actual REST call.
{
"ABC": {
"enabled": "true",
"responses": [
{
"a_name": "{some valid json response matching the AResponse Object structure}"
}
]
},
"DEF": {
"enabled": "true",
"responses": [
{
"d_name": "{some valid json response matching the BResponse Object structure}"
}
]
},
"GHJ": {
"enabled": "true",
"responses": [
{
"123": "{some valid json response matching the CResponse Object structure}"
}
]
}
}
EDIT: Added my ASPECT class as follows:
package com.company.a.b.mock;
import com.domain.abc.b.logging.util.MyLogger;
import com.domain.abc.a.util.generic.ConfigHelper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@Aspect
@Component
public class AspectMocking {
private final ConfigHelper configHelper;
private final MyLogger myLogger;
public AspectMocking(ConfigHelper configHelper, MyLogger myLogger) {
this.configHelper = configHelper;
this.myLogger = myLogger;
}
@Around("@annotation(myMock)")
public Object getMockedData(ProceedingJoinPoint pjp, MyMock myMock) throws Throwable {
final String env = System.getProperty("spring.profiles.active");
String response = null;
Object returnObject = null;
String logMessage = null;
String servName = myMock.name();
Object[] methodArguments = pjp.getArgs();
try {
if ("test_env1".equals(env) || "test_env2".equals(env)) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Class returnType = signature.getReturnType();
Map allDataFromMockedFile = getMockedDataFromFile();
Map getResultForKey = (Map) allDataFromMockedFile.get(myMock.name());
List result = null;
if(getResultForKey != null){
result = (List) getResultForKey.get("responses");
}
switch (myMock.name()) {
case "ABC":
response = fetchABCMockedData(result, (String) methodArguments[0]);
logMessage = "Fetching ABC mock data for ABC Response: " + response;
break;
case "V2_ABC":
response = fetchABCMockedData(getIntlResult(myMock.name(), (String)methodArguments[2]), (String) methodArguments[0]);
logMessage = "Fetching V2 ABC mock data for V2 ABC Response: " + response;
break;
case "DEF":
response = fetchDEFMockedData(result, (String) methodArguments[0]);
logMessage = "Fetching DEF mock data: " + response;
break;
case "GHJ":
response = fetchGHJMockedOfferData(result, (String) methodArguments[0], (String) methodArguments[1], (String) methodArguments[2]);
logMessage = "Fetching GHJ mock data: " + response;
break;
}
ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
returnObject = mapper.readValue(response, returnType);
}
} catch (Exception exp) {
myLogger.addMessageAsWarning(String
.format("Exception occured for service %s while loading the mocked json, so hitting the actual service:"
+ exp.getMessage(), servName));
}
if (returnObject == null) {
returnObject = pjp.proceed(methodArguments);
}
myLogger.addMessage(logMessage);
return returnObject;
}
private List getIntlResult(String name, String locale){
Map localBasedMockFile = getMockedDataFromFile(locale.toLowerCase());
Map localeSpecificMockedData = (Map) localBasedMockFile.get(name);
return (List) localeSpecificMockedData.get("responses");
}
// had to add this recently as we needed locale values now to go to a diff file.
private Map getMockedDataFromFile(String countryCode){
final String DATA_URL = String.format("/a/b/c/%s/data.json", countryCode);
return configHelper.getMAVDataAsObject(DATA_URL, Map.class);
}
private Map getMockedDataFromFile() {
final String DATA_URL = "/a/b/c/zh/mock.json";
return configHelper.getMAVDataAsObject(DATA_URL, Map.class);
}
private String fetchABCMockedData(List<Map> allResponses, String vinNumber) throws IOException {
String response = null;
for (Map m : allResponses) {
String mockedVinNumber = m.keySet().toString().replaceAll("[\\[\\]]", "");
if (vinNumber.equals(mockedVinNumber)) {
response = (String) m.get(mockedVinNumber);
}
}
return response;
}
private String fetchDEFMockedData(List<String> allResponses, String vinNumber) {
return allResponses.contains(vinNumber) ? "true" : "false";
}
private String fetchGHJMockedOfferData(List<Map> allResponses, String journey, String name, String pin) {
String response = null;
String key = journey+"_"+name + "_" + pin;
for (Map m : allResponses) {
String mockedKey = m.keySet().toString().replaceAll("[\\[\\]]", "");
if (mockedKey.equals(key)) {
response = (String) m.get(mockedKey);
}
}
return response;
}
}
Upvotes: 1
Views: 264
Reputation: 67297
Let us make your sample code compile first and then continue talking, shall we? Here is my MCVE:
Helper classes:
package de.scrum_master.app;
public class ARequest {}
package de.scrum_master.app;
public class BRequest {}
package de.scrum_master.app;
public class CRequest {}
package de.scrum_master.app;
public class AResponse {
private String content;
public AResponse(String content) {
this.content = content;
}
@Override
public String toString() {
return "AResponse[content=" + content + "]";
}
}
package de.scrum_master.app;
public class BResponse {
private String content;
public BResponse(String content) {
this.content = content;
}
@Override
public String toString() {
return "BResponse[content=" + content + "]";
}
}
package de.scrum_master.app;
public class CResponse {
private String content;
public CResponse(String content) {
this.content = content;
}
@Override
public String toString() {
return "CResponse[content=" + content + "]";
}
}
Marker annotation:
package de.scrum_master.app;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(METHOD)
public @interface MyMock {
String name();
}
Driver application:
package de.scrum_master.app;
public class Application {
@MyMock(name = "ABC")
public AResponse getAResponse(ARequest aRequest) {
// make a REST call
return new AResponse("real A response");
}
@MyMock(name = "DEF")
public BResponse getBResponse(String fruit, BRequest bRequest) {
// make a REST call
return new BResponse("real B response");
}
@MyMock(name = "GHJ")
public CResponse getCResponse(String vehicle, CRequest cRequest, int id) {
// make a REST call
return new CResponse("real C response");
}
public static void main(String[] args) {
Application application = new Application();
System.out.println(application.getAResponse(new ARequest()));
System.out.println(application.getBResponse("apple", new BRequest()));
System.out.println(application.getCResponse("bicycle", new CRequest(), 11));
}
}
Console log without aspect:
AResponse[content=real A response]
BResponse[content=real B response]
CResponse[content=real C response]
Aspect, variant A, using marker annotation:
package de.scrum_master.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import de.scrum_master.app.ARequest;
import de.scrum_master.app.AResponse;
import de.scrum_master.app.BResponse;
import de.scrum_master.app.CResponse;
import de.scrum_master.app.MyMock;
@Aspect
public class MockResponseAspect {
@Around("@annotation(myMock)")
public Object getMockedData(ProceedingJoinPoint pjp, MyMock myMock) throws Throwable {
// Whatever this does...
//String servName = performMocking.name();
Object[] methodArguments = pjp.getArgs();
switch (myMock.name()) {
case "ABC": return getA((ARequest) methodArguments[0]);
case "DEF": return getB((String) methodArguments[0]);
case "GHJ": return getC((int) methodArguments[2]);
default: return pjp.proceed(methodArguments);
}
}
private AResponse getA(ARequest aRequest) {
return new AResponse("mock A response");
}
private BResponse getB(String fruit) {
return new BResponse("mock B response");
}
private CResponse getC(int id) {
return new CResponse("mock C response");
}
}
Console log using aspect:
AResponse[content=mock A response]
BResponse[content=mock B response]
CResponse[content=mock C response]
Aspect, variant B, using return type:
This is a simplified version of an aspect not using the mock annotation at all. Instead it could use the method name like in my example or maybe another existing method annotation like @Response
or whatever else all response methods have in common.
package de.scrum_master.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import de.scrum_master.app.ARequest;
import de.scrum_master.app.AResponse;
import de.scrum_master.app.BResponse;
import de.scrum_master.app.CResponse;
@Aspect
public class MockResponseAspect {
@Around("execution(* get*Response(..))")
public Object getMockedData(ProceedingJoinPoint pjp) throws Throwable {
// Whatever this does...
//String servName = performMocking.name();
Object[] methodArguments = pjp.getArgs();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Class<?> returnType = signature.getReturnType();
switch (returnType.getSimpleName()) {
case "AResponse": return getA((ARequest) methodArguments[0]);
case "BResponse": return getB((String) methodArguments[0]);
case "CResponse": return getC((int) methodArguments[2]);
default: return pjp.proceed(methodArguments);
}
}
private AResponse getA(ARequest aRequest) {
return new AResponse("mock A response");
}
private BResponse getB(String fruit) {
return new BResponse("mock B response");
}
private CResponse getC(int id) {
return new CResponse("mock C response");
}
}
The console log is the same as for the first aspect.
Now we have something to discuss. What is it you do not like about this? What do you want to achieve?
Upvotes: 1