Sunday, October 20, 2013

Unit Testing and Dependency Injection, with xUnit InlineData and Unity

Inversion of control is great because it makes your code more testable; but you usually still have to write tests for each implementation of your interfaces. So what if your unit testing framework could just work directly with your container to make testing even easier? Well, xUnit can!

Below we define a custom data source for our xUnit theories by extending the InlineDataAttribute. This allows our data driven unit tests to resolve types via a container, and then inject those resolved objects straight into our unit tests.

Bottom line: This allows us to test more with less code!

The rest of post is very code heavy, so I strongly recommend that you start out by taking a look at sections 1 and 2 to get an idea of what we are trying to accomplish. :)

  1. Example Interfaces and Classes
  2. Example Unit Tests
  3. IocInlineDataResolver
  4. UnityInlineDataAttribute

1. Example Interfaces and Classes

public interface IStrategy
{
    bool IsValid(int value);
}
 
public class MyStrategy : IStrategy
{
    public bool IsValid(int value) { return value == 1 || value == 2; }
}
 
public class YourStrategy : IStrategy
{
    public bool IsValid(int value) { return value == 1 || value == 3; }
}

2. Example Unit Tests

public class StrategyTests
{
    // Required for UnityInlineData Attribute
    public static readonly IUnityContainer Container;
 
    static StrategyTests()
    {
        // Setup your container once for all of your tests.
        Container = new UnityContainer();
        Container.RegisterType<MyStrategy>();
        Container.RegisterType<YourStrategy>();
        Container.RegisterType<IStrategy, MyStrategy>("My");
        Container.RegisterType<IStrategy, YourStrategy>("Your");
    }
 
    // Good
    [Theory]
    [UnityInlineData(typeof(MyStrategy), 1, true)]
    [UnityInlineData(typeof(MyStrategy), 2, true)]
    [UnityInlineData(typeof(MyStrategy), 3, false)]
    [UnityInlineData(typeof(YourStrategy), 1, true)]
    [UnityInlineData(typeof(YourStrategy), 2, false)]
    [UnityInlineData(typeof(YourStrategy), 3, true)]
    public void GoodIsValid(IStrategy strategy, int value, bool expected)
    {
        // This will run SIX times.
        var actual = strategy.IsValid(value);
        Assert.Equal(expected, actual);
    }
 
    // Better!
    [Theory]
    [UnityInlineData("My", 1, true)]
    [UnityInlineData("My", 2, true)]
    [UnityInlineData("My", 3, false)]
    [UnityInlineData("Your", 1, true)]
    [UnityInlineData("Your", 2, false)]
    [UnityInlineData("Your", 3, true)]
    public void BetterIsValid(IStrategy strategy, int value, bool expected)
    {
        // This will run SIX times.
        var actual = strategy.IsValid(value);
        Assert.Equal(expected, actual);
    }
 
    // Best?
    [Theory]
    [UnityInlineData]
    public void BestIsValid(IStrategy strategy)
    {
        // This will run TWO times.
        var one = strategy.IsValid(1);
        Assert.True(one);        
        var two = strategy.IsValid(2);
        var three = strategy.IsValid(3);
        Assert.True(two ^ three);
    }
}

3. IocInlineDataResolver

public abstract class IocInlineDataResolver<TContainer>
    where TContainer : class
{
    public const string ContainerName = "Container";
 
    public IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes, object[] args)
    {
        if (args == null)
            args = new object[parameterTypes.Length];
 
        if (args.Length != parameterTypes.Length)
            throw new InvalidOperationException("Parameter count does not match Argument count");
 
        var container = GetContainer(methodUnderTest);
 
        var map = new Dictionary<int, IEnumerable<object>>();
 
        for (var i = 0; i < args.Length; i++)
        {
            var arg = args[i];
            var paramType = parameterTypes[i];
 
            if (arg != null)
            {
                var argType = arg.GetType();
                if (argType == paramType)
                    continue;
            }
 
            var underlyingType = Nullable.GetUnderlyingType(paramType);
            if (underlyingType != null)
                continue;
 
            var typeArg = arg as Type;
            if (typeArg != null)
            {
                var resolveByTypeArg = Resolve(container, typeArg);
                if (resolveByTypeArg != null)
                {
                    map[i] = new[] { resolveByTypeArg };
                    continue;
                }
            }
 
            var stringArg = arg as String;
            if (stringArg != null)
            {
                var resolveByName = ResolveByName(container, paramType, stringArg);
                if (resolveByName != null)
                {
                    map[i] = new[] {resolveByName};
                    continue;
                }
            }
 
            var resolveAll = ResolveAll(container, paramType);
            if (resolveAll != null)
            {
                map[i] = resolveAll;
                continue;
            }
 
            var resolveByParamType = Resolve(container, paramType);
            if (resolveByParamType != null)
            {
                map[i] = new[] { resolveByParamType };
                continue;
            }
 
            throw new InvalidOperationException("Unable to resolve type: " + paramType.Name);
        }
 
        IEnumerable<object[]> results = new[] { args };
        foreach (var pair in map)
        {
            var index = pair.Key;
            var values = pair.Value;
            results = results.SelectMany(r => ReplaceAndExpand(r, index, values));
        }
 
        return results;
    }
 
    private static TContainer GetContainer(MethodInfo methodUnderTest)
    {
        TContainer value = null;
 
        var field = methodUnderTest.DeclaringType.GetField(ContainerName);
        if (field != null)
            value = field.GetValue(null) as TContainer;
 
        if (value != null)
            return value;
            
        var property = methodUnderTest.DeclaringType.GetProperty(ContainerName);
        if (property != null)
            value = property.GetValue(null) as TContainer;
 
        if (value != null)
            return value;
 
        throw new InvalidOperationException("The test class must have a public Property or Field named " + ContainerName);
    }
 
    private static IEnumerable<T[]> ReplaceAndExpand<T>(IEnumerable<T> source, int index, IEnumerable<T> values)
    {
        foreach (var value in values)
        {
            var list = source.ToArray();
            if (index > -1) list[index] = value;
            yield return list;
        }
    }
 
    protected abstract object Resolve(TContainer container, Type type);
 
    protected abstract object ResolveByName(TContainer container, Type type, string name);
 
    protected abstract IEnumerable<object> ResolveAll(TContainer container, Type type);
}

4. UnityInlineDataAttribute

public class UnityInlineDataAttribute : InlineDataAttribute
{
    private readonly object[] _args;
    private readonly UnityIocInlineDataResolver _iocInlineDataResolverResolver;
 
    public UnityInlineDataAttribute(params object[] args)
    {
        _args = args;
        _iocInlineDataResolverResolver = new UnityIocInlineDataResolver();
    }
 
    public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
    {
        return _iocInlineDataResolverResolver.GetData(methodUnderTest, parameterTypes, _args);
    }
 
    public class UnityIocInlineDataResolver : IocInlineDataResolver<IUnityContainer>
    {
        protected override object Resolve(IUnityContainer container, Type type)
        {
            return container.Resolve(type);
        }
 
        protected override object ResolveByName(IUnityContainer container, Type type, string name)
        {
            return container.Resolve(type, name);
        }
 
        protected override IEnumerable<object> ResolveAll(IUnityContainer container, Type type)
        {
            return container.ResolveAll(type);
        }
    }
}
Shout it

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics