Search code examples
unit-testingjava-ee-6interceptorpowermockito

How to get intance of InvocationContext for unit tests


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?


Solution

  • 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.