Create a service mocking framework around Proxy.newProxyInstance
#java #reflection #testingIntroducing Proxy.newProxyInstance
The Java Reflection API provides a helper method to create proxy implementation (substitute or placeholder for another object[1]) of any class.
The idea is to provide a list of classes or interfaces that this new proxy would inherit/implement and provide a callback which will be invoked every time the proxy is called.
Code Example
With this knowledge and the documentation, let’s see if this in action and understand more from there.
private interface Interface1 {
void method1();
}
private interface Interface2 {
void method2();
}
public static void main(String[] args) {
Object o = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{Interface1.class, Interface2.class}, (proxy, method, args1) -> {
System.out.println(method.getDeclaringClass());
System.out.println(method.getName());
return null;
});
Interface1 i1 = (Interface1) o;
i1.method1();
Interface1 i2 = (Interface1) o;
i2.method2();
}
(Gist is present here: https://gist.github.com/sureshsarda/7411796686f44eb5cd3d56e35e93c2f0)
In the above code snippet,
- We have two interfaces -
Interface1
andInterface2
both having two different methods -method1
andmethod2
- We then create an object, a proxy object using
Proxy.newProxyInstance
and pass it the following items - a class loader, an array of interfaces this new object should implement and a callback or more precisely anInvocationHandler
; basically a class that will be delegated an calls to the methods of this proxy class. - Then we cast this object
o
to these interfaces and call the respective methods from them.
When we run this snippet, we see the Interface that this method belongs to and the name of the method. We also get the parameters as third argument to this handler but I have skipped that part.
interface com.pracman.Application$Interface1
method1
interface com.pracman.Application$Interface2
method2
Detailing the use case
Armed with this information, let’s try to build a small utility that will mock responses of a Service using static JSON responses. While testing we generally make calls to third party API or to the database which is time consuming. A faster and more robust way is to mock the response for this. Generally, we use mocks provided by these APIs or use an in memory database or using a mocking library like Mockito. But sometimes we just want something easy to set up and reusable.
Requirements
The requirements are simple, we need a utility that will mock Services and return hard coded responses from a JSON files.
- We should be able to substitute any Service class with the mock
- The mocked class should return responses stored in JSON files from a predefined location
- To keep it simple for now, we will have simple parameters (how simple? you will see this later when we build it)
First Draft
So, what we need for this utility? Well, we need an InvocationHandler
that will return responses from a static directory. For now we will use the resources
directory.
public class StaticApiMock implements InvocationHandler {
private String baseDirectory;
public StaticApiMock(String baseDirectory) {
this.baseDirectory = baseDirectory;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// use the baseDirectory to get the file path and read from it
String filePath = baseDirectory + File.separator + method.getName() + ".json";
InputStream resourceAsStream = proxy.getClass().getClassLoader().getResourceAsStream(filePath);
assert resourceAsStream != null;
String contents = new String(resourceAsStream.readAllBytes());
return contents;
}
}
Now this can be used from anywhere and we have a neat namespacing or grouping using directories which we can use. A sample invocation will look something like this:
StaticApiMock mock = new StaticApiMock("users"); // users is a directory under which the json files will reside
UserService service = (UserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{UserService.class}, mock);
List<UserService.User> allUsers = service.getAllUsers();
But this fails. The invoke
method returns a String
and we expect List of Users. We need to parse and create the objects. We will use Gson
and the return type of the method to achieve this:
GSON.fromJson(contents, method.getGenericReturnType());
And it works! It returns the contents of the file prenset in that method name. A complete code for this is present in this Github Repository.
The call to this utility is still wholesome. We first create an instance of StaticApiMock
and then uses a really long method call to create the mock. This can be improved by creating a factory method. Something like this can save a lot of typing:
public static <T> T createMock(String baseDirectory, Class<T> tClass) {
StaticApiMock mock = new StaticApiMock(baseDirectory);
return tClass.cast(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{tClass}, mock));
}
Two things to observe here:
- We are casting the object created using
Class<T>.cast
method - We are returning a generic type so we don’t have to cast to
UserService
later
After this, creating a mock is as simple as doing this:`
UserService service = StaticApiMock.createMock("users", UserService.class);
Adding Features
This is a very rudementary implementation. We can improve it further by adding a host of new features, like filtering on arguments, calling different methods when different arguments are present to create a more reusable utility. But we will look at this in a future post.
Conclusion
A proxy pattern is quite powerful when we want to swap implementations. Creating these proxies dynamically is even more powerful and Java reflection provides just a neat way to achieve this. In this article we saw how to leverage Proxy.newProxyInstance
to quickly create mock services and return response from mocked JSON files.
References and Further Reading
- https://refactoring.guru/design-patterns/proxy
- https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html
This is my 2nd of 100 post in #100daysToOffload.