Reputation: 168
I have something like the following complex data structure that I need to load into my SpecFlow tests:
public Album {
public string Title;
public float RunTime;
public List<Track> Tracks;
}
public Track {
public string Title;
public float RunTime;
}
Currently I'm using a single table to add all this data, with one row per track:
AlbumTitle | 10.00 | Track1Title | 2.00
AlbumTitle | 10.00 | Track2Title | 2.00
AlbumTitle | 10.00 | Track3Title | 3.00
AlbumTitle | 10.00 | Track4Title | 3.00
For my actual dataset, this results in an overly long table (t least 9 columns) that's difficult to read, not to mention the repetition problem for the parent class data.
Is there a way to pass two tables into a SpecFlow step, or is there a better way to pass in data structues like this?
A more accurate (and therefore complicated) toy model for my problem is
public Policy {
public string Number;
public string CustomerId;
public string Type;
public DateTime Start;
public DateTime End;
public List<InsuredObject> InsuredObjects;
}
public InsuredObject {
public string Type;
public List<Cover> Covers;
}
public Covers {
public string Type;
public string Deductible;
public DateTime ValidFrom;
public DateTime ValidTo;
}
which we populate with
| Policy.Number | Policy.Type | Policy.CustomerNumber | InsuredObject.Type | Cover.Type | Cover.Deductible | Policy.ValidFrom | Cover.ValidTo | Cover.ValidFrom | Cover.ValidTo |
| 1345678 | Home | 87654321 | House | Theft | 130 | 2022-01-01 | 2022-12-31 | 2022-01-01 | 2022-06-01 |
| 1345678 | Home | 87654321 | House | Fire | 130 | 2022-01-01 | 2022-12-31 | 2022-01-01 | 2022-12-31 |
generating a single Policy containing a single InsuredObject with two Cover objects
Upvotes: 1
Views: 1204
Reputation: 18868
Essentially you are providing one giant data table for many entities. Scenarios would look something like:
Scenario: ...
Given some really big data table:
| Policy.Number | Policy.Type | Policy.CustomerNumber | InsuredObject.Type | Cover.Type | Cover.Deductible | Policy.ValidFrom | Cover.ValidTo | Cover.ValidFrom | Cover.ValidTo |
| 1345678 | Home | 87654321 | House | Theft | 130 | 2022-01-01 | 2022-12-31 | 2022-01-01 | 2022-06-01 |
| 1345678 | Home | 87654321 | House | Fire | 130 | 2022-01-01 | 2022-12-31 | 2022-01-01 | 2022-12-31 |
When ...
Then ...
While this results in fewer lines of gherkin to read, it is not readable, because the data table is so wide. Instead, you want to decompose this data table into one step per entity.
Before I begin, I am making the assumption that the policy number and customer number is assigned by the software system. If these numbers do not have relevant domain concepts associated with them, then there is no need to mention them in the scenario. Step definitions should handle connecting these entities together behind the scenes.
You actually have 4 entities (one of which you did not mention in your question, but certainly exists as a domain concept for the business):
Consider writing one step for each entity. This allows for a clear and concise setup for the data in your scenario:
Scenario: ...
Given "Helen" is a customer
And "Helen" has a "Home" policy valid from "2022-01-01" to "2022-12-31"
And the "Home" policy for "Helen" insures a "House"
And the "Home" policy for "Helen" covers her "House" for:
| Coverage Type | Deductible | Valid From | Valid To |
| Theft | 130 | 2022-01-01 | 2022-06-01 |
| Fire | 130 | 2022-01-01 | 2022-12-31 |
When ...
Then ...
As I mentioned earlier, there is no mention of a policy number or customer number. Instead, provide names for things. Here, Given "Helen" is a customer
will create a new customer. This step definition should then track the customer number created for "Helen" so that other steps can dynamically access Helen's customer number by her name (see Context Injection).
Likewise, the step Given "Helen" has a "Home" policy...
will get the customer number based on the person's name, and create an insurance policy. Assuming the system generates the policy number, this step should associate this dynamic policy number with "Helen" and the "Home" policy.
Finally, the only sensible use for a data table is defining the kinds of coverage for Helen's home insurance. This is legitimately a list of things. The step Given the "Home" policy for "Helen" covers her "House" for:
works well with a data table, because each row has a small enough number of columns that the step remains easy to read and understand.
This requires some setup though. Specifically, you will want to understand context injection.
Create your test context class, which is a holder for test data:
public class InsuranceTestContext
{
private readonly Dictionary<string, Customer> customers;
private readonly Dictionary<string, List<InsurancePolicy>> policies;
public InsuranceTestContext()
{
customers = new Dictionary<string, Customer>();
policies = new Dictionary<string, List<InsurancePolicy>>();
}
public void AddCustomer(string customerName, Customer customer)
{
customers[customerName] = customer;
}
public void AddPolicy(string customerNumber, InsurancePolicy policy)
{
if (!policies.ContainsKey(customerNumber))
{
policies[customerNumber] = new List<InsurancePolicy>();
}
policies[customerNumber].Add(policy);
}
public Customer GetCustomer(string customerName)
{
return customers[customerName];
}
public InsurancePolicy GetPolicy(string customerNumber, string policyType)
{
return policies[customerNumber].Single(p => p.PolicyType == policyType);
}
}
Register a new instance of InsuranceTestContext
with the SpecFlow dependency injection framework. I usually do this in a dedicated "hooks" class:
[Binding]
public class Hooks
{
private readonly IObjectContainer container;
public Hooks(IObjectContainer container)
{
this.container = container;
}
[BeforeScenario]
public void CreateTestContext()
{
var testContext = new InsuranceTestContext();
container.RegisterInstanceAs(testContext);
}
}
Create a class to represent each row in the data table defined in the 4th step (you will need this in a step definition):
/// <summary>
/// Represents a row in a SpecFlow datatable describing insurace coverage
/// </summary>
public class CoverageTypeDataRow
{
public string CoverageType { get; set; }
public decimal Deductible { get; set; }
public DateTime ValidFrom { get; set; }
public DateTime ValidTo { get; set; }
}
Glue everything together in your step definition class:
[Binding]
public class CustomerSteps
{
private readoly InsuranceTestContext testContext;
public CustomerSteps(InsuranceTestContext testContext)
{
this.testContext = testContext;
}
[Given(@"""(.*)"" is a customer")]
public void GivenIsACustomer(string customerName)
{
var customer = // create customer however you normally create it
testContext.AddCustomer(customerName, customer);
}
[Given(@"""(.*)"" has a ""(.*)"" policy valid from ""(.*)"" to ""(.*)""")]
public void HasAPolicyValidFromTo(string customerName, string policyType, DateTime validFrom, DateTime validTo)
{
var customer = testContext.GetCustomer(customerName);
var policy = // create policy however you normally create it (using customerNumber, policyType, validFrom and validTo)
testContext.AddPolicy(customerNumber, policy);
}
[Given(@"the ""(.*)"" policy for ""(.*)"" insures an? ""(.*)""")]
public void ThePolicyForInsuresA(string policyType, string customerName, string insuredObjectType)
{
var customer = testContext.GetCustomer(customerName);
var policy = testContext.GetPolicy(customerNumber, policyType);
var insuredObject = // create this based on customer, policy, insuredObjectType
// This will depend on your entity API
policy.InsuredObjects.Add(insuredObject);
}
[Given(@"the ""(.*)"" policy for ""(.*)"" covers ([^ ]+) ""(.*)"" for:")]
public void ThePolicyForCoversFor(string policyType, string customerName, string pronoun, string insuredObjectType, Table table)
{
var customer = testContext.GetCustomer(customerName);
var policy = testContext.GetPolicy(customer.CustomerNumber, policyType);
var insuredObject = policy.InsuredObjects.Single(i => i.Type == insuredObjectType);
var coverageTypesToAdd = table.CreateSet<CoverageTypeDataRow>();
foreach (var coverage in coverageTypesToAdd)
{
// This will depend on your entity API
insuredObject.AddCoverage(/* add using coverage.CoverageType, coverage.Deductible, coverage.ValidFrom and coverage.ValidTo */);
}
}
}
Important: I left out some technical details, like data access. This gives you the basic scaffolding required, but your specific entity API, including other calls to repositories or web services, has been left out. This can be pretty involved, and is entirely dependent on your application.
Upvotes: 4