4

My apologies in advance for a terrible title- suggestions welcome!

I've been reading about DI and AOP, and I think I grasp the basics; at least for the canonical example of adding logging.

I would like to apply this to the test cases we've been creating in NUnit e.g. be able to automatically add entry/exit logging for all test case methods and any 'helper methods' that they call. (And I'm not tied to NUnit- if it's easier in another framework, please let me know.)

NOTE- this is not about the subject under test; I want to apply these techniques to the testcases themselves.

It's pretty clear how to do this using PostSharp - it's their First example. However I don't want to add the handling of their licensing to our project for just this experiment.

All the other references I've found to AOP for C# are based around (dynamic) Interceptors provided by IoC container implementations such as CastleWindsor, Unity, Spring.Net, ... They all have a common problem in this case: you need a piece of setup-code that creates the proxy for the object you want to add interceptors to. (I did originally think this code would also have to create an IoC container, but I see I'm wrong there.)

But I can't see where this setup code would go for nUnit testcases.

The options I've come up with, and their problems:

  1. Have the testfixture classes constructor create a proxy to itself. Won't work, due to recursion (consumer asks for thing, thing tries to return proxy to thing, proxy tries to create thing... from reading this StackOverflow question)
  2. Roll my-own reflection based magic (which would be a big undertaking for me)
    1. have the constructor wrap all methods in the testfixture class and return this 'wrapped' object (not sure if this it's possible for a constructor to do this)
    2. use a static constructor on the testfixture to do this magic (assuming you can dynamically wrap the methods of the class in place.)
    3. do something at the module level using a module cctor (via Einar Egilsson's InjectModuleInitializer) and wrap all methods in all classes with logging.
  3. The simplest: some kind of factory for instantiating the testcases (NOT parameters for the tests), from which I could use one of the IoC Proxy Generators
    1. For nUnit: the only means I can find to do this is to create a custom AddIn. Advantage- may not break integration with ReSharper. Disadvantage- deploying to all devs machines, especially on updates to NUnit. Are there other ways of doing this for nUnit?
    2. For MbUnit: looks like it treats testcases as first class values, and this is straight-forward. Advantage: easy to deploy to all developers. Disadvantage: tests aren't going to show up in Resharper. Side note: how to handle setup and teardown.

Have I missed anything in my options and conclusions?

Are there any simpler means of doing this that I've missed?

Community
  • 1
  • 1
JulianRendell
  • 93
  • 1
  • 4
  • 1
    What I'm missing from your question is why you need to add logging to your test classes. This is a very unusual thing. Would you mind explaining this? – Steven Nov 21 '12 at 08:10
  • I agree, this is a very unusual situation, perhaps some more explanation about 'why' would help. Otherwise, I think PostSharp is probably your best bet. – Matthew Groves Nov 25 '12 at 13:12
  • Why? My tests may be run by many people, some are long running (eg Selenium, performance tests...), some may be run on remote machines (eg CI.) They're not always run from within Visual Studio. In all these cases, being able to look at the logs (or re-run with logging enabled) can be a very useful piece of information. Also I'm still learning, and seeing the tests ACTUAL execution often teaches me mistakes in my thinking :). PostSharp is probably the only answer, but the licensing makes it something I don't want to introduce at the moment. – JulianRendell Dec 03 '12 at 00:24

2 Answers2

1

Aspect Oriented Programming is not only about using dynamic proxies (interception) or post compilation code weaving (PostSharp). AOP is mainly about adding cross-cutting concerns. Using dynamic proxies is one way of adding cross-cutting concerns. Code weaving is another way of doing this. But there is another, IMO better, way of adding cross-cutting concerns.

Instead of using dynamic proxies or code weaving, let the design of your application lead you. When you design your application using the right abstractions, it would be easy to add cross-cutting concerns using decorators. You can find examples of systems that are designed using proper abstractions here and here.

Those articles describe how you define cross-cutting concerns using decorators. When you designed the system in this way, you can test the implementation of a cross-cutting concern separately from the rest of the code. This will be easy when using the right abstractions.

When you do this, there is no need to do anything special in your unit tests. No need for code weaving, no need to run your DI container in your test to build up objects for you. You can test your application logic without any cross-cutting concern to be in the way. You can test every little piece in isolation and put all pieces together it in your application’s the Composition Root.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • Thanks for the response, and the links to the detailed articles; I've skimmed them but will sit down and read them fully later. Looks to me like we agree. **BUT**, probably due to a poor description, you've missed my goal. I'm not talking about the application under test, but rather how to apply cross-cutting-concerns TO an EXISTING test framework, such as NUnit; ie something outside of my control. I don't want to invent my own- skills, time, existing tooling, etc, etc. Any ideas on how to apply these principles to existing (test) frameworks? – JulianRendell Nov 21 '12 at 01:30
  • I agree with this answer, except that the decorator/proxy pattern breaks down when you want to use the same cross-cutting concern for a large or indeterminate number or of classes. – Matthew Groves Nov 25 '12 at 12:56
  • @mgroves. That's true, but in that case the problem is again in the design of your application. – Steven Nov 25 '12 at 16:03
0

Here a sample using Puresharp Framework (by installing IPuresharp + Puresharp nugets on test project.)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using NUnit.Framework;
using Puresharp;

namespace TEST
{
    /// <summary>
    /// Class to test with NUnit
    /// </summary>
    public class Calculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }
    }

    /// <summary>
    /// Test class for calculator with a simple test.
    /// </summary>
    [TestFixture]
    public class CalculatorTest
    {
        /// <summary>
        /// Static constructor (class constructor) to attach aspect to test method.
        /// </summary>
        static CalculatorTest()
        {
            //Instantiate my custom aspect.
            var myBasicAspect = new MuCustomAspect();

            //Attach my custom aspect to my custom pointcut
            myBasicAspect.Weave<MyCustomPointcut>();
        }

        [Test]
        public void ShouldAddTwoNumbers()
        {
            var _calculator = new Calculator();
            int _result = _calculator.Add(2, 8);
            Assert.That(_result, Is.EqualTo(10));
        }
    }

    /// <summary>
    /// Pointcut to identify methods group to weave (here test methods of CalculatorTest).
    /// </summary>
    public class MyCustomPointcut : Pointcut
    {
        override public bool Match(MethodBase method)
        {
            return method.DeclaringType == typeof(CalculatorTest) && method.GetCustomAttributes(typeof(TestAttribute), true).Any();
        }
    }

    /// <summary>
    /// Défine an aspect.
    /// </summary>
    public class MuCustomAspect : Aspect
    {
        public override IEnumerable<Advisor> Manage(MethodBase method)
        {
            //Aspect will advice method on boundary using MyCustomAdvice.
            yield return Advice.For(method).Around(() => new MyCustomAdvice());
        }
    }

    /// <summary>
    /// Define an advice.
    /// </summary>
    public class MyCustomAdvice : IAdvice
    {
        public MyCustomAdvice()
        {
        }

        public void Instance<T>(T value)
        {
        }

        public void Argument<T>(ref T value)
        {
        }

        public void Begin()
        {
        }

        public void Await(MethodInfo method, Task task)
        {
        }

        public void Await<T>(MethodInfo method, Task<T> task)
        {
        }

        public void Continue()
        {
        }

        public void Return()
        {
        }

        public void Return<T>(ref T value)
        {
        }

        public void Throw(ref Exception exception)
        {
        }

        public void Throw<T>(ref Exception exception, ref T value)
        {
        }

        public void Dispose()
        {
        }
    }
}
Tony THONG
  • 772
  • 5
  • 11