I am trying to write a unit test for a method taking the InvocationContext as parameter. More specifically here's the signature and essentials of the method.
@AroundInvoke
public Object autoLogMethodCall(final InvocationContext context) throws Exception {
String className = context.getClass().getSimpleName();
String packageName = context.getClass().getPackage().getName();
String methodName = context.getMethod().getName();
// Some logging stuff that is the target of actual testing
}
As you see, it is an interceptor method I intend to use for doing some basic logging for certain method calls.
Then I have unit test which I want to test that the logged messages will be properly formatted. But the problem is that that I can not create an instance of the InvocationContext to pass as a parameter for testing.
I have tried the following mocking.
@RunWith(PowerMockRunner.class)
public class AutoLoggingTest extends TestCase {
@Test
public void testAutoLogger() {
Logger log = new MyLogger(); // This is an implementation of org.apache.logging.log4j.Logger, which will hold the generated messages to check at the test
InvocationContext mockContext = PowerMockito.mock(InvocationContext.class);
Class clazz = AutoLoggingTest.class;
// The row causing the error 'MissingMethodInvocation'
PowerMockito.when(mockContext.getClass()).thenReturn(clazz);
try {
InterceptingClass ic = new InterceptingClass();
ic.setLogger(log);
ic.autoLogMethodCall(mockContext);
MyLogger myLogger = (MyLogger) ic.getLogger();
assertEquals(2, myLogger.getMessages().size());
} catch (Exception e) {
e.printStackTrace();
fail("Should not cause an exception in any case");
}
}
// Check the actual messages based on the information given in mocked InvocationContext object
}
But it does not work.
causes:
Tests in error: AutoLoggingTest.testAutoLogger:25 » MissingMethodInvocation.
when() requires an argument which has to be 'a method call on a mock'.).
Any advice on how to do the mocking properly?
This required some thinking out of the box. Some mixed content with the mocked InvocationContext is needed. We can provide the testing class itself in the mocked InvocationContext object, thus I added and changed the following in the test class itself:
@RunWith(PowerMockRunner.class)
public class AutoLoggingTest extends TestCase {
// This method needs to be added here to provide it for mocked InvocationContext.
public void methodForLoggingTesting() {
}
@Test
public void testAutoLogger() {
Logger log = new MyLogger();
// Some renaming & refactoring after the initial stuff
AutoLoggingUtility alu = new AutoLoggingUtilityImplForTesting();
alu.setLogger(log);
InvocationContext mockContext = PowerMockito.mock(InvocationContext.class);
try {
Method testMethod = this.getClass().getMethod("methodForLoggingTesting");
PowerMockito.when(mockContext.getMethod()).thenReturn(testMethod);
PowerMockito.when(mockContext.proceed()).thenReturn(null);
} catch (Exception e) {
e.printStackTrace();
fail("Should not throw an exception, InvocationContext mocking failed!");
}
try {
alu.autoLogMethodCall(mockContext);
} catch (Exception e) {
e.printStackTrace();
fail("Should not throw an exception, logging failed!");
}
MyLogger myLogger = (MyLogger) alu.getLogger();
assertEquals(3, myLogger.getMessages().size());
// More tests to check the actual logged content
}
}
Also I realized I should provide the code for the 'MyLogger' as it wasn't quite trivial to implement for the test.
// Logger = org.apache.logging.log4j.Logger
// ExtendedLoggerWrapper = org.apache.logging.log4j.spi.ExtendedLoggerWrapper
@SuppressWarnings("serial")
protected class MyLogger extends ExtendedLoggerWrapper implements Logger {
private List<String> messages;
public MyLogger() {
super(null, null, null);
this.clearMessages();
}
// The actual log calls need to get stored to store the messages + prevent from NullPointerExceptions
@Override
public void trace(String msg) {
messages.add(msg);
}
// The actual log calls need to get stored to store the messages + prevent from NullPointerExceptions
@Override
public Object exit(Object obj) {
messages.add("Exited with: " + obj);
return obj;
}
public List<String> getMessages() {
return this.messages;
}
public void clearMessages() {
messages = new ArrayList<>();
}
/**
* You need to override all the method calls used to prevent NullPointerExceptions.
*
* @return <code>True</code> always, as required so in test.
*/
@Override
public boolean isTraceEnabled() {
return true;
}
}
And since there was some minor refactoring needed in the original Logging class, it now looks like this:
public abstract class AutoLoggingUtility {
private static final String logEntryTemplate = "Call to: %1$s#%2$s";
private static final String logExitTemplate = "'%1$s' call duration: %2$s ms";
public AutoLoggingUtility() {
}
@AroundInvoke
public Object autoLogMethodCall(final InvocationContext context) throws Exception {
// Note the methods Overridden in MyLogger
if (this.getLogger().isTraceEnabled()) {
String methodName = null;
String className = null;
try {
Method method = context.getMethod();
methodName = method.getName();
// Contains package
className = context.getMethod().getDeclaringClass().getName();
} catch (Exception e) {
// May not crash
methodName = "?method?";
className = "?class?";
}
Object[] args1 = { className, methodName };
String logMsg = String.format(getLogentrytemplate(), args1);
this.getLogger().trace(logMsg);
long startTime = System.currentTimeMillis();
try {
return this.getLogger().exit(context.proceed());
} finally {
Object[] args2 = { methodName, System.currentTimeMillis() - startTime };
logMsg = String.format(getLogexittemplate(), args2);
this.getLogger().trace(logMsg);
}
} else {
// mocked
return context.proceed();
}
/**
* Forces each extending class to provide their own logger.
*
* @return The logger of the extending class to direct the messages to correct logging context.
*/
abstract Logger getLogger();
}
The 'AutoLoggingUtilityImplForTesting' simply extends 'AutoLoggingUtility' to hold instance of MyLogger.
Summarum:
The trick is to provide instance of the test classes method 'methodForLoggingTesting' for the mocked object to return when the 'getMethod()' is called. => No need to try to mock excess stuff.