Crafty.Specflow.Extensions.FluentTableAsserter 2.1.1

dotnet add package Crafty.Specflow.Extensions.FluentTableAsserter --version 2.1.1
NuGet\Install-Package Crafty.Specflow.Extensions.FluentTableAsserter -Version 2.1.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Crafty.Specflow.Extensions.FluentTableAsserter" Version="2.1.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Crafty.Specflow.Extensions.FluentTableAsserter --version 2.1.1
#r "nuget: Crafty.Specflow.Extensions.FluentTableAsserter, 2.1.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Crafty.Specflow.Extensions.FluentTableAsserter as a Cake Addin
#addin nuget:?package=Crafty.Specflow.Extensions.FluentTableAsserter&version=2.1.1

// Install Crafty.Specflow.Extensions.FluentTableAsserter as a Cake Tool
#tool nuget:?package=Crafty.Specflow.Extensions.FluentTableAsserter&version=2.1.1

Specflow Fluent Table Asserter

Status Version Nuget

A specflow extension library to simplify table assertion with fluent code.

Installation

Nuget package:

dotnet add package Crafty.Specflow.Extensions.FluentTableAsserter

Why?

Asserting Specflow table can be very painful in large scale application. Even if SpecFlow.Assist Helpers is a good start to simplify data rehydration from table, it is not very flexible.

The idea to this library is:

  • very little code required
  • can be extended with extra configuration
  • avoid creating record or class for every single table to rehydrate
  • tend to be ubiquitous language centric (clever string parsing from human readable input)
  • make column declaration optional in gherkin, in order to declare only the columns that are relevant for a scenario

Example: collection comparison

You can compare a collection with a table. Headers represent the property names and rows represent the values of the items.

For example, you can write the gherkin assertion:

Scenario: List all registered customers
    Then the customer list is
      | Name      | Email address       | Job                   |
      | John Doe  | john.doe@gmail.com  | Scientist             |
      | Sam Smith | sam.smith@gmail.com | Chief product officer |

With the assertion code:

[Then(@"the customer list is")]
public void ThenTheCustomerListIs(Table table) => 
    _customers
        .CollectionShouldBeEquivalentToTable(table)
        .WithProperty(x => x.Name)
        .WithProperty(x => x.EmailAddress)
        .WithProperty(x => x.Job)
        .Assert();

When the collection is:

var customers = new[]
{
    new Customer("John Doe", "john.doe@gmail.com", Job.Scientist),
    new Customer("Sam Smith", "sam.smith@gmail.com", Job.ChiefProductOfficer)
};
internal record Customer(
    string Name, 
    string EmailAddress, 
    Job Job
 );

public enum Job
{
    Scientist,
    ChiefProductOfficer
}

You can find more example here.

Example: object comparison

You can also compare a single object with a Specflow Table. The first column should represent the field names, and the second column the values.

For example, you can write the gherkin assertion:

Scenario: Show email details
    Then the received email is
      | Field           | Value                             |
      | From email      | sender@company.com                |
      | To email        | receiver@company.com              |
      | Subject         | Provide schedule                  |
      | Plain text      | Hi,                               |
      | Plain text      | Can you provide me your schedule? |
      | Plain text      | Thanks.                           |
      | AttachmentCount | 3                                 |

With the assertion code:

[Then(@"the received email is")]
public void WhenAssertingTheEmailPropertiesWith(Table table) => 
    _receivedEmail
        .ObjectShouldBeEquivalentToTable(table)
        .WithProperty(x => x.FromEmail)
        .WithProperty(x => x.ToEmail)
        .WithProperty(x => x.Subject)
        .WithProperty(x => x.PlainText)
        .WithProperty(x => x.AttachmentCount)
        .Assert();

When the object is:

var email = new Email(
    "sender@company.com",
    "receiver@company.com",
    "Provide schedule",
    "Hi,\nCan you provide me your schedule?\nThanks.",
    3
);
internal record Email(
    string FromEmail,
    string ToEmail,
    string Subject,
    string PlainText,
    int AttachmentCount
);

You can find more example here.

Mapping between columns and properties

The table asserter is smart 🤓 and try to determine column name of the table, based on the mapped property names.

EmailAddress field ⇒ EmailAddress column

Or to more readable set of words:

EmailAddress field ⇒ Email address column

EmailAddress field ⇒ email address column

The same principle is applied for parsing enum values : ChiefProductOfficer value works but also Chief product officer.

It allows to have gherkin scenario closer to natural language.

Override default comparison behaviour

In certain cases, you would want to override the column that is compared to the property. A second argument of .WithProperty() allows you to provide a delegate to configure the PropertyConfiguration object.

Override column name

.WithProperty(x => x.Name, o => o.ComparedToColumn("FullName"))

It is useful when your gherkin language is different than english but your classes and records still are in english.

💡 Remember in Domain Driven Development guidelines, a strong objective is to share the same language across the team / company, from domain experts to developers. The code must be aligned to domain specific terms. So in the example, if we decided a * customer* has a full name instead of a name, it is preferable to rename the property Name into FullName instead of overriding the mapped column.

Define conversion delegate

If the cell value (a string) cannot be converted to the property type, you must define how the table asserter will converted it, by providing a conversion delegate.

.WithProperty(
    x => x.Price,
    o => o.WithCellToPropertyConversion(columnValue => ...)
)

For example, given the following Price record.

 public record Price(decimal Amount, string Symbol);

If the table looks

Scenario: Created products are listed
    When I create the product "Black jacket" for 100 USD
    Then the product list is
      | Name         | Price |
      | Black jacket | $100  |

You can define the conversion as:

.WithProperty(
    x => x.Price,
    o => o.WithCellToPropertyConversion(Price.Parse)
)

Define column transformation

When you have complex objects with wrapped objects, you may want to provide transformation logic within the property declaration.

It works fine, however, error message can be a little bit tricky to understand.

Example:

.WithProperty(
    x => x.Customers.Select(c => c.Name),
    o => o
        .ComparedToColumn("Customers")
        .SplitCellValueBySeparator()
)

Error message example:

"At index 0, 'Customers.Select(c ⇒ c.Name)' actual data is [John Doe ; Erika Doe] but should be [John Doe2 ; Erika Doe] from column 'Customers'."

To avoid this, you can add a property transformation WithPropertyTransformation:

.WithProperty(
    x => x.Customers,
    o => o
        .WithPropertyTransformation(x => x.Select(c => c.Name))
        .SplitCellValueBySeparator()
)

Better error message:

"At index 0, 'Customers' actual data is [John Doe ; Erika Doe] but should be [John Doe2 ; Erika Doe] from column ' Customers'."

Optional columns

All columns are optional by default, so you don't need to specify them all in all your scenarios. The ones you specify are used to assert your data. Depending on your scenario, you can specify only the ones that are relevant.

From the previous example, a new customer deletion scenario can be asserted only with the column Name, we can volontary remove the EmailAddress: because it is useless here, the Name is enough.

Scenario: Deleted customers are not listed anymore
    When I delete the customer "John Doe"
    Then the customer list is
      | Name      |
      | Sam Smith |

Roadmap

  • natively handle enumeration without converter
  • handle single object assertion (instead of list)
  • protect if no column match any property defined
  • add more examples
  • default converters : string ⇒ date (sql format), ...
  • reversed converter from value to column value
  • Provide default list comparison delegate converter
  • handle enum flags
  • provide examples on ColumnValueConversion
  • provide examples on chained property expression to assert sub elements
  • automatic conversion using implicit operator converter
  • configure to use regex as assertion method on string
  • better assert error when chaining method on property (Obj.MyProperty.ToString())
  • better assert error by providing the table that would match
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
2.1.1 552 1/19/2024
2.1.0 72 1/19/2024
2.0.1 480 10/10/2023
2.0.0 816 8/6/2023
1.3.2 416 6/19/2023
1.3.1 123 6/19/2023
1.3.0 130 6/19/2023
1.2.0 124 6/18/2023
1.1.0 137 6/18/2023
1.0.0 132 6/16/2023

Add WithPropertyTransformation() on both collection and object asserter.
Provide extension methods for splitting cell value with a separator.
Display the full Expression body instead of the property name on error message.