LINQ-based instance and set comparison in SpecFlow

A few weeks ago I blogged about a proposed feature: enhance SpecFlow Assist comparison helpers with support for different comparison modes. The enchancment was based on introduction of an enumerator that would specify how a table should be compared with an instance or set: TheTableAndSetMatchWithoutRegardForOrder, TheTableAndSetAreAnExactMatch etc. In a discussion in SpecFlow Google group there came another suggestion: use LINQ instead of enumerator. It would open for use of any set transformation operation supported by LINQ.

Now that the feature implementation has been accepted and will become a part of next SpecFlow release, I would like first to show how it works and then explain how it is implemented.

How it works

So let’s look at the following scenario (you will never want to mix so many validations in a single scenario but I just want to take all at once):

Scenario: Match
When I have a collection
| Artist | Album |
|
Beatles | Rubber Soul |
|
Pink Floyd | Animals |
|
Muse | Absolution |
Then it should match
| Artist | Album |
| Beatles | Rubber Soul |
|
Pink Floyd | Animals |
|
Muse | Absolution |
And it should match
| Artist | Album |
|
Beatles | Rubber Soul |
|
Muse | Absolution |
|
Pink Floyd | Animals |
And it should exactly match
| Artist | Album |
|
Beatles | Rubber Soul |
|
Pink Floyd | Animals |
|
Muse | Absolution |
But it should not match
| Artist | Album |
|
Beatles | Rubber Soul |
|
Queen | Jazz |
|
Muse | Absolution |
And it should not match
| Artist | Album |
|
Beatles | Rubber Soul |
|
Muse | Absolution |
And it should not exactly match
| Artist | Album |
|
Beatles | Rubber Soul |
| Muse | Absolution |
|
Pink Floyd | Animals |

CompareToSet Table extension method only checks for equivalence of collections which is a reasonable default. But with LINQ-based operations each of the above comparisons can be expressed using a single line of code:

[When(@"I have a collection")]
public void WhenIHaveACollection(Table table)
{
    var collection = table.CreateSet<Item>();
    ScenarioContext.Current.Add("Collection", collection);
}

[Then(@"it should match")]
public void ThenItShouldMatch(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsTrue(table.RowCount == collection.Count() && table.ToProjection<Item>().Except(collection.ToProjection()).Count() == 0);
}

[Then(@"it should exactly match")]
public void ThenItShouldExactlyMatch(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsTrue(table.ToProjection<Item>().SequenceEqual(collection.ToProjection()));
}

[Then(@"it should not match")]
public void ThenItShouldNotMatch(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsFalse(table.RowCount == collection.Count() && table.ToProjection<Item>().Except(collection.ToProjection()).Count() == 0);
}

[Then(@"it should not exactly match")]
public void ThenItShouldNotExactlyMatch(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsFalse(table.ToProjection<Item>().SequenceEqual(collection.ToProjection()));
}

In a similar way we can implement containment validation:

Scenario: Containment

When I have a collection

| Artist | Album |

| Beatles | Rubber Soul |

|
Pink Floyd | Animals |

|
Muse | Absolution |

Then it should contain all items

| Artist | Album |

| Beatles | Rubber Soul |

|
Muse | Absolution |

But it should not contain all items

| Artist | Album |

| Beatles | Rubber Soul |

|
Muse | Resistance |

And it should not contain any of items

| Artist | Album |

| Beatles | Abbey Road |

|
Muse | Resistance |

And here are corresponding step definitions:

[Then(@"it should contain all items")]
public void ThenItShouldContainAllItems(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsTrue(table.ToProjection<Item>().Except(collection.ToProjection()).Count() == 0);
}

[Then(@"it should not contain all items")]
public void ThenItShouldNotContainAllItems(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsFalse(table.ToProjection<Item>().Except(collection.ToProjection()).Count() == 0);
}

[Then(@"it should not contain any of items")]
public void ThenItShouldNotContainAnyOfItems(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable<Item>;
    Assert.IsTrue(table.ToProjection<Item>().Except(collection.ToProjection()).Count() == table.RowCount);
}

ToProjection, ToProjectionOfSet and ToProjectionOfInstance

It’s so easy to compare tables to collections using CompareToSet<T>, why do we need to call ToProjection both for table and collection? The answer becomes clear once you take a look at Table class definition. A Table class does not implement IEnumerable, it is a composite of a header and collection of rows. So it’s impossible to call Except or SequenceEqual directly on an instance of a Table. But it’s not only support for IEnumerable that requires introduction of an adapter class: a collection to be compared to does not need to be a set of items of a type known at compile time. It can be generated as a result of execution of a LINQ statement, so collection elements will have anonymous type.

What if Artist and Album are properties of different entities? Look at this piece of code:

    var collection = from x in ctx.Artists 
                     where x.Name == "Muse"
                     join y in ctx.Albums on
                     x.ArtistId equals y.ArtistId
                     select new
                     {
                         Artist = x.Name,
                         Album = y.Name
                     };

A “collection” object represents a projection of a join of two tables, so if we want to compare a Table instance to this collection, we should be able to compare Table to a set of anonymous classes. This makes it tricky to implement an adapter class: we define a generic class of “T”, but T in the example above is an anonymous type.

So here is how it works. SpecFlow.Assist has a new generic class EnumerableProjection<T>. If a type “T” is known at compile time, “ToProjection” method converts a table or a collection straight to an instance of EnumerableProjection:

    table.ToProjection<Item>();

But if we need to compare a table with the collection of anonymous types from the example above, we need to express this type in some way so ToProjection will be able to build an instance of specialized EnumerableProjection. This is done by sending a collection as an argument to ToProjection. And to support both sets and instances and avoid naming ambiguity, corresponding methods are called ToProjectionOfSet and ToProjectionOfInstance:

    table.ToProjectionOfSet(collection);
    table.ToProjectionOfInstance(instance);

Here are the definitions of SpecFlow Table extensions methods that convert tables and collections of IEnumerables to EnumerableProjection:

    public static IEnumerable<Projection<T>> ToProjection<T>(this IEnumerable<T> collection, Table table = null)
    {
        return new EnumerableProjection<T>(table, collection);
    }

    public static IEnumerable<Projection<T>> ToProjection<T>(this Table table)
    {
        return new EnumerableProjection<T>(table);
    }

    public static IEnumerable<Projection<T>> ToProjectionOfSet<T>(this Table table, IEnumerable<T> collection)
    {
        return new EnumerableProjection<T>(table);
    }

    public static IEnumerable<Projection<T>> ToProjectionOfInstance<T>(this Table table, T instance)
    {
        return new EnumerableProjection<T>(table);
    }

Note that last arguments of ToProjectionOfSet and ToProjectionOfInstance methods are not used in method implementation! Their only purpose is to bring information about “T”, so the EnumerableProjection adapter class can be built properly. Now we can perform the following comparisons with anomymous types collections and instances:

[Test]
public void Table_with_subset_of_columns_with_matching_values_should_match_collection()
{
    var table = CreateTableWithSubsetOfColumns();
    table.AddRow(1.ToString(), "a");
    table.AddRow(2.ToString(), "b");

    var query = from x in testCollection
                select new { x.GuidProperty, x.IntProperty, x.StringProperty };

    Assert.AreEqual(0, table.ToProjectionOfSet(query).Except(query.ToProjection()).Count());
}

[Test]
public void Table_with_subset_of_columns_should_be_equal_to_matching_instance()
{
    var table = CreateTableWithSubsetOfColumns();
    table.AddRow(1.ToString(), "a");

    var instance = new { IntProperty = testInstance.IntProperty, StringProperty = testInstance.StringProperty };

    Assert.AreEqual(table.ToProjectionOfInstance(instance), instance);
}

Powerful helpers, simple step definitions

SpecFlow is becoming preferred BDD tool for many .NET developers, it has been easy to use from the beginning, and recent enhancements like Intellisense suport for Gherkin makes it really addictive. SpecFlow.Assist table management helpers add to the efficiency of the product by simplifying collection instantiation and comparison. I believe adding LINQ support to Table extension methods will help developers write more compact scenario step definitions.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s