When writing BDD tests (for example using
Reqnroll, the successor of SpecFlow) you don't want to "bloat" your tests with irrelevant data, such as GUIDs of IDs - or really, any data that doesn't play a direct role in the success or failure of the test case!
For example:
Given a user registers for an account using the below profile information
| Email | Firstname | Surname |
| user@domain.com | Hello | World |
When a request is made by "user@domain.com" to retreive their profile information
Then the below profile information is returned
| Email | Firstname | Surname |
| user@domain.com | Hello | World |
Under the hood, your user profile DTO might look something like this:
public class UserProfile {
public Guid Id { get; set; }
public string Email { get; set; }
public string Firstname { get; set; }
public string Surname { get; set; }
public DateTime LastUpdated { get;set; }
}
Depending on your API, you might need to "generate" some values for your request object too - I won't cover that here as there are several ways to achieve it (e.g. using AutoFixture to fill in the missing properties, custom builders, merging object instances etc.).
When it comes to assertions; whether you're using FluentAssertions, or free alternatives like DeepEqual - when comparing the "expected" and "actual" objects, you only want to include the properties for the data that was defined in the test.
Your test step definition might look something like this:
[Then(@"the below profile information is returned")]
public async Task ThenTheBelowProfileInformationIsReturned(Table table)
{
var expected = table.CreateInstance<UserProfile>();
var actual = await _lastResponse.Content.ReadFromJsonAsync<UserProfile>();
actual.WithDeepEqual(expected)
.IgnoreUnmatchedProperties()
.IgnoreProperty(p => !table.Header.Contains(p.Name))
.Assert();
}
The above will work for collections too when you're comparing the entire set. In some cases you might be checking if a collection "contains" an object that "partially matches" another, for example:
Given the system already contains several recent users
And a user registers for an account using the below profile information
| Email | Firstname | Surname |
| user@domain.com | Hello | World |
When the new users report is generated
Then the below profiles are included
| Email | Firstname | Surname |
| user@domain.com | Hello | World |
In which case you can combine partial matcher with a "contains" (or check the entire sequence if it must only contain) assertion, e.g.:
[Then(@"the below profiles are included")]
public async Task ThenTheBelowProfilesAreIncluded(Table table)
{
var expected = table.CreateSet<UserProfile>().ToList();
var actual = await _lastResponse.Content.ReadFromJsonAsync<List<UserProfile>>();
actual.ShouldContain(a => expected.Any(e => a.WithDeepEqual(e)
.IgnoreUnmatchedProperties()
.IgnoreProperty(p => !table.Header.Contains(p.Name))
.Compare()));
}
Extending this to support "inline complex properties". When you have "nested" objects in BDD you have a few options available to interrogate the "child" objects, such as splitting out multiple step definitions or somehow "stringify" the complex type into the parent row. Let's assume you opt for the latter and choose JSON as your "stringification" algorithm, you might write a test like the below:
Given the system already contains several recent users
And a user registers for an account using the below profile information
| Email | Firstname | Surname | Address |
| user@domain.com | Hello | World | { "Street": "123 Test Street", "City": "Testtown", "Postcode": "TE57 7WN" } |
When the new users report is generated
Then the below profiles are included
| Email | Firstname | Surname | Address |
| user@domain.com | Hello | World | { "Street": "123 Test Street", "City": "Testtown", "Postcode": "TE57 7WN" } |
Using the same "UserProfile" object as before, but with an added "Address" property with the below structure:
public class Address {
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
public bool IsVerified { get; set; }
}
In this example there's an extra "IsVerified" property that does not partake in the test.
In order to A) build the expectation and B) partially match the child, you can write the below:
[Then(@"the below profile information is returned")]
public async Task ThenTheBelowProfileInformationIsReturned(Table table)
{
var expected = table.CreateSet<UserProfile>().ToList();
var currentAddressProps = new HashSet<string>();
if (table.ContainsColumn("Address"))
{
for (var i = 0; i < table.Rows.Count; i++)
{
var row = table.Rows[i];
if (!string.IsNullOrWhiteSpace(row["Address"]) && row["Address"] != "<null>")
{
using var jsonDoc = JsonDocument.Parse(row["Address"]);
jsonDoc.RootElement.EnumerateObject().Select(p => p.Name.ToPascalCase()).ForEach(p => currentAddressProps.Add(p));
expected[i].Address = jsonDoc.Deserialize<Address>();
}
}
}
var actual = await _lastResponse.Content.ReadFromJsonAsync<List<UserProfile>>();
actual.ShouldContain(a => expected.Any(e => a.WithDeepEqual(e)
.IgnoreUnmatchedProperties()
.IgnoreProperty(p =>
p.DeclaringType == typeof(UserProfile) && !table.Header.Contains(p.Name) ||
p.DeclaringType == typeof(Address) && !currentAddressProps.Contains(p.Name))
.Compare()));
}