Enhancing SpecFlow set comparison methods

SpecFlow has an excellent set of Table extension methods contributed by Darren Cauthon (he also made a video available on TekPub). Methods such as CreateInstance, CreateSet or CompareSet enable conversion of SpecFlow tables to strong-typed item collections and comparing tables to IEnumerable generic types. Here is an example of such comparison.

First we create a feature scenario:

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 here are the step definitions:

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

[Then(@"it should match")]
public void ThenItShouldMatch(Table table)
{
    var collection = ScenarioContext.Current["Collection"] as IEnumerable;
    table.CompareToSet(collection);
}

Use of CompareToSet is a great time-saver, but it current implementation (1.6) covers only collection equivalence: two collections are considered to be equivalent if they have the same items without regard for order. But there may be scenarios when collections need to be compared differently. If you check MsTest helper class CollectionAssert, you will find methods like IsSubsetOf, Contains, DoesNotContain etc. Comparison criteria can be divided in two groups, with four modes in each. Positive comparison group include the following criteria:

  • Equivalence (same items in both collections, order is insignificant, corresponds to CompareToSet implementation in SpecFlow 1.6 and earlier versions);
  • Equality (same items in the same order);
  • Subset (one collection is a subset of another collection);
  • Intersection (collections have common items).

Negative comparison group consists of the same comparison criteria, but in negated form: not equivalent, not equal, not subset and empty intersection.

CompareToSet<T> can be extended with these criteria, it just needs extra optional parameters and enumerator type that lists comparison modes:

public enum TableComparison
{
    TheTableAndSetMatchWithoutRegardForOrder,
    TheTableAndSetAreAnExactMatch,
    AllItemsInTheTableCanBeFoundWithinTheSet,
    TheTableAndSetHaveCommonItems
}

public static void CompareToSet(this Table table, IEnumerable set, 
    TableComparison comparisonMode = TableComparison.TheTableAndSetMatchWithoutRegardForOrder, 
    bool expectedMatch = true)

Calling CompareToSet without specifying optional parameters will result in test for equivalence, while specifying explicit enumerator value can choose other criteria. In addition, passing “false” ad expectedMatch will assert for comparison failure. Here are examples of using enhanced version of CompareToSet:

Feature: Table comparison

In order to avoid writing my own collection comparison code

As a SpecFlow user

I want to compare collections using different criteria

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 |

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 the step definitions:

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

    [Then(@"it should match")]
    public void ThenItShouldMatch(Table table)
    {
        Compare(table);
    }

    [Then(@"it should exactly match")]
    public void ThenItShouldExactlyMatch(Table table)
    {
        Compare(table, TableComparison.TheTableAndSetAreAnExactMatch);
    }

    [Then(@"it should not match")]
    public void ThenItShouldNotMatch(Table table)
    {
        Compare(table, TableComparison.TheTableAndSetMatchWithoutRegardForOrder, false);
    }

    [Then(@"it should not exactly match")]
    public void ThenItShouldNotExactlyMatch(Table table)
    {
        Compare(table, TableComparison.TheTableAndSetAreAnExactMatch, false);
    }

    [Then(@"it should contain all items")]
    public void ThenItShouldContainAllItems(Table table)
    {
        Compare(table, TableComparison.AllItemsInTheTableCanBeFoundWithinTheSet);
    }

    [Then(@"it should not contain all items")]
    public void ThenItShouldNotContainAllItems(Table table)
    {
        Compare(table, TableComparison.AllItemsInTheTableCanBeFoundWithinTheSet, false);
    }

    [Then(@"it should not contain any of items")]
    public void ThenItShouldNotContainAnyOfItems(Table table)
    {
        Compare(table, TableComparison.TheTableAndSetHaveCommonItems, false);
    }

    private void Compare(Table table, TableComparison comparisonMode = TableComparison.TheTableAndSetMatchWithoutRegardForOrder, bool expectedMatch = true)
    {
        var collection = ScenarioContext.Current["Collection"] as IEnumerable;
        table.CompareToSet(collection, comparisonMode, expectedMatch);
    }
}

Currently this extensions are a proposition that I created in a forked branch of SpecFlow, available here.

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