A Better Table.CreateInstance() for SpecFlow

SpecFlow remains my top choice for automated integration testing. I love writing cute little cukes and putting them together in different ways to create different test scenarios. And one of my favorite cuke tricks is using tables to define an object, which I wrote about some time ago.

The CreateInstance<T> extension method provides an easy way to convert that table data into objects for testing, but I really think there should be a better way to populate child properties of complex objects.

Consider this example:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
}

It would be nice if you could write a cuke like this:

Given a person
	| field          | value         |
	| firstName      | adam          |
	| lastName       | prescott      |
	| address.street | 123 number ln |
	| address.city   | anytown       |
	| address.state  | ny            |
	| address.zip    | 10000         |

And then convert it to a person like this:

[Given(@"a person")]
public void GivenAPerson(Table table)
{
    var person = table.CreateInstance<Person>();
    ScenarioContext.Current.Set(person);
}

But you can’t. Well, you can, but the address property won’t be populated. I didn’t like that, so I decided to grow my own. It works by identifying the child properties in the table and creating sub-tables, then using reflection to find and set the property on the result object. It works recursively, too, so you could even go n properties deep (i.e., address.state.abbreviation).

At its core, it’s just using CreateInstance<T> so you get all the niceties that go along with that. Note also that it only works with the 2-column, vertical tables with field and value columns. I called my extension method BuildInstance since CreateInstance was already taken. Here it is… Enjoy!

using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;

public static class TableExtensions
{
    public static T BuildInstance<T>(this Table table)
    {
        T result = table.CreateInstance<T>();

        // find sub-properties by looking for "."
        var propNames = table.Rows.OfType<TableRow>()
            .Where(x => x[0].Contains("."))
            .Select(x => Regex.Replace(x[0], @"^(.+?)\..+$", "$1"));

        foreach (var propName in propNames)
        {
            // look for matching property in result object
            var prop = typeof(T).GetProperty(
                propName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

            if (prop != null)
            {
                // create sub-table with relevant rows (field == propName.something)
                var subTable = new Table("field", "value");
                var re = new Regex(string.Format(@"^{0}\.([^\.]*)$", propName), RegexOptions.IgnoreCase);
                table.Rows.OfType<TableRow>().Where(x => re.IsMatch(x[0]))
                    .Select(x => new[] { re.Replace(x[0], "$1"), x[1] })
                    .ToList()
                    .ForEach(x => subTable.AddRow(x));

                // make recursive call to create child object
                var createInstance = typeof(TableExtensions)
                    .GetMethod(
                        "BuildInstance",
                        BindingFlags.Public | BindingFlags.Static,
                        null,
                        CallingConventions.Any,
                        new Type[] { typeof(Table) },
                        null);
                createInstance = createInstance.MakeGenericMethod(prop.PropertyType);
                object propValue = createInstance.Invoke(null, new object[] { subTable });

                // assign child object to result
                prop.SetValue(result, propValue);
            }
        }

        return result;
    }
}
Advertisement

Database SpecFlow Scenarios and TransactionScope

My team has been using SpecFlow to verify that our data access components are working correctly. In an effort to write repeatable tests that won’t pollute the database, we’ve decided to wrap each scenario in a transaction.

Creating and rolling back the transactions is very simple to do with SpecFlow’s BeforeScenario & AfterScenario hooks.

using System.Transactions;

[Binding]
public class TransactionHooks
{
    private TransactionScope _transactionScope;

    [BeforeScenario]
    public void BeforeScenario()
    {
        _transactionScope = new TransactionScope();
    }

    [AfterScenario]
    public void AfterScenario()
    {
        _transactionScope.Dispose();
    }
}

By including these hooks, you’re placing your test scenarios in the loving embrace of a transaction scope. The act of instantiating the TransactionScope will update the ambient transaction that will be used by all subsequent code unless you explicitly tell it to do otherwise.

If you’re testing code that uses TransactionScope itself, using TransactionScopeOption.Required will allow the code to use the ambient transaction if one exists. Note that this is the default value, so it’s what you’re using if you’re not explicitly specifying an option. However, the other TransactionScopeOption values will cause code to execute outside your test scenario’s ambient transaction by either creating a new/different transaction (RequiredNew) or executing outside the transaction (Suppress).

Learning Cukes

Cucumber

I’ve written [several] [posts] [in] [the] [past] about my team’s adoption of SpecFlow and BDD, and I’m still loving it several months later. The large project that we started with has nearly 10,000 lines of code and 93% code coverage. We’ve gone through several large refactors, and each time we walk away with a high level of confidence that no functionality was lost or affected negatively. It’s been a really great experience.

One of the challenges of adoption was just learning the Cucumber step definition syntax, or rather, how to write cukes. For getting started, I recommend taking a look at this page for a good list of descriptions, scenarios, and examples. If you’re using SpecFlow, you may also want to check out their step definition documentation on GitHub.

Once you’ve got the basic syntax down, the hard part begins. My team hasn’t had much formal discussion about Cucumber best practices, and we’re still learning what works and what doesn’t. If you look around online, you can find a few good articles with helpful suggestions, though.

Here’s a great post that I recommend reading. This article offers advice on just about every aspect of creating and managing your cukes, from feature files to tags to running and refactoring.

I also found this post from EggsOnBread to be very helpful. All of the recommended practices are good. This was one of the first articles I read when I was getting started, and it’s served me well. I’ll be honest, though–many of the points didn’t stick during my initial read. It became much more valuable after spending several months working with Cucumber and then re-reading.

ScenarioContext in SpecFlow

I’ve been using SpecFlow pretty regularly for a few weeks now, and I must say, I’m a fan. I find that it’s a lot easier to do test-first development because I’m writing the test in human-readable, business language.

One of the ideas that I didn’t understand right away was how to re-use generated step definitions across features and scenarios. ScenarioContext and FeatureContext give you great options to handle this, though. Let’s check out an example using a modified version of the default scenario SpecFlow generates with a new feature file:

Scenario: Add two numbers
	Given I enter 50 into the calculator
	And I press plus
	And I enter 70 into the calculator
	When I press enter
	Then the result should be 120 be displayed

When I generate step definitions, I might end up with a class that looks like this:

namespace adamprescott.net.Calculator.SpecFlow
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using TechTalk.SpecFlow;

    [Binding]
    public class Calculator_AddSteps
    {
        Calculator calculator = new Calculator();

        [Given(@"I enter (.*) into the calculator")]
        public void GivenIEnterIntoTheCalculator(int p0)
        {
            calculator.Number(p0);
        }

        [Given(@"I press plus")]
        public void GivenIPressPlus()
        {
            calculator.Plus();
        }

        [When(@"I press enter")]
        public void WhenIPressEnter()
        {
            calculator.Enter();
        }

        [Then(@"the result should be (.*) be displayed")]
        public void ThenTheResultShouldBeBeDisplayed(int p0)
        {
            Assert.AreEqual(Convert.ToString(p0), calculator.Display);
        }
    }
}

Okay, not bad. A logical next feature might be subtraction. Some of the steps, like entering numbers and pressing enter, are shared. It would be nice if we could re-use those, but they’re configured to manipulate private variables in the Calculator_AddSteps class. So let’s do some refactoring! Instead of using a member-level variable, I can store my Calculator object in the ScenarioContext, making it accessible to other steps being executed in the same scenario.

// store to ScenarioContext like this:
ScenarioContext.Current.Set<Calculator>(new Calculator());
ScenarioContext.Current.Set<Calculator>(new Calculator(), "Calc");
ScenarioContext.Current["Calc"] = new Calculator();

// retrieve from ScenarioContext like this:
var c = ScenarioContext.Current.Get<Calculator>();
var c = ScenarioContext.Current.Get<Calculator>("Calc");
var c = ScenarioContext.Current["Calc"] as Calculator;

This is overkill for such a simple example, but I separated my shared steps into a new step definitions file. The final solution has three classes for the step definitions: Calculator_AddSteps, Calculator_SubtractSteps, and Calculator_SharedSteps.

Here’s the final solution, broken up by file:

Calculator_Add.feature

Feature: Calculator_Add
	In order to avoid silly mistakes
	As a math idiot
	I want to be told the sum of two numbers

Background:
	Given I have a calculator

Scenario: Add two numbers
	Given I enter 50 into the calculator
	And I press plus
	And I enter 70 into the calculator
	When I press enter
	Then the result should be 120 be displayed

Calculator_AddSteps.cs

namespace adamprescott.net.Calculator.SpecFlow
{
    using TechTalk.SpecFlow;

    [Binding]
    public class Calculator_AddSteps
    {
        [Given(@"I press plus")]
        public void GivenIPressPlus()
        {
            ScenarioContext.Current.Get<Calculator>().Plus();
        }
    }
}

Calculator_SharedSteps.cs

namespace adamprescott.net.Calculator.SpecFlow
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using TechTalk.SpecFlow;

    [Binding]
    public class Calculator_SharedSteps
    {
        [Given(@"I have a calculator")]
        public void GivenIHaveACalculator()
        {
            ScenarioContext.Current.Set<Calculator>(new Calculator());
        }

        [Given(@"I enter (.*) into the calculator")]
        public void GivenIEnterIntoTheCalculator(int p0)
        {
            ScenarioContext.Current.Get<Calculator>().Number(p0);
        }

        [When(@"I press enter")]
        public void WhenIPressEnter()
        {
            ScenarioContext.Current.Get<Calculator>().Enter();
        }

        [Then(@"the result should be (.*) be displayed")]
        public void ThenTheResultShouldBeBeDisplayed(int p0)
        {
            Assert.AreEqual(Convert.ToString(p0), ScenarioContext.Current.Get<Calculator>().Display);
        }
    }
}

Calculator_Subtract.feature

Feature: Calculator_Subtract
	In order to avoid silly mistakes
	As a math idiot
	I want to be told the difference between two numbers

Background:
	Given I have a calculator

Scenario: Subtract two numbers
	Given I enter 70 into the calculator
	And I press minus
	And I enter 50 into the calculator
	When I press enter
	Then the result should be 20 be displayed

Calculator_SubtractSteps.cs

namespace adamprescott.net.Calculator.SpecFlow
{
    using TechTalk.SpecFlow;

    [Binding]
    public class Calculator_SubtractSteps
    {
        [Given(@"I press minus")]
        public void GivenIPressMinus()
        {
            ScenarioContext.Current.Get<Calculator>().Minus();
        }
    }
}

BDD in .NET with SpecFlow

Yesterday, I was invited to sit in on a demo/discussion about behavior-driven development (BDD) with SpecFlow. As a self-proclaimed unit test enthusiast, I was excited right away.

I’ve been doing my own, made-up version of BDD for a while. For example, I would have a function that calls several sub-methods. I’d mock the sub-methods and write tests to verify that the sub-methods were called. Then I’d move to the next “layer” by implementing the sub-methods in a similar fashion, repeating until I hit the bottom layer, where the actual work happens.

The problem with that approach is that all tested behaviors relate to the code and not business requirements or real-world scenarios. SpecFlow fixes that by using a Gherkin parser, allowing you to write test scenarios using business-readable, domain-specific language instead of code.

Here’s how you can get started with SpecFlow and Visual Studio. This tutorial is basically a modified version of the Project Setup Guide on the SpecFlow site. I ran into a couple issues with their tutorial, though, and their tutorial also doesn’t get much into implementing the application logic to fix unit tests once they’re generated.

Installing SpecFlow

SpecFlow is included in the Visual Studio Extensions Gallery, so installing it is a breeze.

  1. Open Visual Studio, and go to Tools > Extensions and Updates (Extension Manager in VS2010)
  2. Select the Online gallery
  3. Search for “SpecFlow”
  4. Install it!

Nice, now you’re ready to create your first test project.

Creating your first test project

SpecFlow tests are defined as “features” that have multiple scenarios. There is a small amount of project setup that needs to be done, but it’s not too bad and you can be up and running in just a few minutes.

  1. In Visual Studio, create a new Unit Test Project
  2. Install SpecFlow via NuGet
    • PM> Install-Package SpecFlow
    • I had to manually add the unit test provider to my app.config:
      <?xml version="1.0" encoding="utf-8"?>
      <configuration>
        <configSections>
          <section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
        </configSections>
        <specFlow>
          <unitTestProvider name="MsTest" />
        </specFlow>
      </configuration>
      
  3. Add a SpecFlow Feature to your project
  4. Generate Step Definitions by right-clicking in your newly created feature file

  5. Run your scenario by right-clicking in the feature file

    • Right-clicking in the feature and choosing Run SpecFlow Scenarios produced an error for me; I’ll be looking into this
    • I instead ran the unit tests as usual, and that worked fine
  6. Implement your code/step definitions until you have a passing test
    • I added my class-to-be-tested to the same file as my step definitions for brevity
      namespace adamprescott.net.SpecFlow.FirstProject
      {
          using Microsoft.VisualStudio.TestTools.UnitTesting;
          using System.Collections.Generic;
          using System.Linq;
          using TechTalk.SpecFlow;
      
          public class AddingMaching
          {
              private List<int> _numbers = new List<int>();
              private int _sum;
      
              public int Screen
              {
                  get { return _sum; }
              }
      
              public void EnterNumber(int num)
              {
                  _numbers.Add(num);
              }
              public void PressAdd()
              {
                  _sum = _numbers.Sum();
              }
          }
      
          [Binding]
          public class MyFirstFeatureSteps
          {
              private AddingMaching _addingMachine;
      
              public MyFirstFeatureSteps()
              {
                  _addingMachine = new AddingMaching();
              }
      
              [Given(@"I have entered (.*) into the calculator")]
              public void GivenIHaveEnteredIntoTheCalculator(int p0)
              {
                  _addingMachine.EnterNumber(p0);
              }
      
              [When(@"I press add")]
              public void WhenIPressAdd()
              {
                  _addingMachine.PressAdd();
              }
      
              [Then(@"the result should be (.*) on the screen")]
              public void ThenTheResultShouldBeOnTheScreen(int p0)
              {
                  Assert.AreEqual(p0, _addingMachine.Screen);
              }
          }
      }
      
    • The example shows a simple AddingMachine class with implemented steps to pass the default feature scenario
      Feature: MyFirstFeature
      	In order to avoid silly mistakes
      	As a math idiot
      	I want to be told the sum of two numbers
      
      @mytag
      Scenario: Add two numbers
      	Given I have entered 50 into the calculator
      	And I have entered 70 into the calculator
      	When I press add
      	Then the result should be 120 on the screen
      

That’s it! Now you should have a passing unit test that verifies the business-language requirement. You can add more scenarios to your feature file with different values and/or steps. Note that if you add more steps, you’ll probably need to re-generate step definitions. When you do this, only new step definitions will be generated. I recommend using the Copy methods to clipboard button in the Generate Step Definitions dialog to avoid overwriting the previously created and implemented step definitions.

%d bloggers like this: