This blog post consists of three parts
- On generating sample data in general
- Extending AutoFixture with automatic collections generation support
- Adding a sample data generator in Silverlight for Visual Studio/Blend screen previews
[Note: Windows Live Writer did a horrible job in transferring the post to the blog. I've fixed quite a bit on the formatting, but it's still in poor quality. Apologies for that. Let me know if you want the code samples without the formatting]
Generating sample data in general
Creating objects with sample data is a challenge developers often need to handle. The most common use case is for testing purposes, where you need an object with various values set. Some might be important, but many just need to be set to anything (Also called an anonymous variable).
I’ve seen and used various solutions for this. Often you see it either done manually, by an object mother, with test data builders, or by loading existing objects from a database. Many people start out writing a simple reflection tool to handle it, but stops a few hours down the road once the actual complexity involved becomes apparent.
I had a new use case for it this time, and wanted to avoid many of the problems with the solutions above. After searching for and trying various reflection based tools, I found AutoFixture, and was impressed from the get-go. It pretty much does exactly what you’d expect, and has a clean interface as well. Some examples:
- Creating an anonymous string
var anonymousText = fixture.CreateAnonymous<string>();
Result:
- anonymousText: aa48c714-6c6a-4ac4-9c27-3658c9e78d5f- Creating an anonymous object
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
..
var personWithAnonymousData = new Fixture().CreateAnonymous<Person>();
- Name: Nameb7c2a9fc-a5b0-4836-83d9-be1b057e0ff1
- Age: 1
Take a look a the AutoFixture cheat sheat for a number of good examples. There’s many things you can do to customize the behavior and output.
That covers the introduction to AutoFixture. If you need a sample data creator, for test data or other purposes, I advice you to check it out. Over to the next point.
Extending AutoFixture with automatic collections generation support
There was one feature I was missing in AutoFixture that I really wanted. If an object has a collection (, list, or any other sort of .NET collection) of something, that collection will only be initialized to an empty collection automatically. If I do a change to the Person object and rerun, the PhoneNumbers list below will be an empty list of strings.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<string> PhoneNumbers { get; set; }
}
AutoFixture have functionality to handle this. If you run the line below first..
fixture.Register(() => fixture.CreateMany<string>().ToList());
..then the output of PhoneNumbers will be something similar to:
[0]: 635bc212-8d16-4029-8423-1f45016fe020
[1]: fcbed54b-94f6-4eea-b773-e7ec5e75b6dd
[2]: 814c120a-7e9c-487a-b99e-9e23d9d511b0
However, I would like this to be handled automatically. Doing it the AutoFixture-way you’ll need to know the internal structure of a class to define that it should handle a certain collection. Another problem is collection interfaces. If PhoneNumbers was defined as IList<string>, I would be forced to define it since it causes a runtime exception of:
“AutoFixture was unable to create an instance from System.Collections.Generic.IList`1[System.String], most likely because it has no public constructor.”
So I set out to see if I could extend AutoFixture to handle collections automatically. Seemed like a fun enough way of getting to know AutoFixture better as well as some good reflection fun. The requirements I set was to handle generic collections and interfaces to collections automatically.
First how to use it (Only the second line is new):
var fixture = new Fixture();
fixture.Customizations.Add(new CollectionsGenerator(fixture));
var personWithAnonymousData = fixture.CreateAnonymous<Person>();
The new CollectionsGenerator:
1: /// <summary>
2: /// Support for handling automatic generation of anonymous data for collections
3: /// </summary>
4: internal class CollectionsGenerator : ISpecimenBuilder
5: {
6: private readonly Fixture _parentFixture;
7:
8: /// <param name="parentFixture">Requires parentFixture to ensure existing options are used.</param>
9: public CollectionsGenerator(Fixture parentFixture)
10: {
11: _parentFixture = parentFixture;
12: }
13:
14: public object Create(object request, ISpecimenContext context)
15: {
16: if (!ReflectionHelper.IsGenericDotNetCollectionOrInterface(request))
17: return new NoSpecimen(request);
18:
19: Type collectionType;
20: if (ReflectionHelper.ObjectIsADotNetCollectionInterface(request))
21: collectionType = ReflectionHelper.GetConcreteCollectionTypeToMatchInterface(request);
22: else
23: collectionType = ReflectionHelper.GetUnderlyingSystemType(request);
24:
25: var returnCollection = (IList)Activator.CreateInstance(collectionType);
26:
27: AddAnonymousValuesToCollection(returnCollection, _parentFixture);
28:
29: return returnCollection;
30: }
31:
32: private static void AddAnonymousValuesToCollection(IList collection, Fixture parentFixture)
33: {
34: Type genericType = collection.GetType().GetGenericArguments()[0];
35: var createAnonymousMethod = typeof(SpecimenFactory).GetMethod("CreateAnonymous", new Type[] { typeof(ISpecimenBuilderComposer) }).MakeGenericMethod(new[] { genericType });
36: for (int i = 0; i < parentFixture.RepeatCount; i++)
37: {
38: collection.Add(createAnonymousMethod.Invoke(null, new ISpecimenBuilderComposer[] { parentFixture }));
39: }
40: }
41: }
And an accompanying ReflectionHelper:
1: public static class ReflectionHelper
2: {
3: private const string UnderlyingSystemTypeString = "UnderlyingSystemType";
4:
5: public static bool CanRetrieveUnderlyingSystemTypeFromObject(object input)
6: {
7: return (input != null)
8: && (input.GetType().GetProperty(UnderlyingSystemTypeString) != null)
9: && (input.GetType().GetProperty(UnderlyingSystemTypeString).GetValue(input, null) != null);
10: }
11:
12: public static bool InputIsAssignableFrom(object request, Type ofType)
13: {
14: return (request != null)
15: && (request.GetType().GetProperty(UnderlyingSystemTypeString) != null)
16: && (request.GetType().GetProperty(UnderlyingSystemTypeString).GetValue(request, null) != null)
17: && (ofType.IsAssignableFrom((Type)request.GetType().GetProperty(UnderlyingSystemTypeString).GetValue(request, null)));
18: }
19:
20: public static Type GetUnderlyingSystemType(object input)
21: {
22: return (Type)input.GetType().GetProperty(UnderlyingSystemTypeString).GetValue(input, null);
23: }
24:
25: public static bool ObjectHasGenericTypeSpecified(object input)
26: {
27: return GetUnderlyingSystemType(input).IsGenericType && GetUnderlyingSystemType(input).GetGenericArguments().Length > 0;
28: }
29:
30: public static bool IsGenericDotNetCollectionOrInterface(object request)
31: {
32: if (!CanRetrieveUnderlyingSystemTypeFromObject(request))
33: return false;
34:
35: return (ObjectIsADotNetCollection(request) || ObjectIsADotNetCollectionInterface(request))
36: && (ObjectHasGenericTypeSpecified(request));
37: }
38:
39: public static bool ObjectIsADotNetCollection(object request)
40: {
41: return InputIsAssignableFrom(request, typeof(IList));
42: }
43:
44: public static bool ObjectIsADotNetCollectionInterface(object request)
45: {
46: var objectTypeName = GetUnderlyingSystemType(request).ToString();
47:
48: var dotNetCollectionTypes = new List<string> //.NET Collections
49: {
50: "System.Collections.Generic.IList",
51: "System.Collections.Generic.IEnumerable",
52: "System.Collections.Generic.IEnumerator",
53: "System.Collections.Generic.ICollection",
54: "System.Collections.Generic.ISet",
55: "System.Collections.IList",
56: "System.Collections.IEnumerable",
57: "System.Collections.IEnumerator", 58: "System.Collections.ICollection",
59: };
60:
61: return dotNetCollectionTypes.Any(objectTypeName.Contains);
62: }
63:
64: public static Type GetConcreteListTypeToMatchInterface(object request)
65: {
66: Type genericType = GetUnderlyingSystemType(request).GetGenericArguments()[0];
67:
68: string genericListTypeName = "System.Collections.Generic.List`1"
69: + "[[" + genericType.AssemblyQualifiedName + "]]"
70: + ","
71: + Type.GetType("System.Collections.IList").Assembly.FullName;
72: return Type.GetType(genericListTypeName);
73: }
74: }
Note that this isn’t release quality. There’s a few issues, like with recursion if a complex type contains an instance of itself, and the way .NET collections are identified.
Adding a sample data generator in Silverlight for Visual Studio/Blend screen previews
The actual use case I was trying to solve was creating automatic sample data for XAML pages in Silverlight. This would enable me to better see how pages looked in Visual Studio and Blend, and a way of testing data bindings as well.
Note that AutoFixture doesn’t support Silverlight just yet, but there is a fork available that supports it (If you want to use a newer version, you only need to do a couple of changes to trunk to make it work).
I wanted a more natural interface for the sample data, so I created a SampleDataGenerator class:
1: /// <summary>
2: /// Creates sample data for object or collection of objects recursively
3: /// </summary>
4: public static class SampleDataGenerator
5: {
6: public static T GenerateObjectWithData<T>()
7: {
8: return CreateFixtureWithDefaultSetup<T>().CreateAnonymous<T>();
9: }
10:
11: public static T GenerateObjectWithData<T>(int collectionItemAmount)
12: {
13: var fixture = CreateFixtureWithDefaultSetup<T>();
14: fixture.RepeatCount = collectionItemAmount; //Object count generated per list
15: return fixture.CreateAnonymous<T>();
16: }
17:
18: public static IEnumerable<T> CreateMany<T>()
19: {
20: return CreateFixtureWithDefaultSetup<T>().CreateMany<T>();
21: }
22: public static IEnumerable<T> CreateMany<T>(T seed)
23: {
24: return CreateFixtureWithDefaultSetup<T>().CreateMany(seed);
25: }
26:
27: public static IEnumerable<T> CreateMany<T>(int count)
28: {
29: return CreateFixtureWithDefaultSetup<T>().CreateMany<T>(count);
30: }
31:
32: public static IEnumerable<T> CreateMany<T>(T seed, int count)
33: {
34: return CreateFixtureWithDefaultSetup<T>().CreateMany<T>(seed, count);
35: }
36:
37: private static Fixture CreateFixtureWithDefaultSetup<T>()
38: {
39: var fixture = new Fixture();
40: fixture.Customizations.Add(new StringGenerator(() => ""));
41: fixture.Customizations.Add(new CollectionsGenerator(fixture));
42: return fixture;
43: }
44: }
At the bottom of the code above I have included use of the CollectionsGenerator class. I also changed the default behavior of StringGenerator, so it only outputs the property name instead of property name + guid.
You can then use it in a sample object that inherits the ViewModel:
1: public class EditPersonSampleData : EditPersonViewModel
2: {
3: public EditPersonSampleData()
4: {
5: Person = SampleDataGenerator.GenerateObjectWithData<Person>();
6: }
7: }
And then in the EditPersonView.xaml, include:
1: xmlns:vm="clr-namespace:SomeNamespace.EditPerson"
2:
3: ...
4:
5: <Grid x:Name="LayoutRoot" Background="White" d:DataContext="{d:DesignInstance Type=vm:EditPersonSampleData, IsDesignTimeCreatable=True}">
6:
That’s all you need to get sample data visible in a XAML viewer. Good way of checking that the interface looks OK and that the binding is set up correctly.
Vote here to get similar support in the actual AutoFixture product.
2 comments:
Cool! Pretty good writeup. Thanks for that!
Even for a "non-production quality" effort I would like to commend your code for being easy to follow. I didn't even have to read every single character to understand what was going on, because you have well-named methods and classes. I don't know if you've read "Clean Code", but you seem to be following some of its practices :)
I am thrilled to see someone else take the AutoFixture API and run with it. How much time did you spend with AutoFixture before you created this?
BTW, the link to the collection support work item on CodePlex has too many trailing whitespace character, so it doesn't currently work.
Thank you.
No, thank you for making it :)
Not a lot of time really. Tested it, made it compile in SL, tried out a few things and read the cheat sheet to find out more about the usage.
It was simple to see how to extend it from the various concrete implementations of ISpecimenBuilder.
Spent some time debugging to see the usage, when Create was called and such, but spent more time with reflection and on a too complex approach to the interfaces than on the AutoFixture bits.
At the end I did have a quick look at RecursionGuard to see if I could use it to stop recursing a few steps down the tree if the type had an instance of itself, but could'nt quickly see a solution.
I'm a big fan of the Clean Code paradigm, but I've only briefly read the book. The way I see it it's a good summary of the related parts from Refactoring, Refactoring to Patterns, Code complete and even a dash of ddd.
Post a Comment