Click or drag to resize
SpecFlow's Table and TableRow Guidelines

In this walkthrough you will get experience with handling SpecFlow's Table and TableRow objects with the UAT SDK (Project Blue) API.

Prerequisites
  • You have completed the "Using the Selenium WebDriver" walkthrough and have access to an Enterprise CRM application.

  • You are comfortable adding tests and step implementations to existing feature and step files.

  • You are comfortable accessing the existing UAT SDK (Project Blue) Core API.

  • You are comfortable modifying the app.config to change which application the tests run against.

  • You are comfortable identifying the unique attribute values for the XPath constructors in the Core API and have completed the "XPath Guidelines" walkthrough.

From Feature File to Step File - The Old Approach To Tables
SpecFlow feature files support Tables for passing in variables to the .NET step methods. Here is a test example for adding an address to a constituent
Old Table Feature
At some point the test attempts to set the fields on the 'Add an address' dialog.
Add Address Dialog

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

Step method with a Table parameter.
using System;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Base;
using TechTalk.SpecFlow;
using System.Collections.Generic;

namespace Delving_Deeper
{
    [Binding]
    public class SampleTestsSteps : BaseSteps
    {
        [Given(@"I have logged into BBCRM")]
        public void GivenIHaveLoggedIntoBBCRM()
        {
            BBCRMHomePage.Logon();
        }

        [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
[Given(@"I have logged into BBCRM")]
public void GivenIHaveLoggedIntoBBCRM()
{
    BBCRMHomePage.Logon();
}

[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 SDK (Project Blue). At this point your build should be failing. Let's create an AddressDialog class and implement the SetAddressFields() method.

AddressDialog Class with empty method.
using System;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Base;
using TechTalk.SpecFlow;
using System.Collections.Generic;

namespace Delving_Deeper
{
    public class AddressDialog : Dialog
    {
        public static void SetAddressFields(Table addressFields)
        {
            throw new NotImplementedException();
        }
    }
}

First we ensure that we are on the 'Address' tab. Then we parse through every row in the Table.

For each TableRow in Table
using System;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Base;
using TechTalk.SpecFlow;
using System.Collections.Generic;

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

            }
        }
    }
}

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. How we construct the XPath, what variables we pass to the XPath constructor, and what type of field setter we use are all determined by the specific field represented as the TableRow object. 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 what type of field we want to set and how to set its value.

Implemented AddressDialog
using System;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Base;
using TechTalk.SpecFlow;
using System.Collections.Generic;

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));
                }
            }
        }
    }
}
Note Note

If you do not understand where the variables for the XPath constructors come from, please review the XPath Guidelines walkthrough.

This approach will handle the desired logic and UI interactions, but the code itself is bulky and unpleasant. The next section shows how manipulating the format of your table can lead to cleaner, more adaptable code.

Table Guidelines
"Table headers are no longer required to be 'Field' and 'Value'"

By changing the format of our feature file tables and how we pass variables to a step method, we can take advantage of more functionality in the UAT SDK.

Here is the same test from the previous section with a different format for the Tables.
New Table Feature

Changing the table's headers from "Field" and "Value" to the dialog's field captions forces a change to the code and how it handles the Table object.

Edited step definitions.
[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();
    }
}

Instead of passing the whole table to the SetMethod, we loop through the rows in the Table and pass in a single TableRow.

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

Let's implement the method for handling a single TableRow.

Edited AddressDialog class.
using System;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Base;
using TechTalk.SpecFlow;
using System.Collections.Generic;

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)}
        };

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

Note there is also support in CrmFields for setting fields through a search dialog. Refer to the CrmField and FieldType API documentation to get a better understanding of the CrmField constructors.

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 mapping the supported field captions to the relevant variables needed to set the field's value. These variables are encapsulated in the CrmField class.

Now when we want 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. By changing the table format here, 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
[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));
    }
}

BUT WAIT THERE'S MORE!!!!!

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

Modify your test case to contain multiple rows.
New Table Multiple Rows Feature

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

Note Note

Empty table cells are treated as empty strings.

Leaving a cell as empty will result in the an attempt to set the field's value to an empty string. If you wish to skip setting the field, you must remove the key from the TableRow or set the value to null.

if (row.ContainsKey("Country") && row["Country"] == String.Empty) row["Country"] = null;

Empty table cells for a datalist select or validation are skipped and no code edits are necessary.

Supporting Multiple Dialog Ids

Continuing from the previous section, let's create a test that edits an existing address.

Test case adding and editing an address.
New Table Edit Address Feature

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 for editing and address
[Given(@"I add an address to the current constiteunt")]
public void GivenIAddAnAddressToTheCurrentConstiteunt(Table addressTable)
{
    WhenIAddAnAddressToTheCurrentConstituent(addressTable);
}

[When(@"I start to edit an address on the current constituent")]
public void WhenIStartToEditAnAddressOnTheCurrentConstituent(Table addressTable)
{
    ConstituentPanel.SelectTab("Contact");
    ConstituentPanel.SelectSectionDatalistRow(addressTable.Rows[0], "Addresses");
    ConstituentPanel.WaitClick(ConstituentPanel.getXSelectedDatalistRowButton("Edit"));
}

[When(@"set the address fields and save the dialog")]
public void WhenSetTheAddressFieldsAndSaveTheDialog(Table addressTable)
{
    AddressDialog.SetAddressFields(addressTable.Rows[0]);
    Dialog.Save();
}
Note Note

Notice that you can call step methods from within step methods as done in GivenIAddAnAddressToTheCurrentConstiteunt().

The above code will compile but fail 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
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.
public class AddressDialog : Dialog
{
    protected static readonly string[] DialogIds = { "AddressAddForm2", "AddressEditForm" };

    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)}
    };

    public static void SetAddressFields(TableRow addressFields)
    {
        OpenTab("Address");
        SetFields(GetDialogId(DialogIds), addressFields, SupportedFields);
    }

}
See Also