In this walkthrough, you will get experience with handling SpecFlow's Table and TableRow objects with the UAT Automation Kit.
This tutorial guides you through the steps to handle SpecFlow's Table and TableRow objects with the . In this walkthrough, you will:
SpecFlow feature files allow you to use tables to pass variables to .NET step methods.
@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.
Specflow creates bindings between test cases and step methods. Field variables for the Add an address dialog are passed through the Table
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.
[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
AddressDialog
SetAddressFields()
using System;
using System.Collections.Generic;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Crm;
using TechTalk.SpecFlow;
 
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.
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
TableRow
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.
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.
@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
[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();
}
}
TableRow
We only want to pass an object with the relevant address dialog values to the SetAddressFields()
Table
TableRow
Let's implement the method to handle a single TableRow
using System;
using System.Collections.Generic;
using Blackbaud.UAT.Base;
using Blackbaud.UAT.Core.Crm;
using TechTalk.SpecFlow;
 
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);
}
}
}
CrmField
[CrmField]()
[FieldType]()
CrmField
With a TableRow
Keys
Dialog.SetFields()
CrmField
To add support for a new field, we define the logic in a single line for the SupportedFields
Let's examine the Then
TableRows
Table
Panel.SectionDatalistRowExists()
[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.
@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
Table
TableRows
TableRow
if (row.ContainsKey("Country") && string.IsNullOrEmpty(row["Country"])) row["Country"] = null;
Let's create a test that edits an existing address.
Note: We leave the previous Gherkin feature and step methods in place.
@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
[Given(@"I add an address to the current constituent")]
public void GivenIAddAnAddressToTheCurrentConstituent(Table addressTable)
{
WhenIAddAnAddressToTheCurrentConstituent(addressTable);
}
 
[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();
}
}
GivenIAddAnAddressToTheCurrentConstituent()
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
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.
public class AddressDialog : Dialog
{
protected static readonly ICollection<string> DialogIDs = new List<string>
{
"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);
}
}