Thursday, November 5, 2009

Dynamic Unit Test Data

5-November-2009

Ever written a test and found yourself copying and pasting it multiple times to pass in different pieces of test data?  Particularly if you have multiple classes that transform the same input data.  I am currently working on some client side libraries that work off transfered Data Transfer Objects (dtos).  So most of the client classes take these dtos as input.  One such class is a Json Adaptor that converts any dto to a string and back again (internally it uses the .Net Json serialiser see DataContractJsonSerializer with some more goodness injected).

To test this class I need to give it a wide range of test data. Including many different types of data, and with each type instantiated with a wide range of values.  I created a library of test data in the Data namespace, with each type in this namespace offering many different instances of the type.


namespace TestData
    public static class AddressDtoDataLibrary {
        private static readonly IList<TestDataGroup<Dto.AddressDto>> Data = new List<TestDataGroup<Dto.AddressDto>>();

        static AddressDtoDataLibrary() {
            Guid id = Guid.NewGuid();
            var serialised = new Dictionary<OutputFormat, string>() {
                { OutputFormat.Xml, GetInput1Xml(id) }, 
                { OutputFormat.Json, GetInput1Json(id) }
            };
            Data.Add(new TestDataGroup<Dto.AddressDto>(GetInput1(id), serialised));

            id = Guid.NewGuid();
            serialised = new Dictionary<OutputFormat, string>() {
                { OutputFormat.Xml, GetInput2Xml(id) },
                { OutputFormat.Json, GetInput2Json(id) },
            };
            Data.Add(new TestDataGroup<Identity.AddressDto>(GetInput2(id), serialised));

            // etc
        }

        public static TestDataGroup<Dto.AddressDto>[] DataArray {
            get {
                return Data.ToArray();
            }
        }

        internal static Identity.AddressDto GetInput1(Guid id) {
            return new Identity.AddressDto() {
                City = "Auckland",
                Country = "New Zealand",
                CreatedOn = new DateTime(2000, 2, 29),
                Id = id,
                Line1 = "24 Rodney Street",
                Line2 = string.Empty,
                Sid = string.Format("{0}@{1}", id, typeof(AddressDtoDataLibrary).Name),
                State = "Auckland",
                Suburb = "Birkenhead",
                Updated = new DateTime(2007, 12, 31, 7, 41, 59),
                Version = id,
                Zip = "2801"
            };
        }
        internal static string GetInput1Json(Guid id) {
            return @"{""City"":""Auckland"",""Country"":""New Zealand"",""CreatedOn"":""\/Date(951735600000+1300)\/"",""Id"":""" + id + @""",""Line1"":""24 Rodney Street"",""Line2"":"""",""Sid"":""" + id + @"@AddressDto"",""State"":""Auckland"",""Suburb"":""Birkenhead"",""Updated"":""\/Date(1199040119000+1300)\/"",""Version"":""" + id + @""",""Zip"":""2801""}";
        }
        internal static string GetInput1Xml(Guid id) {
            return @"<AddressDto>
                <City>Auckland</City>
                <Country>New Zealand</Country>
                <CreatedOn>2000-02-29T00:00:00</CreatedOn>
                <Id>" + id + @"</Id>
                <Line1>24 Rodney Street</Line1>
                <Line2></Line2>
                <Sid>" + id + @"@AddressDto</Sid>
                <State>Auckland</State>
                <Suburb>Birkenhead</Suburb>
                <Updated>2007-12-31T07:41:59</Updated>
                <Version>" + id + @"</Version>
                <Zip>2801</Zip>
            </AddressDto>";
        }
    }
}
I could use the NUnit [Values] attribute, but this requires me to hard code each set of data for each test method. Not to mention its an uglier syntax. Maybe acceptable for basic scalar types and strings, but not for objects imho. If I add more test data, it won't be applied to existing tests.
If I did go down this track it would look like this:


[Test]
public void TestJsonSerialiser(
    [Values(TestData.AddressDtoDataLibrary.DataArray[0].DtoObject)] IDto dtoObject, 
    [Values(TestData.AddressDtoDataLibrary.DataArray[0].Serialised[OutputFormat.Json])] string serialised) {
    /// test code here
}

Using TestDataSource attribute allows you to use a method to gather the testdata programmatically.

Here's the test fixture:



    [TestFixture]
    public class HelperTest {
        private static IEnumerable<Type> dataSourceClasses = new[] {
             typeof(Data.AddressDto), 
             typeof(Data.ClientIdentityAddressDto),
             typeof(Data.ClientIdentityDto),
             typeof(Data.CreateIdentityResponseDto),
             typeof(Data.PingResponseDto),
             typeof(Data.ScreenNameAvailabilityDto),
             typeof(Data.UpdateResponseDto)
        };
        private static object[] TestDataSourceArray;

        static HelperTest() {
            TestDataSourceArray = PopulateTestDataArray();
        }

        private static object[] PopulateTestDataArray() {
            var list = new List<object[]>();
            dataSourceClasses.All(t => {
                list.AddRange(from index in Enumerable.Range(0, HowMuchTestDataDoesThisClassHave(t)) select new object[] { t, index });
                return true;
            });
            return list.ToArray();
        }

        private static int HowMuchTestDataDoesThisClassHave(Type type) {
            var property = type.GetProperty("DataArray", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
            if (property == null) {
                return 0;
            }

            var array = property.GetValue(null, new object[] { }) as object[];
            if (array == null) {
                return 0;
            }

            return array.Length;
        }

        [Test]
        [TestCaseSource("TestDataSourceArray")]
        public void TestJsonSerialise(Type dtoType, int index) {
            var dto = Data.TestDataHelper.GetTestDataItem(dtoType, index);
            Assert.IsNotNull(dto);
            var serialiser = new JsonSerialiser();
            var result = serialiser.Serialise(dto);
            Debug.WriteLine(result);
            Assert.IsNotNullOrEmpty(result);
        }
    }

Suffice to say the GetTestDataItem method in the Test gets the actual test data input from the type passed in. This type argument has a static property that presents an array of test data sets.  Each set contains the object, the matching xml serialisation string, and the matching json string.


public static class TestDataHelper {
        public static IDto GetTestDataItem(Type type, int index) {
            if (type == null) {
                return null;
            }

            if (index < 0) {
                return null;
            }

            try {
                var array = type.GetProperty("DataArray", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).GetValue(null, new object[] { }) as object[];
                if (array == null) {
                    return null;
                }

                if (index >= array.Length) {
                    return null;
                }

                var testDataGroup = array[index];
                return testDataGroup.GetType().GetProperty("DtoObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).GetValue(testDataGroup, new object[] { }) as IDto;
            } catch (Exception ex) {
                throw new NotSupportedException("The test data dto object " + type.Name + " does not have a DataArray or DtoObject property implemented.", ex);
            }
        }
    }

A little crude using reflection, (not cool in heavily used production code) but it works like a charm for testing.

Killed more than two birds with one test (especially since the test data was already written :-) ...

The test output from Nunit looks like this:

No comments:

Post a Comment