Click here to Skip to main content
Click here to Skip to main content

Tagged as

Dynamic Unit Testing

, 1 Sep 2011
Rate this:
Please Sign up or sign in to vote.
Dynamic Unit Testing

Introduction

While writing Unit Tests for an application with a complex business logic, I came across some classes that were exposing very few public methods, and had a bunch of private methods. Since the core logic was mostly embedded in those private methods, I thought I couldn't skip them in the tests. So I started to seek for a way for invoking them in my Test project. I found out that MsTest already has some shortcuts for invoking private methods (namely the PrivateObject and the PrivateType classes). Unfortunately, I was already familiar with NUnit and didn't want to switch to another testing framework in the middle of the development. I was tempted to mark the private methods as internal, since the methods with that access modifier can be invoked from outside the assembly they reside, if one marks the assembly itself with the InternalsVisibleTo attribute, in the following way (code from AssemblyInfo.cs):

[assembly: InternalsVisibleTo("Business.Tests")]

Background

After having replaced some modifier from private to internal, I soon felt some kind of frustration, since I saw that I was widening the scope of those members only for my tests. So I thought that Reflection could be a better way for accomplishing that task. Here is a simple example of a private method to be tested:

public class Range<T> : IRange<T> where T : IComparable<T>
{
    public T Start { get; set; }
    public T End { get; set; }

    //... other methods omitted

    private static bool Overlaps(IRange<T> first, IRange<T> second)
    {
        return first.Start.Between(second) || first.End.Between(second);
    }
}

And here's the Test method that uses Reflection for invoking it (it uses NUnit version 2.5.10):

[Test,
Description("Tests two ranges that don't overlap.")]
public void OverlapTest([Random(0, 1000, 5)] int firstStart,
                        [Random(1001, 2000, 5)] int firstEnd,
                        [Random(2001, 5000, 5)] int secondStart,
                        [Random(5001, 10000, 5)] int secondEnd)
{
    var methodInfo = typeof(Range<int>).GetMethod
	("Overlaps", BindingFlags.NonPublic | BindingFlags.Static);
    var result = methodInfo.Invoke(null, new[]
                                {
                                    new Range<int>(firstStart, firstEnd),
                                    new Range<int>(secondStart, secondEnd)
                                });
    Assert.False((bool)result);
}

I knew from earlier experiences how a naive use of Reflection can dramatically slow down the procedures that rely on it. In the particular case, the use of random values as parameters results in multiple calls to the Test method, and subsequently to the Type.GetMethod method. Now, one could also ignore performance issues on Unit Tests, but here there was also a verbosity issue. Using the above syntax results in a lot of "ceremonial" code. So I began considering to use a new feature of C# 4, namely the dynamic keyword and the DynamicObject class. I had just finished the reading of the nice article Understanding the Dynamic Keyword in C# 4 that shows some examples of how the COM methods of Microsoft Excel get called more concisely using the dynamic keyword. I thought it might be useful for my unit tests too. So I wrote the DynamicObjectBase class:

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace Business.Test
{
    public class DynamicObjectBase: DynamicObject
    {
        private readonly Type _type;
        private readonly Object _instance;

        private readonly Dictionary<string, MethodInfo> _methods =
                new Dictionary<string, MethodInfo>();

        public DynamicObjectBase(Object instance)
        {
            _instance = instance;
            _type = instance.GetType();
        }

        public DynamicObjectBase(Type type)
        {
            _instance = null;
            _type = type;
        }

        public override bool TryInvokeMember
        (InvokeMemberBinder binder, object[] args, out object result)
        {
            if (!_methods.ContainsKey(binder.Name))
            {
                var methodInfo = _type.GetMethod(binder.Name,
                BindingFlags.NonPublic | BindingFlags.Static |
                BindingFlags.Instance | BindingFlags.Public);
                _methods.Add(binder.Name, methodInfo);
            }

            var method = _methods[binder.Name];
            result =  method.Invoke(method.IsStatic ? null : _instance, args);

            return true;
        }
    }
}

Using the Code

The code above overrides the TryInvokeMember method, that gets called every time a method is invoked on an instance of DynamicObjectBase declared as dynamic.

The class uses a cache of MethodInfo objects, and it accepts in its constructors either the instance or the type of the class where to seek the private methods.

If a class contains only static methods, we can call the constructor that accepts a Type instance. Instead, if we are going to test instance private methods, it is mandatory to use the constructor that accepts Object as a parameter. In the Test class, we can create an instance of DynamicObjectBase and invoke the private methods on it, whether instance or static they are. So the Type.GetMethod method gets called only once, and in the next calls it uses the cached instance of MethodInfo. However, the call to MethodInfo.Invoke remains, and it's the most expensive in term of CPU load. So the performance test shows only a very little gain in the execution time. But we've stripped away the messy reflection code from the test, so look how less verbose is the calling code in the rewritten Unit Test:

private readonly dynamic _range;

public RangeFixture()
{
    _range = new DynamicObjectBase(typeof(Range<int>));
}

[Test,
Description("Tests two ranges that don't overlap.")]
public void DynamicOverlapTest([Random(0, 1000, 5)] int firstStart,
                        [Random(1001, 2000, 5)] int firstEnd,
                        [Random(2001, 5000, 5)] int secondStart,
                        [Random(5001, 10000, 5)] int secondEnd)
{
    var result = _range.Overlaps(new Range<int>(firstStart, firstEnd),
                                new Range<int>(secondStart, secondEnd));
    Assert.False(result);
}

Points of Interest

The DynamicObject class fits well not only for interacting with objects that require a runtime type checking (once called "late binding"), but also for hiding some verbose Reflection code that dynamically invokes the members of a class. It is good for writing concise and readable Unit Tests, like in the scenario depicted above.

History

  • 2011 August, 30 - First release
  • 2011 August, 31 - Simplified the example code

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

spartacus23
Software Developer (Senior)
Italy Italy
My name is Idalgo Cantelli. I'm a software developer skilled in .Net technologies. I work with .Net since February, 2002. I also have a strong experience as a technical trainer, having taught in more than thirty classroom courses. I'm MCTS and MCPD-EAD on .Net 2.0, planning an upgrade to 3.5.

Comments and Discussions

 
QuestionTry Gallio/MBUnit.... PinmemberAlberto Bar-Noy30-Aug-11 20:21 
It has a "mirror pattern" that does exactly that although it hides the reflection creation code from you
Alberto Bar-Noy
---------------
“The city’s central computer told you? R2D2, you know better than to trust a strange computer!”
(C3PO)

AnswerRe: Try Gallio/MBUnit.... Pinmemberspartacus2330-Aug-11 22:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140902.1 | Last Updated 1 Sep 2011
Article Copyright 2011 by spartacus23
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid