Search code examples
unit-testingmockingcastle-dynamicproxy

How to unit test an interceptor?


I want to write some unit tests for an interceptor that intercepts the Loggable base class (which implements ILoggable).
The Loggable base class has no methods to call and it is used only to be initialized by the logging facility.
To my understanding I should:

  1. Mock an ILoggable and an ILogger
  2. Initialize the logging facility
  3. Register my interceptor on it
  4. Invoke some method of the mocked ILoggable

The problem is that my ILoggable interface has no methods to call and thus nothing will be intercepted.
What is the right way to act here?
Should I mock ILoggable manually and add a stub method to call?
Also, should I be mocking the container as well?

I am using Moq and NUnit.
EDIT:
Here's my interceptor implementation for reference:

public class LoggingWithDebugInterceptor : IInterceptor
{
    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        var invocationLogMessage = new InvocationLogMessage(invocation);

        ILoggable loggable = invocation.InvocationTarget as ILoggable;

        if (loggable == null)
            throw new InterceptionFailureException(invocation, string.Format("Class {0} does not implement ILoggable.", invocationLogMessage.InvocationSource));

        loggable.Logger.DebugFormat("Method {0} called with arguments {1}", invocationLogMessage.InvokedMethod, invocationLogMessage.Arguments);

        Stopwatch stopwatch = new Stopwatch();
        try
        {
            stopwatch.Start();
            invocation.Proceed();
            stopwatch.Stop();
        }
        catch (Exception e)
        {
            loggable.Logger.ErrorFormat(e, "An exception occured in {0} while calling method {1} with arguments {2}", invocationLogMessage.InvocationSource, invocationLogMessage.InvokedMethod, invocationLogMessage.Arguments);
            throw;
        }
        finally
        {
            loggable.Logger.DebugFormat("Method {0} returned with value {1} and took exactly {2} to run.", invocationLogMessage.InvokedMethod, invocation.ReturnValue, stopwatch.Elapsed);
        }
    }

    #endregion IInterceptor Members
}

Solution

  • I agree with Krzysztof, if you're looking to add Logging through AOP, the responsibility and implementation details about logging should be transparent to the caller. Thus it's something that the Interceptor can own. I'll try to outline how I would test this.

    If I follow the question correctly, your ILoggable is really just a naming container to annotate the class so that the interceptor can determine if it should perform logging. It exposes a property that contains the Logger. (The downside to this is that the class still needs to configure the Logger.)

    public interface ILoggable
    {
         ILogger { get; set; }
    }
    

    Testing the interceptor should be a straight-forward process. The only challenge I see that you've presented is how to manually construct the IInvocation input parameter so that it resembles runtime data. Rather than trying to reproduce this through mocks, etc, I would suggest you test it using classic State-based verification: create a proxy that uses your interceptor and verify that your log reflects what you expect.

    This might seem like a bit more work, but it provides a really good example of how the interceptor works independently from other parts of your code-base. Other developers on your team benefit from this as they can reference this example as a learning tool.

    public class TypeThatSupportsLogging : ILoggable
    {
         public ILogger { get; set; }
    
         public virtual void MethodToIntercept()
         {
         }
    
         public void MethodWithoutLogging()
         {
         }
    }
    
    public class TestLogger : ILogger
    {
         private StringBuilder _output;
    
         public TestLogger()
         {
            _output = new StringBuilder();
         }
    
         public void DebugFormat(string message, params object[] args)
         {
            _output.AppendFormat(message, args);
         }
    
         public string Output
         {
            get { return _output.ToString(); }
         }
    }
    
    [TestFixture]
    public class LoggingWithDebugInterceptorTests
    {
         protected TypeThatSupportsLogging Input;
         protected LoggingWithDebugInterceptor Subject;
         protected ILogger Log;         
    
         [Setup]
         public void Setup()
         {
             // create your interceptor
             Subject = new LoggingWithDebugInterceptor();
    
             // create your proxy
             var generator = new Castle.DynamicProxy.ProxyGenerator();
             Input = generator.CreateClassProxy<TypeThatSupportLogging>( Subject );
    
             // setup the logger
             Log = new TestLogger();
             Input.Logger = Log;
         }
    
         [Test]
         public void DemonstrateThatTheInterceptorLogsInformationAboutVirtualMethods()
         {
              // act
              Input.MethodToIntercept();
    
              // assert
              StringAssert.Contains("MethodToIntercept", Log.Output);
         }
    
         [Test]
         public void DemonstrateNonVirtualMethodsAreNotLogged()
         {
              // act
              Input.MethodWithoutLogging();
    
              // assert
              Assert.AreEqual(String.Empty, Log.Output);
         }
    }