SpecFlow's Table and TableRow Guidelines

In this walkthrough, you will get experience with handling SpecFlow's Table and TableRow objects with the UAT Automation Kit.

  • Complete the Get Started walkthrough. For each new project, you must:
    • Add the UAT Automation Kit NuGet packages.
    • Ensure that ChromeDriver is on your path.
    • Add target environment URLs to the App.config file.
  • Complete the Selenium WebDriver walkthrough.
  • Access to a Blackbaud CRM instance to test against.
  • Familiarity with adding tests and step implementations to existing feature and step files.
  • Familiarity with accessing the UAT Automation Kit Core API.
  • Familiarity with modifying the App.config to change which application the tests run against.
  • Familiarity with identifying the unique attribute values for the XPath constructors in the Core API and completion of the XPath Guidelines walkthrough.

This tutorial guides you through the steps to handle SpecFlow's Table and TableRow objects with the . In this walkthrough, you will:

  • Create a test to add an address to a constituent to explore the old approach for using tables to pass variables to .NET step methods.
  • Manipulate the format of the table to create cleaner, more adaptable code and take advantage of features.
  • Modify the example to add multiple addreses by adding rows to the table.
  • Create a test that edits an existing address.

SpecFlow feature files allow you to use tables to pass variables to .NET step methods.

Test to add address to constituent

Gherkin
@DelvingDeeper
Scenario: Add an address to a constituent
  Given I have logged into BBCRM
    And a constituent exists with last name "Enterprise"
  When I add an address to the current constituent
  | Field                            | Value                 |
  | Type                             | Business              |
  | Country                          | United States         |
  | Address                          | 2000 Daniel Island Dr |
  | City                             | Charleston            |
  | State                            | SC                    |
  | ZIP                              | 29492                 |
  | Do not send mail to this address | checked               |
  Then an address exists
  | Field               | Value                 |
  | Contact information | 2000 Daniel Island Dr |
  | Type                | Business              |

At some point, the test example attempts to set the fields on the Add an address dialog.

Add an address dialog

Specflow creates bindings between test cases and step methods. Field variables for the Add an address dialog are passed through the Table parameter.

Step method with Table parameter

C#
using System;
using System.Collections.Generic;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Crm;
using TechTalk.SpecFlow;
 
namespace Delving_Deeper
{
    [Binding]
    public class SampleTestsSteps : BaseSteps
    {
        [Given(@"I have logged into BBCRM")]
        public void GivenIHaveLoggedIntoBBCRM()
        {
            BBCRMHomePage.Login();
        }
 
        [Given(@"a constituent exists with last name ""(.*)""")]
        public void GivenAConstituentExistsWithLastName(string p0)
        {
            ScenarioContext.Current.Pending();
        }
 
        [When(@"I add an address to the current constituent")]
        public void WhenIAddAnAddressToTheCurrentConstituent(Table table)
        {
            ScenarioContext.Current.Pending();
        }
 
        [Then(@"an address exists")]
        public void ThenAnAddressExists(Table table)
        {
            ScenarioContext.Current.Pending();
        }
    }
}

Here is an implementation of the step methods.

Implemented steps

C#
[Given(@"I have logged into BBCRM")]
public void GivenIHaveLoggedIntoBBCRM()
{
    BBCRMHomePage.Login();
}
 
[Given(@"a constituent exists with last name ""(.*)""")]
public void GivenAConstituentExistsWithLastName(string constituent)
{
    constituent += uniqueStamp;
    BBCRMHomePage.OpenConstituentsFA();
    ConstituentsFunctionalArea.AddAnIndividual();
    IndividualDialog.SetLastName(constituent);
    IndividualDialog.Save();
}
 
[When(@"I add an address to the current constituent")]
public void WhenIAddAnAddressToTheCurrentConstituent(Table addressFields)
{
    ConstituentPanel.SelectTab("Contact");
    ConstituentPanel.ClickSectionAddButton("Addresses");
    AddressDialog.SetAddressFields(addressFields);
    Dialog.Save();
}
 
[Then(@"an address exists")]
public void ThenAnAddressExists(Table addressFields)
{
    IDictionary<string, string> addressRow = new Dictionary<string, string>();
    foreach (TableRow row in addressFields.Rows)
    {
        addressRow.Add(row["Field"], row["Value"]);
    }
    ConstituentPanel.SelectTab("Contact");
    if (!ConstituentPanel.SectionDatalistRowExists(addressRow, "Addresses"))
        FailTest(String.Format("Address '{0}' not found.", addressRow.Values));
}

AddressDialog is not a class in the UAT Automation Kit, so your build should fail at this point. We need to create the AddressDialog class and implement a SetAddressFields() method.

AddressDialog Class with empty method

C#
using System;
using System.Collections.Generic;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Crm;
using TechTalk.SpecFlow;
&#32;
namespace Delving_Deeper
{
    public class AddressDialog : Dialog
    {
        public static void SetAddressFields(Table addressFields)
        {
            throw new NotImplementedException();
        }
    }
}

We ensure that we are on the Address tab, and then we parse every row in the table.

For each TableRow in Table

C#
namespace Delving_Deeper
{
    public class AddressDialog : Dialog
    {
        public static void SetAddressFields(Table addressFields)
        {
            OpenTab("Address");
            foreach (TableRow row in addressFields.Rows)
            {
&#32;
            }
        }
    }
}

Each iteration through the loop gives us a new row from the table. We need to use the TableRow object to find a field with an XPath selector and set the field's value. The specific field represented as the TableRow object determines how to construct the XPath, what variables to pass to the XPath constructor, and what type of field setter to use. This logic must be defined for each possible value of row["Field"].

To handle this, we create a switch on the caption value. The caption dictates the type of field to set and how to set its value.

Implemented AddressDialog

C#
namespace Delving_Deeper
{
    public class AddressDialog : Dialog
    {
        public static void SetAddressFields(Table addressFields)
        {
            OpenTab("Address");
            foreach (TableRow row in addressFields.Rows)
            {
                string caption = row["Field"];
                string value = row["Value"];
                switch (caption)
                {
                    case "Type":
                        SetDropDown(getXInput("AddressAddForm2", "_ADDRESSTYPECODEID_value"), value);
                        break;
                    case "Country":
                        SetDropDown(getXInput("AddressAddForm2", "_COUNTRYID_value"), value);
                        break;
                    case "Address":
                        SetTextField(getXTextArea("AddressAddForm2", "_ADDRESSBLOCK_value"), value);
                        break;
                    case "City":
                        SetTextField(getXInput("AddressAddForm2", "_CITY_value"), value);
                        break;
                    case "State":
                        SetDropDown(getXInput("AddressAddForm2", "_STATEID_value"), value);
                        break;
                    case "ZIP":
                        SetTextField(getXInput("AddressAddForm2", "_POSTCODE_value"), value);
                        break;
                    case "Do not send mail to this address":
                        SetCheckbox(getXInput("AddressAddForm2", "_DONOTMAIL_value"), value);
                        break;
                    default:
                        throw new NotImplementedException(String.Format("Field '{0}' is not implemented.", caption));
                }
            }
        }
    }
}

This approach handles the desired logic and UI interactions, but the code itself is bulky and unpleasant. The next section demonstrates how to manipulate the format of your table to get cleaner, more adaptable code.

The table headers of "Field" and "Value" from the SpecFlow feature file example in the previous section are not required to pass variables to .NET step methods. To take advantage of more functionality in the UAT Automation Kit, we can change the table format and how we pass variables to a step method.

Modified test to add address to constituent

C#
@DelvingDeeper
Scenario: Add an address to a constituent
  Given I have logged into BBCRM
  And a constituent exists with last name "Enterprise"
  When I add an address to the current constituent
  | Type     | Country       | Address               | City       | State | ZIP   | Do not send mail to this address |
  | Business | United States | 2000 Daniel Island Dr | Charleston | SC    | 29492 | checked                          |
  Then an address exists
  | Contact information   | Type     |
  | 2000 Daniel Island Dr | Business |

After we change the table headers from "Field" and "Value" to the field captions in the dialog, we must then change how the code handles the Table object.

Edited step definitions

C#
[When(@"I add an address to the current constituent")]
public void WhenIAddAnAddressToTheCurrentConstituent(Table addressTable)
{
    foreach (TableRow row in addressTable.Rows)
    {
        ConstituentPanel.SelectTab("Contact");
        ConstituentPanel.ClickSectionAddButton("Addresses");
        AddressDialog.SetAddressFields(row);
        Dialog.Save();
    }
}

We only want to pass an object with the relevant address dialog values to the SetAddressFields() method. In the previous method, the entire Table object contained these values. In this situation, only a TableRow is necessary to gather the necessary values.

Let's implement the method to handle a single TableRow.

Edited AddressDialog class

C#
using System;
using System.Collections.Generic;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Crm;
using TechTalk.SpecFlow;
&#32;
namespace Delving_Deeper
{
    public class AddressDialog : Dialog
    {
        protected static readonly IDictionary<string, CrmField> SupportedFields = new Dictionary<string, CrmField>
        {
            {"Type", new CrmField("_ADDRESSTYPECODEID_value", FieldType.Dropdown)},
            {"Country", new CrmField("_COUNTRYID_value", FieldType.Dropdown)},
            {"Address", new CrmField("_ADDRESSBLOCK_value", FieldType.TexArea)},
            {"City", new CrmField("_CITY_value", FieldType.TextInput)},
            {"State", new CrmField("_STATEID_value", FieldType.Dropdown)},
            {"ZIP", new CrmField("_POSTCODE_value", FieldType.TextInput)},
            {"Do not send mail to this address", new CrmField("_DONOTMAIL_value", FieldType.Checkbox)}
        };
&#32;
        public static void SetAddressFields(TableRow addressFields)
        {
            OpenTab("Address");
            SetFields("AddressAddForm2", addressFields, SupportedFields);
        }
    }
}

With a TableRow whose Keys represent the dialog's field captions, we can now utilize the API's Dialog.SetFields() method. Instead of creating a switch on the field caption value, we can create a dictionary to map the supported field captions to the relevant variables that are needed to set the field's value. These variables are encapsulated in the CrmField class.

To add support for a new field, we define the logic in a single line for the SupportedFields dictionary instead of a switch-case handler.

Let's examine the Then step again. Since we changed the table format, we no longer need to convert the table to a dictionary. Instead we can directly pass the TableRows of the Table to Panel.SectionDatalistRowExists().

Edited 'Then' Step

C#
[Then(@"an address exists")]
public void ThenAnAddressExists(Table addressTable)
{
    ConstituentPanel.SelectTab("Contact");
    foreach (TableRow row in addressTable.Rows)
    {
        if (!ConstituentPanel.SectionDatalistRowExists(row, "Addresses"))
            FailTest(String.Format("Address '{0}' not found.", row.Values));
    }
}

With this format, we can also add and validate multiple addresses simply by adding rows to the table. No additional code is required.

Modified test to contain multiple rows

C#
@DelvingDeeper
Scenario: Add an address to a constituent
  Given I have logged into BBCRM
  And a constituent exists with last name "Enterprise"
  When I add an address to the current constituent
  | Type     | Country       | Address               | City       | State | ZIP   | Do not send mail to this address |
  | Business | United States | 2000 Daniel Island Dr | Charleston | SC    | 29492 | checked                          |
  |          | United States | 1990 Daniel Island Dr | Charleston | SC    | 29492 | checked                          |
  Then an address exists
  | Contact information   | Type     |
  | 2000 Daniel Island Dr | Business |
  | 1990 Daniel Island Dr |          |

The foreach loop in the step methods breaks down the Table to TableRows, which allows us to reliably add and validate each address.

Let's create a test that edits an existing address.

Note: We leave the previous Gherkin feature and step methods in place.

Test case to add and edit an address

C#
@DelvingDeeper
Scenario: Edit an address on a constituent
  Given I have logged into BBCRM
  And a constituent exists with last name "Enterprise"
  And I add an address to the current constituent
  | Address               | City       | State | ZIP   |
  | 2000 Daniel Island Dr | Charleston | SC    | 29492 |
  When I select a row under Addresses
  | Contact information   |
  | 2000 Daniel Island Dr |
  And I edit the address to individual constituent
  | Address            | Type     | ZIP   |
    | 100 Aquarium Wharf | Business | 29401 |
  Then an address exists
  | Contact information  | Type     |
  | 100 Aquarium Wharf   | Business |

Here are implementations for the new step methods. Because of our table format, we can use TableRows to find and select our desired address row before clicking Edit.

New step methods to edit an address

C#
[Given(@"I add an address to the current constituent")]
public void GivenIAddAnAddressToTheCurrentConstituent(Table addressTable)
{
    WhenIAddAnAddressToTheCurrentConstituent(addressTable);
}
&#32;
[When(@"I select a row under Addresses")]
public void WhenISelectARowUnderAddresses(Table table)
{
    foreach (var row in table.Rows)
    {
        Panel.SelectTab("Contact");
        Panel.SelectSectionDatalistRow(row, "Addresses");
    }
}
[When(@"I edit the address to individual constituent")]
public void WhenIEditTheAddressToIndividualConstituent(Table, table)
{
    foreach (var EditAddress in table.Rows)
    {
        BaseComponent.WaitClick(Panel.getXSelectedDatalistRowButton("Edit"));
        AddressDialog.SetAddressFields(EditAddress);
        Dialog.Save();
    }
}

The above code compiles but fails against the application. The implementation of SetAddressFields(TableRow addressFields) statically enters "AddressAddForm2" as the dialog's unique if for the XPath constructors

Static dialog ID

C#
public static void SetAddressFields(TableRow addressFields)
{
    OpenTab("Address");
    SetFields("AddressAddForm2", addressFields, SupportedFields);
}

Instead of creating a separate method or class, we can create a list of supported dialog IDs.

AddressDialog with supported dialog IDs

C#
public class AddressDialog : Dialog
{
    protected static readonly ICollection<string> DialogIDs = new List<string>
    {
        "AddressAddForm2",
        "AddressEditForm"
    }
&#32;
    protected static readonly IDictionary<string, CrmField> SupportedFields = new Dictionary<string, CrmField>
    {
        {"Type", new CrmField("_ADDRESSTYPECODEID_value", FieldType.Dropdown)},
        {"Country", new CrmField("_COUNTRYID_value", FieldType.Dropdown)},
        {"Address", new CrmField("_ADDRESSBLOCK_value", FieldType.TextArea)},
        {"City", new CrmField("_CITY_value", FieldType.TextInput)},
        {"State", new CrmField("_STATEID_value", FieldType.Dropdown)},
        {"ZIP", new CrmField("_POSTCODE_value", FieldType.TextInput)},
        {"Do not send mail to this address", new CrmField("_DONOTMAIL_value", FieldType.Checkbox)}
    };
&#32;
    public static void SetAddressFields(TableRow addressFields)
    {
        OpenTab("Address");
        SetFields(GetDialogId(DialogIDs), addressFields, SupportedFields);
    }
}

See Also

SpecFlow Tables and Table Rows