Sunday, July 13, 2014

Use RavenDB to power Data Driven xUnit Theories

I love xUnit's data driven unit tests, I also really enjoy working with RavenDB, and now I can use them together!

Data driven unit tests are very powerful tools that allow you to execute the same test code against multiple data sets. Testing frameworks such as xUnit makes this extremely easy to develop by offering an out of the box set attributes to quickly and easily annotate your test methods with dynamic data sources.

Below is some simple code that adds a RavenDataAttribute to xUnit. This attribute will pull arguments from a document database and pass them into your unit test, using the fully qualified method name as a key.

Example Unit Tests

public class RavenDataTests
{
    [Theory]
    [RavenData]
    public void PrimitiveArgs(int number, bool isDivisibleBytwo)
    {
        var remainder = number % 2;
        Assert.Equal(isDivisibleBytwo, remainder == 0);
    }
 
    [Theory]
    [RavenData]
    public void ComplexArgs(ComplexArgsModel model)
    {
        var remainder = model.Number % 2;
        Assert.Equal(model.IsDivisibleByTwo, remainder == 0);
    }
 
    [Fact(Skip = "Only run once for setup")]
    public void Setup()
    {
        var type = typeof(RavenDataTests);
 
        var primitiveArgsMethod = type.GetMethod("PrimitiveArgs");
        var primitiveArgs = new object[] { 3, false };
        RavenDataAttribute.SaveData(primitiveArgsMethod, primitiveArgs);
 
        var complexArgsMethod = type.GetMethod("ComplexArgs");
        var complexArgsModel = new ComplexArgsModel
        {
            IsDivisibleByTwo = true,
            Number = 4
        };
        RavenDataAttribute.SaveData(complexArgsMethod, complexArgsModel);
    }
 
    public class ComplexArgsModel
    {
        public int Number { get; set; }
        public bool IsDivisibleByTwo { get; set; }
    }
}

RavenDataAttribute Implementation

[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
public class RavenDataAttribute : DataAttribute
{
    private static readonly IDocumentStore DocumentStore;
 
    static RavenDataAttribute()
    {
        DocumentStore = new DocumentStore
        {
            Url = "http://localhost:8080/",
            DefaultDatabase = "RavenData"
        };
 
        DocumentStore.Initialize();
    }
 
    public override IEnumerable<object[]> GetData(
        MethodInfo methodUnderTest, 
        Type[] parameterTypes)
    {
        RavenDataModel document;
        var id = RavenDataModel.GetId(methodUnderTest); 
            
        using (var session = DocumentStore.OpenSession())
        {
            document = session.Load<RavenDataModel>(id);
        }
 
        if (document == null)
        {
            return Enumerable.Empty<object[]>();
        }
 
        return document.Rows
            .Where(r => !r.IsDisabled)
            .Select(r => ConvertDataTypes(r.Arguments, parameterTypes));
    }
 
    private static object[] ConvertDataTypes(
        object[] args, 
        Type[] parameterTypes)
    {
        if (args.Length != parameterTypes.Length)
        {
            throw new ArgumentException("Argument counts do not match");
        }
 
        var typedArgs = new object[args.Length];
 
        for (var i = 0; i < args.Length; i++)
        {
            var arg = args[i];
            var parameterType = parameterTypes[i];
            var type = args[0].GetType();
 
            typedArgs[i] = type == parameterType
                ? arg
                : Convert.ChangeType(arg, parameterType);
        }
 
        return typedArgs;
    }
 
    public static void SaveData(
        MethodInfo methodUnderTest, 
        params object[] args)
    {
        var id = RavenDataModel.GetId(methodUnderTest);
 
        using (var session = DocumentStore.OpenSession())
        {
            var document = session.Load<RavenDataModel>(id);
 
            if (document == null)
            {
                session.Store(new RavenDataModel
                {
                    Name = RavenDataModel.GetName(methodUnderTest),
                    Rows = new List<RavenDataModel.Row>
                    {
                        new RavenDataModel.Row
                        {
                            Arguments = args
                        }
                    }
                });
            }
            else
            {
                document.Rows.Add(new RavenDataModel.Row
                {
                    Arguments = args
                });
            }
 
            session.SaveChanges();
        }
    }
}
 
public class RavenDataModel
{
    public static string GetName(MethodInfo methodUnderTest)
    {
        if (methodUnderTest == null || methodUnderTest.DeclaringType == null)
        {
            throw new ArgumentNullException(
                "methodUnderTest", 
                "MethodUnderTest.DeclaringType is required");
        }
 
        return String.Format(
            "{0}.{1}.{2}",
            methodUnderTest.DeclaringType.Namespace, 
            methodUnderTest.DeclaringType.Name, 
            methodUnderTest.Name);
    }
 
    public static string GetId(MethodInfo methodUnderTest)
    {
        var name = GetName(methodUnderTest);
        return GetId(name);
    }
 
    public static string GetId(string name)
    {
        var type = typeof(RavenDataModel);
        return String.Concat(type.Name, "/", name);
    }
 
    public string Id { get { return GetId(Name); } }
    public string Name { get; set; }
    public IList<Row> Rows { get; set; }
 
    public class Row
    {
        public bool IsDisabled { get; set; }
        public object[] Arguments { get; set; }
    }
}

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics