behaviour driven blog
This article is part of the BDDfy In Action series.
The rest of the articles in this series revolve around customizing and extending BDDfy. I thought it would be good to start with an overview of the main components of the BDDfy architecture to provide some context for that discussion and to illustrate the extensibility points.
The unit of operation in BDDfy is the Story. A Story has metadata (information about the Story) and a collection of Scenarios. Each Scenario represents a test class and contains metadata and a collection of Execution Steps, which are the methods on the test class. There are three types of architectural components in BDDfy: Scanners, Processors and Batch Processors. For each test class BDDfy composes a Story unit with various Scanners and passes it to the Processors in a processor pipeline. Once all of the test classes have been scanned and processed the Batch Processors run aggregate operations against all of the Stories.
Scanners turn a call to BDDfy (from a method) into a Scenario which could potentially be related to a Story. BDDfy doesn't need Stories but if there is one it uses it. If a Scenario is not related to a Story then it is associated with a dummy placeholder. Each Story is then passed to the Processors, which perform various operations, including executing the tests, and populate the Stories, Scenarios and Steps with the test execution results. Once all of the tests have been scanned and processed, the Batch Processors take the collection of Stories and process their results. This could be any sort of aggregate operation, but currently all the batch processors are reports.
Most of the BDDfy in Action series so far has covered the various Scanners, so I won’t go into much detail here. Suffice to say, BDDfy uses Scanners to scan each test class to find all of the methods on it and turn the test class into a Scenario. The different Scanners are shown here:
BDDfy creates a Story Scanner for each test object. This is the Scanner that actually scans the test object and turns it into a Story. It composes together the Story Metadata Scanner and the appropriate Scenario Scanner – Fluent or Reflective.
The Story Metadata Scanner gets information from the Story attribute, if one exists on the class.
A Story has the following properties:
Step Execution Results have a numerical hierarchy and can be (in ascending order):
The Test Runner Processor assigns a numerical Step Execution Result to every Execution Step. The result of a Scenario is then determined by the highest value of from its Steps and the result of a Story is determined by the highest result of its Scenarios. For example, if a Step fails, then its parent Scenario and Story will also have a result of Failed.
Scenario Scanners scan the test class and use the information they find to create a Scenario. There is a Fluent Scenario Scanner and a Reflective Scenario Scanner.
A Scenario has the following properties:
Step Scanners turn methods into Execution Steps. The Reflective Scanners (the Executable Attribute Step Scanner and the Method Name Scanner) scan the test class to find all the methods on it and turns them into Execution Steps. The Fluent Step Scanner is only a registry and in practice doesn't do any scanning.
An Execution Step has the following properties:
Once a test class has been scanned into a Story, the Story is passed into a Processor pipeline where a series of processing steps are performed on it. The Processors are categorized by Type and the order they run in is determined by this Type.
The various Process Types, in order, are:
Once all of the tests have been scanned and processed, the Batch Processors take all of the Stories and process their results (technically speaking they run in the AppDomain DomainUnload event). This could be any sort of result processing, but currently all the batch processors are reports. The built-in Batch Processors are displayed in the diagram below.
The static Configurator class allows you to configure Scanners, Processors and Batch Processors. It lets you enable, disable, or replace individual components and it also allows you to add custom implementations.