You can use Albedo with AutoFixture to build custom conventions.

In a question to one of my previous posts, Jeff Soper asks about using custom, string-based conventions for AutoFixture:

"I always wince when testing for the ParameterType.Name value [...] It seems like it makes a test that would use this implementation very brittle."
Jeff's concern is that when you're explicitly looking for a parameter (or property or field) with a particular name (like "currencyCode"), the unit test suite may become brittle, because if you change the parameter name, the string may retain the old name, and the Customization no longer works.

Jeff goes on to say:

"This makes me think that I shouldn't need to be doing this, and that a design refactoring of my SUT would be a better option."
His concerns can be addressed on several different levels, but in this post, I'll show you how you can leverage Albedo to address some of them.

If you often find yourself in a situation where you're writing an AutoFixture Customization based on string matching of parameters, properties or fields, you should ask yourself if you're targeting one specific class, or if you're writing a convention? If you often target individual specific classes, you probably need to rethink your strategy, but you can easily run into situations where you need to introduce true conventions in your code base. This can be beneficial, because it'll make your code more consistent.

Here's an example from the code base in which I'm currently working. It's a REST service written in F#. To model the JSON going in and out, I've defined some Data Transfer Records, and some of them contain dates. However, JSON doesn't deal particularly well with dates, so they're treated as strings. Here's a JSON representation of a comment:

{
    "author": {
        "id": "1234",
        "name": "Mark Seemann",
        "email": "1234@ploeh.dk"
    },
    "createdDate": "2014-04-30T18:14:08.1051775+00:00",
    "text": "Is this a comment?"
}

The record is defined like this:

type CommentRendition = {
    Author : PersonRendition
    CreatedDate : string
    Text : string }

This is a problem for AutoFixture, because it sees CreatedDate as a string, and populates it with an anonymous string. However, much of the code base expects the CreatedDate to be a proper date and time value, which can be parsed into a DateTimeOffset value. This would cause many tests to fail if I didn't change the behaviour.

Instead of explicitly targeting the CreatedDate property on the CommentRendition record, I defined a conventions: any parameter, field, or property that ends with "date" and has the type string, should be populated with a valid string representation of a date and time.

This is easy to write as a one-off Customization, but then it turned out that I needed an almost similar Customization for IDs: any parameter, field, or property that ends with "id" and has the type string, should be populated with a valid GUID string formatted in a certain way.

Because ParameterInfo, PropertyInfo, and FieldInfo share little polymorphic behaviour, it's time to pull out Albedo, which was created for situations like this. Here's a reusable convention which can check any parameter, proeprty, or field for a given name suffix:

type TextEndsWithConvention(value, found) =
    inherit ReflectionVisitor<bool>()
        
    let proceed x =
        TextEndsWithConvention (value, x || found) :> IReflectionVisitor<bool>
 
    let isMatch t (name : string) =
        t = typeof<string>
        && name.EndsWith(value, StringComparison.OrdinalIgnoreCase)
 
    override this.Value = found
 
    override this.Visit (pie : ParameterInfoElement) =
        let pi = pie.ParameterInfo
        isMatch pi.ParameterType pi.Name |> proceed
 
    override this.Visit (pie : PropertyInfoElement) =
        let pi = pie.PropertyInfo
        isMatch pi.PropertyType pi.Name |> proceed
 
    override this.Visit (fie : FieldInfoElement) =
        let fi = fie.FieldInfo
        isMatch fi.FieldType fi.Name |> proceed
 
    static member Matches value request =
        let refraction =
            CompositeReflectionElementRefraction<obj>(
                [|
                    ParameterInfoElementRefraction<obj>() :> IReflectionElementRefraction<obj>
                    PropertyInfoElementRefraction<obj>()  :> IReflectionElementRefraction<obj>
                    FieldInfoElementRefraction<obj>()     :> IReflectionElementRefraction<obj>
                |])
        let r = refraction.Refract [request]
        r.Accept(TextEndsWithConvention(value, false)).Value

It simply aggregates a boolean value (found), based on the name and type of various properties, fields, and parameters that comes its way. If there's a match, found will be true; otherwise, it'll be false.

The date convention is now trivial:

type DateStringCustomization() =
    let builder = {
        new ISpecimenBuilder with
            member this.Create(request, context) =
                if request |> TextEndsWithConvention.Matches "date"
                then box ((context.Resolve typeof<DateTimeOffset>).ToString())
                else NoSpecimen request |> box }
 
    interface ICustomization with
        member this.Customize fixture = fixture.Customizations.Add builder

The ID convention is very similar:

type IdStringCustomization() =
    let builder = {
        new ISpecimenBuilder with
            member this.Create(request, context) =
                if request |> TextEndsWithConvention.Matches "id"
                then box ((context.Resolve typeof<Guid> :?> Guid).ToString "N")
                else NoSpecimen request |> box }
 
    interface ICustomization with
        member this.Customize fixture = fixture.Customizations.Add builder

With these conventions in place in my entire test suite, I can simply follow them and get correct values. What happens if I refactor one of my fields so that they no longer have the correct suffix? That's likely to break my tests, but that's a good thing, because it alerts me that I deviated from the conventions, and (inadvertently, I should hope) made the production code less consistent.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Thursday, 01 May 2014 21:40:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!