SpecSalad and Tables

I started a new home project the other day, a small website and started to write the features that I wanted, now I knew that SpecSalad supported tables in the usual Scenario Outline format as I had used them when writing the calculator example, but this time I wanted to include the table as part of the scenario for example

Scenario: a simple table in the given statement
Given I am a table specification
And I can see the table whats your favourite colour
| answer | vote |
| Red | 1 |
| Cucumber green | 1 |
| blue | 1 |
When I attempt to up vote the answer, Cucumber green
Then I should see the favourite colour is 'Cucumber green'

So simply expecting it to ‘Just Work’ I created my specification and watched it fail with some very interesting error messages! As it turned out there was actually no provision for the step definitions to take SpecFlow tables, so some time later I now have a couple of scenarios that can cope with tables both in the Given part of a scenario and the Then part, I didn’t add anything to the when syntax as I think currently this should be a unique call and probably won’t need tables.

So how does it work? using the above example first the given  line above uses the new SpecSalad syntax

Given I can see a table Table Name
Given you can see a table Table Name
Given the Role Name can see a table Table Name

Then I should see Table Name table
Then you should see Table Name table
Then the Role Name should see Table Name table

For the Given steps unusually you don’t create a task as the framework simply saves the table into the current scenario context using the table name supplied as the key. 

In the above scenario the rest of the steps are just the usual Spec Salad syntax, resulting the the following task code.

   1: public class UpVoteTheAnswer : ApplicationTask

   2:     {

   3:         public override object Perform_Task()

   4:         {

   5:             Role.UpVoteAnswer(Details.Value());

   6:  

   7:             return null;

   8:         }

   9:     }

this calls the role to add a vote for the selected colour, the role in this case is TableSpecification

   1: public class TableSpecification : ApplicationRole

   2:     {

   3:          public void UpVoteAnswer(string answer)

   4:          {

   5:              var currentVoting = (Table)Retrieve("whats your favourite colour");

   6:  

   7:              foreach (TableRow row in currentVoting.Rows)

   8:              {

   9:                  if (row["answer"] == answer)

  10:                  {

  11:                      int voteCount = Convert.ToInt32(row["vote"]);

  12:                      voteCount++;

  13:  

  14:                      row["vote"] = voteCount.ToString(CultureInfo.InvariantCulture);

  15:                  }

  16:              }

  17:  

  18:              StoreValue("whats your favourite colour", currentVoting);

  19:          }

  20:     }

this simply loops through the table stored in the scenario context looking for the given colour and adds one to the vote for that colour, re-saving the table back to the scenario context.

The then step

   1: public override object Perform_Task()

   2:         {

   3:             var votes = (Table)Retrieve("whats your favourite colour");

   4:  

   5:             string winningColour = "";

   6:             int[] winningVoteCount = {0};

   7:  

   8:             foreach (var row in votes.Rows.Where(row => Convert.ToInt32(row["vote"]) > winningVoteCount[0]))

   9:             {

  10:                 winningColour = row["answer"];

  11:                 winningVoteCount[0] = Convert.ToInt32(row["vote"]);

  12:             }

  13:  

  14:             return winningColour;

  15:         }

this loops through the stored table looking for a row with the highest vote count storing the winning colour, this is then returned from the task to the framework to compare to the value in the specification.

I also wanted a syntax that could compare multiple results, so I could have a draw and two winning colours, the scenario looks like.

Scenario: a simple table in the then statement
Given I am a table specification
And I can see the table whats your favourite colour
| answer | vote |
| Red | 1 |
| Cucumber green | 1 |
| blue | 1 |
When I attempt to up vote the answer, Cucumber green
And I attempt to up vote the answer, blue
Then I should see the favourite colours table
| answer | vote |
| Cucumber green | 2 |
| blue | 2 |

Most of the steps are the same, but now the then step also has a table with the expected results in, the There task looks like.

   1: public class TheFavouriteColours : ApplicationTask

   2:     {

   3:         public override object Perform_Task()

   4:         {

   5:             var table = (Table)this.Retrieve("whats your favourite colour");

   6:  

   7:             int maxCount = table.Rows.Select(row => Convert.ToInt32(row["vote"])).Concat(new[] {0}).Max();

   8:  

   9:             var result = new Table(table.Header.ToArray());

  10:  

  11:             foreach (var row in table.Rows.Where(row => Convert.ToInt32(row["vote"]) == maxCount))

  12:             {

  13:                 result.AddRow(row);

  14:             }

  15:  

  16:             return result;

  17:         }

The then task first retrieves the table stored in the context, then goes through the table to find the top vote. 
A new table is created that has the same shape as the table in the context and into that table each row that has the max score is added, it is then this new table that is returned  to the framework to be compared to the expected results.

This last part is interesting as I am attempting to compare one table with another, not knowing if the two tables are even the same shape!

   1: private void ValidateTableAnswers(Table actualAnswers, Table expectedAnswers)

   2:         {

   3:             Assert.That(actualAnswers.RowCount, Is.EqualTo(expectedAnswers.RowCount), "row counts do not match");

   4:  

   5:             var expectedValues = new List<string>();

   6:  

   7:             foreach (TableRow row in expectedAnswers.Rows)

   8:             {

   9:                 var builder = new StringBuilder();

  10:                 foreach (var key in row.Keys)

  11:                 {

  12:                     builder.Append(row[key]);

  13:                     builder.Append(",");

  14:                 }

  15:  

  16:                 expectedValues.Add(builder.ToString());

  17:             }

  18:  

  19:             foreach (TableRow row in actualAnswers.Rows)

  20:             {

  21:                 var builder = new StringBuilder();

  22:                 foreach (var key in row.Keys)

  23:                 {

  24:                     builder.Append(row[key]);

  25:                     builder.Append(",");

  26:                 }

  27:  

  28:                 string found = (from v in expectedValues where v == builder.ToString() select v).FirstOrDefault();

  29:  

  30:                 Assert.That(found, Is.Not.Null, "values not found in expected table");

  31:             } 

  32:         }

First up the framework asserts that the two tables have the same number of rows, after which the framework builds a list containing line contains the combined row values in the expected table. 
The framework then iterates over the actual answers table and builds a string of the row values which is checked against the list form the expected values to ensure it exists. 

I am not sure if this is the best way to achieve this, but currently it works!

This new syntax is available on Nuget as version 1.9.1

Advertisements

About Duncan Butler

Trying to be a very agile software developer, working in C# with Specflow, Nunit and Machine Specifications, and in the evening having fun with Ruby and Rails
This entry was posted in Programming and tagged . Bookmark the permalink.

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