Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
}
like image 207
the_drow Avatar asked Dec 28 '22 21:12

the_drow


2 Answers

If it's just the interceptor that uses the Logger Property on your class than why have in there at all? You might just as well have it on the interceptor. (like Ayende explained in his post here).

Other than that - interceptor is just a class which interacts with an interface - everything highly testable.

like image 124
Krzysztof Kozmic Avatar answered Dec 31 '22 13:12

Krzysztof Kozmic


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);
     }
}
like image 29
bryanbcook Avatar answered Dec 31 '22 15:12

bryanbcook