Experiments with mocking static methods via mockito - moltmann/dexmaker GitHub Wiki
On Android/asop-master using InlineDexmakerMockMaker, but this should apply to other JVMs as well.
When mocking static methods the scope of the mocking is not tied to the mocked object as there is no associated object.
Still I wanted an interface that is
- As similar as possible to mocking of objects
- Flexible enough to allow all use cases
- Mocking a whole test-class (i.e. in @BeforeClass)
- Mocking for a single test
- Mocking for a part of a test
- Using as little code as possible
I ended up with a method pair mockStatic/spyStatic that adds mocking to the static methods of a class.
OngoingStaticStubbing<MyClass> token = spyStatic(MyClass.class);
The token is used to define the scope of the mocking. It is AutoCloseable so you can do:
try (StaticMockingInProgress<MyClass> token = spyStatic(MyClass.class)) {
when(MyClass.aStaticMethod()).thenReturn(“mockedValue”);
assertEquals(“mockedValue”, MyClass.aStaticMethod());
}
assertEquals(“originalValue”, MyClass.aStaticMethod());
I did not look into the following topics, but it might be worth considering:
- Allow to restrict mocking to a certain (usually the current) thread to not impact code that is not tested. This feature does not need to be restricted to static mocking, but can also be used for object-mocking.
- Allow to reset all StaticMockingInProgress’ via a method. This would simplify handling of failed tests. The user can just add the resetAll() method in @After. The implementation of that is trivial as all StaticMockingInProgress’ are known to the mockmaker.
Static mocking has no mocked object to operate on, so doReturn(...).when(mockedObject).aStaticMethod(...) does not make sense.
At least on Android, java.lang.Class objects cannot be mocked. Hence doReturn(“mockedValue”).when(MyClass.class).aStaticMethod() clearly indicates that we want a static mocking not an object mocking.
Now the compiler complains as aStaticMethod is not a method on the class object, hence we need to call
doReturn(“mockedValue”).when(MyClass.class).getMethod(“aStaticMethod”).invoke(null);
With method parameters this gets even uglier, i.e.
doReturn("mockedValue").when(MyClass.class)
.getMethod("aStaticMethod", new Class[]{Parameter1.class, String.class})
.invoke(null, any(Parameter1.class), eq(“aRestriction”));
In Java you can call static methods on objects. E.g.
MyClass myObj = new MyClass();
myObj.aStaticMethod();
So you could allow
doReturn(“mockedValue”).whenStatic(MyClass.class).aStaticMethod();
if whenStatic is implemented as
public <T> T whenStatic(Class<T> mockedClass) {
return when(mock(mockedClass));
}
I see the following issues with this approach:
- Some IDEs (e.g. IntelliJ) flag calls to static methods on objects
- Mockito has to be taught that the ongoing stubbing is for the static methods, not the temporary mock object
Mocking of static methods can be implemented to behave very similar to inline-mocking of object methods. I.e. we add method entry hook to the static methods that calls into MockMethodAdvice.handle(Object instance, Method origin, Object[] arguments). Of course instance == null for static methods.
When mocking static’s of a class all calls to the methods get mocked. Hence there can only be one mocking for the static methods of the class going on at a time, otherwise they would interfere. Hence mockStatic should check if the class’s static methods are already mocked and fail if they are.
You could allow multiple spyings at the same time as long as they don’t mock the same methods, but I did not explore this scenario and did not allow “parallel” spying.
Overridden methods caused some issue during my experiments. In the following code
public class Main {
static class SuperClass {
static StackTraceElement[] getStackTrace() {
return Thread.currentThread().getStackTrace();
}
}
static class SubClass extends SuperClass {
}
public static void main(String[] args) {
System.out.println(Arrays.toString(SubClass.getStackTrace()));
}
}
You would expect the output to contain “Main$Subclass.getStackTrace”
But the output is
[java.lang.Thread.getStackTrace(Thread.java:1559), Main$SuperClass.getStackTrace(Main.java:6), Main.main(Main.java:15)]
This is because non-overridden static methods are resolved in the back traces. According to my VM guys this is as defined by the Java spec. I verified this behavior on Android/asop-master and OpenJDK 1.8.0 .
We need to know the called class as SubClass.getStackTrace might be mocked but SuperClass.getStackTrace might not be mocked.
The only place I could find that contains the information whether the SubClass or SuperClass is called is the byte code of the caller. In my case this is the Main class. Hence I ended up registering a temporary retransformation agent and triggered a retransformation of Main.class to get the byte code to inspect. I am working on Android so I am using jvmti to restransform the code, but I think the same problems apply to bytebuddy.
As the calling class might itself be instrumented, all agents need to be able to handle interference by other agents. Further the additional transformation is very slow. In the current state of the Android master tree, this can be more than 20 ms per method call. Hence I make sure that this is not done when I know from other, cheaper signals that this cannot happen. E.g. if there is no StaticMockingInProgress at this time, the expensive exploration is not necessary. It might be worth investigating removing the entry hooks once the mocking is over. In this case we trade faster methods calls vs. the cost of adding the entry hooks.