{"id":37050762,"url":"https://github.com/ewingjm/scenario-builder","last_synced_at":"2026-01-14T05:52:48.102Z","repository":{"id":226460508,"uuid":"768735678","full_name":"ewingjm/scenario-builder","owner":"ewingjm","description":"A NuGet package that provides a framework for test scenario setup using the builder pattern.","archived":false,"fork":false,"pushed_at":"2024-03-08T08:36:53.000Z","size":35,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-23T04:58:08.456Z","etag":null,"topics":["acceptance-testing","builder","builder-design-pattern","builder-pattern","integration-testing","specflow","test-automation","testing"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ewingjm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-03-07T16:29:56.000Z","updated_at":"2025-04-21T07:41:43.000Z","dependencies_parsed_at":"2024-03-07T18:25:43.517Z","dependency_job_id":"53d831ab-b125-4330-be8e-84ea8bf55f6c","html_url":"https://github.com/ewingjm/scenario-builder","commit_stats":null,"previous_names":["ewingjm/scenario-builder"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ewingjm/scenario-builder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewingjm%2Fscenario-builder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewingjm%2Fscenario-builder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewingjm%2Fscenario-builder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewingjm%2Fscenario-builder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ewingjm","download_url":"https://codeload.github.com/ewingjm/scenario-builder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ewingjm%2Fscenario-builder/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28411890,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T05:26:33.345Z","status":"ssl_error","status_checked_at":"2026-01-14T05:21:57.251Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["acceptance-testing","builder","builder-design-pattern","builder-pattern","integration-testing","specflow","test-automation","testing"],"created_at":"2026-01-14T05:52:47.081Z","updated_at":"2026-01-14T05:52:48.093Z","avatar_url":"https://github.com/ewingjm.png","language":"C#","readme":"# Scenario Builder\n\nA framework for implementing complex test scenario setup using the builder pattern.\n\n## Table of contents\n\n- [Scenario Builder](#scenario-builder)\n  - [Table of contents](#table-of-contents)\n  - [Introduction](#introduction)\n  - [Example](#example)\n    - [Incremental](#incremental)\n  - [Installation](#installation)\n  - [Usage](#usage)\n    - [Scenarios](#scenarios)\n      - [Defining scenario events](#defining-scenario-events)\n      - [Builder](#builder)\n    - [Events](#events)\n      - [Builder](#builder-1)\n    - [Composite events](#composite-events)\n      - [Defining child events](#defining-child-events)\n      - [Builder](#builder-2)\n  - [Contributing](#contributing)\n\n## Introduction\n\nThe scenario builder provides an intuitive test setup experience that can be shared across multiple automated test suites. It uses the [builder](https://refactoring.guru/design-patterns/builder) pattern to ensure that tests are only concerned with what is relevant to them.\n\nThe scenario builder can help you improve test automation productivity through greater code reuse and abstraction as well as a fluent and discoverable interface.\n\n## Example\n\nImagine a simple case management application where an end-to-end scenario looks like this:\n\n```mermaid\nflowchart LR\n    submitCase[User submits case]\n    subgraph processCase [Caseworker processes case]\n        direction LR\n        assignCase[Assigns case] --\u003e completeDocCheck[Completes document check] --\u003e sendEmail[Sends email] --\u003e closeCase[Closes case]\n    end\n    submitCase --\u003e processCase\n```\n\nThe automated tests for this application will need to setup scenarios up to any given point in that process. In addition, they will need to configure the specific parameters of each of those events. The scenario builder provides a framework for achieving both of those goals. \n\nA scenario with a completely processed case:\n\n```csharp\nvar scenario = await this.scenarioBuilder\n    .CaseworkerProcessesCase()\n    .BuildAsync();\n```\n\nA scenario with a case that has been processed as far as the documents check (which is missing documents):\n\n```csharp\nvar scenario = await this.scenarioBuilder\n    .CaseworkerProcessesCase(a =\u003e a\n        .ByCompletingDocumentsCheck(b =\u003e b\n            .WithOutcome(DocumentsCheckOutcome.MissingDocuments))\n        .AndAllPreviousSteps())\n    .BuildAsync();\n```\n\nThe test only has to specify as much detail as is relevant. Any preceding events (e.g. the case submission and assignment) will be executed implicitly as dependencies - this abstraction of the process means that a mass refactor of tests is not required if process changes are introduced. \n\nThe scenario builder returns a `scenario` object that can be to perform further actions and assertions on any data generated by the builder. It captures selected outputs from the events (such as the case ID from the case submission event).\n\n### Incremental\n\nThe `scenario` object can also be used to build incrementally via multiple calls to `BuildAsync`. This is particularly useful when using [SpecFlow](https://specflow.org/), as test setup is typically split over multiple methods. \n\nBelow is an example SpecFlow scenerio which is scenario building incrementally:\n\n```gherkin\nGiven a case has been assigned to me\nAnd I have completed the documents check with an outcome of 'missing documents'\n```\n\nBelow is the implementation of those bindings:\n\n```csharp\n[Given(\"a case has been assigned to me\")]\npublic void GivenACaseHasBeenAssignedToMe()\n{\n    var scenario = this.scenarioBuilder.\n        .CaseworkerProcessesCase(a =\u003e a\n            .ByAssigningTheCase(b =\u003e b\n                .WithAssignee(GetLoggedInUserId()))\n        .BuildAsync()\n        \n    this.ctx.Set(scenario, \"scenario\");\n}\n```\n\n```csharp\n[Given(\"I have completed the documents check\")]\npublic void GivenIHaveCompletedTheDocumentsCheck()\n{\n    var scenario = this.ctx.Get\u003cMyScenario\u003e(\"scenario\");\n\n    this.scenarioBuilder.\n        .CaseworkerProcessesCase(a =\u003e a\n            .ByCompletingDocumentsCheck()\n        .BuildAsync(scenario);\n}\n```\n\nEvents that have already executed are not executed again and events that are yet to execute will be provided with the context generated by the previous call(s) to `BuildAsync`.\n\n## Installation\n\nInstall the `ScenarioBuilder` NuGet package:\n\n```shell\ndotnet add package ScenarioBuilder\n```\n\n## Usage\n\nTo implement a scenario builder:\n\n- Create one or more concrete `Event` class and nested `Builder\u003cTEvent\u003e`\n- Create zero or more concrete `CompositeEvent` classes and nested `Builder\u003cTCompositeEvent\u003e` classes\n- Create a concrete `Scenario` class and nested `Builder\u003cTScenario\u003e`\n- Declare the event order on your scenario and composite events using the `ComposeUsing` attributes\n\n### Scenarios\n\nScenarios define the events that execute and the outputs returned to the caller. Scenarios must inherit from the `Scenario` class. \n\nThe below example shows a `CaseScenario` class used for an example case management system. A case submission event has been declared (through the `ComposeUsing` attribute), the case ID output (through the `CaseId` property), and the associated builder. Note that all scenario outputs should be nullable types as not all events will always run.\n\n```csharp\n/// \u003csummary\u003e\n/// An example scenario for a case.\n/// \u003c/summary\u003e\n[ComposeUsing(0, EventIds.CaseSubmission, typeof(UserSubmitsCaseEvent))]\npublic class CaseScenario : Scenario\n{\n    /// \u003csummary\u003e\n    /// Gets the case submission info.\n    /// \u003c/summary\u003e\n    public Guid? CaseId { get; internal set; }\n\n    /// \u003csummary\u003e\n    /// The event IDs for the events within the \u003csee cref=\"CaseScenario\"/\u003e.\n    /// \u003c/summary\u003e\n    public static class EventIds\n    {\n        /// \u003csummary\u003e\n        /// The event ID of the case submission event.\n        /// \u003c/summary\u003e\n        public const string CaseSubmission = nameof(CaseSubmission);\n    }\n}\n```\n\n#### Defining scenario events\n\nDefine the events that occur as part of a scenario by decorating the scenario class with `[ComposeUsing]` attributes. These attributes store the following about the event:\n\n- Execution order\n- ID\n- Type\n- Compile-time constructor arguments (optional)\n\nIn the example above, the `UserSubmitsCaseEvent` event is the first event that executes (as order is `0`). It has an ID assigned from the `EventIds.CaseSubmission` constant. It is recommended to assign these IDs to constants in a nested class within the scenario, as they will be referred to again when configuring the scenario builder.\n\n```csharp\n[ComposeUsing(0, EventIds.CaseSubmission, typeof(UserSubmitsCaseEvent))]\n```\n\n#### Builder\n\nScenario builders defines the interface for how scenarios are configured. \n\nThe scenario builder class is the entry-point to building scenarios. This is where the methods are defined that will allow the events (added with the `[ComposeUsing]` attributes) be configured. Implement a scenario builder by creating a nested class within the event which extends the `Builder\u003cTScenario\u003e` class:\n\n```csharp\n    /// \u003csummary\u003e\n    /// A builder for the \u003csee cref=\"CaseScenario\"/\u003e.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"clientFactory\"\u003eA client factory.\u003c/param\u003e\n    public class Builder(IServiceClientFactory clientFactory) : Builder\u003cCaseScenario\u003e\n    {\n        private readonly IServiceClientFactory clientFactory = clientFactory;\n\n        /// \u003csummary\u003e\n        /// Configures the portal user submitting a case event.\n        /// \u003c/summary\u003e\n        /// \u003cparam name=\"configurator\"\u003eThe configurator.\u003c/param\u003e\n        /// \u003creturns\u003eThe builder.\u003c/returns\u003e\n        public Builder UserSubmitsCase(Action\u003cUserSubmitsCaseEvent.Builder\u003e? configurator = null)\n        {\n            this.ConfigureEvent\u003cUserSubmitsCaseEvent, UserSubmitsCaseEvent.Builder\u003e(EventIds.CaseSubmission, configurator);\n\n            return this;\n        }\n\n        /// \u003cinheritdoc/\u003e\n        protected override IServiceCollection InitializeServices(ServiceCollection serviceCollection)\n        {\n            return serviceCollection.AddSingleton(this.clientFactory);\n        }\n    }\n```\n\nEach event (e.g. `UserSubmitsCaseEvent`) needs a corresponding builder method (e.g. `UserSubmitsCase`). These methods are boilerplate and will always resemble the above - a call to `this.ConfigureEvent` and `return this;`. The only thing that will change between these methods is the name of the method, the event ID, and the types of the event and event builder classes. Note that the `configurator` parameter should always be optional and defaulted to `null`.\n\nThe builder is where any services that events are dependent on can be registered by overriding the `InitializeServices` method. These services will be injected into events when they are added to the event constructor parameters. Below shows the `IServiceClientFactory` service that was added to the builder constructor parameters being registered.\n\n```csharp\nprotected override IServiceCollection InitializeServices(ServiceCollection serviceCollection)\n{\n    return serviceCollection\n        .AddSingleton(this.clientFactory);\n}\n```\n\n### Events\n\nEvents are the implementation of an actor performing an action. The naming convention is typically `SubjectVerbObjectEvent` (e.g. `UserSubmitsCaseEvent`).\n\nImplement an event by extending the abstract `Event` and providing an implementation for the `ExecuteAsync` method. Events should read the `context` to get data captured by preceding events and they should also add anything to the context which may be required by later events. Events should always result in a valid outcome unless explicitly configured via their builder.\n\n```csharp\n/// \u003csummary\u003e\n/// An event for a user submitting an case.\n/// \u003c/summary\u003e\n/// \u003cparam name=\"eventId\"\u003eThe event ID.\u003c/param\u003e\n/// \u003cparam name=\"clientFactory\"\u003eThe client factory.\u003c/param\u003e\n/// \u003cparam name=\"logger\"\u003eThe logger\u003c/param\u003e\npublic class UserSubmitsCaseEvent(string eventId, IServiceClientFactory clientFactory, ILogger\u003cUserSubmitsCaseEvent\u003e logger)\n    : Event(eventId)\n{\n    private readonly IServiceClientFactory clientFactory = clientFactory;\n    private readonly ILogger\u003cUserSubmitsCaseEvent\u003e logger = logger;\n    private readonly CaseFaker caseFaker = new CaseFaker();\n\n    /// \u003cinheritdoc/\u003e\n    public async override Task ExecuteAsync(ScenarioContext context)\n    {\n        this.logger.LogInformation($\"Submitting a case.\");\n\n        using var client = this.clientFactory.GetServiceClient(Persona.User);\n        var caseId = await client.CreateAsync(caseFaker.Generate());\n\n        this.logger.LogInformation($\"Created case {caseId}.\");\n\n        context.Set(\"CaseId\", caseId);\n    }\n}\n```\n\nEvents will have at least one constructor parameter of `eventId`, but you can add additional parameters for any services your events are dependent on (these are then resolved from the services registered in the scenario builder). You can also make reusable events configurable at compile-time with constructor arguments supplied by the `ComposeUsing` attributes. These constructor arguments must appear at the beginning of the event constructor.\n\n#### Builder\n\nEvent builders define the interface for how events are configured. \n\nImplement an event builder by creating a nested class within the event which extends the `Builder\u003cTEvent\u003e` class. All event builders will need to define the same constructor parameters which are passed to the base constructor.\n\nIn the above example, the `CaseFaker` could be configurable. An example builder is shown below which allows the `CaseFaker` field to be overridden. To do this, a builder method is defined which assigns to a field with the same name and type of a corresponding field in the event. These fields should be private. The value will automatically be mapped. The naming convention for event builder methods is to start with `With`.\n\n```csharp\n/// \u003csummary\u003e\n/// A builder for the \u003csee cref=\"UserSubmitsCaseEvent\"/\u003e event.\n/// \u003c/summary\u003e\n/// \u003cparam name=\"eventFactory\"\u003eThe event factory.\u003c/param\u003e\n/// \u003cparam name=\"eventId\"\u003eThe event ID.\u003c/param\u003e\n/// \u003cparam name=\"constructorArgs\"\u003eThe constructor args.\u003c/param\u003e\npublic class Builder(EventFactory eventFactory, string eventId, object[]? constructorArgs = null)\n    : Builder\u003cUserSubmitsCaseEvent\u003e(eventFactory, eventId, constructorArgs)\n{\n    private CaseFaker caseFaker;\n\n    public Builder WithCase(CaseFaker caseFaker)\n    {\n        this.caseFaker = caseFaker;\n\n        return this;\n    }\n}\n```\n\nNote that the possibility that the builder fields have not been explicitly configured must always be handeld i.e. default values must be in place. This can be seen in the updated event below:\n\n```csharp\nprivate CaseFaker caseFaker;\n\n/// \u003cinheritdoc/\u003e\npublic async override Task ExecuteAsync(ScenarioContext context)\n{\n    this.logger.LogInformation($\"Submitting a case.\");\n\n    using var client = this.clientFactory.GetServiceClient(Persona.User);\n    var caseId = await client.CreateAsync((this.caseFaker ?? new CaseFaker()).Generate());\n\n    this.logger.LogInformation($\"Created case {caseId}.\");\n\n    context.Set(\"CaseId\", caseId);\n}\n```\n\n### Composite events\n\nComposite events are special kinds of events which are comprised of other events. They enable layers of abstraction as well as fine-grained control over what events execute. The naming convention is the same as standard events. It is preferable to use verbs such as `Processes` rather than `Completes` due to the configurable nature of these events.\n\nIn the example below, the caseworker processing a case is a composite event. If a fully processed case is needed then there is no need to configure the composite event via the builder:\n\n```csharp\nthis.scenarioBuilder\n    .CaseworkerProcessesCase()\n    .BuildAsync();\n```\n\nHowever, if a partially processed case (or a case processed with specific parameters) is needed then it is possible to drill-down and configure the events within the composite:\n\n```csharp\nthis.scenarioBuilder\n    .CaseworkerProcessesCase(a =\u003e a\n        .ByCompletingDocumentsCheck(b =\u003e b\n            .WithOutcome(DocumentsCheckOutcome.MissingDocuments))\n        .AndAllPreviousSteps())\n    .BuildAsync();\n```\n\nThere are two methods on the composite event builders that are implemented by default: `AndAllPreviousSteps()` and `AllAllOtherSteps()`. Unlike the builder at the scenario level (which runs all previous steps when you configure an event), the composite events will run only the configured events by default unless you use one of those two methods.\n\n#### Defining child events\n\nDefining child events on a composite event is exactly the same as defining events on a scenario. You simply use the `[ComposeUsing]` attribute on your composite event class.\n\nIn the example below, the `CaseworkerProcessesCaseEvent` composite event is composed using the `CaseworkerAssignsCaseEvent` and the `CaseworkerCompletesDocumentCheckEvent`.\n\n```csharp\n/// \u003csummary\u003e\n/// A composite event for a caseworker processing a case.\n/// \u003c/summary\u003e\n[ComposeUsing(0, EventIds.CaseAssignment, typeof(CaseworkerAssignsCaseEvent))]\n[ComposeUsing(1, EventIds.CaseDocumentCheck, typeof(CaseworkerCompletesDocumentCheckEvent))]\npublic class CaseworkerProcessesCaseEvent : CompositeEvent\n{\n    /// \u003csummary\u003e\n    /// Initializes a new instance of the \u003csee cref=\"CaseworkerProcessesCaseEvent\"/\u003e class.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"eventsFactory\"\u003eThe event factory.\u003c/param\u003e\n    /// \u003cparam name=\"eventId\"\u003eThe ID of the event.\u003c/param\u003e\n    public CaseworkerProcessesCaseEvent(EventFactory eventsFactory, string eventId)\n        : base(eventsFactory, eventId)\n    {\n    }\n\n    /// \u003csummary\u003e\n    /// The IDs for the events within the \u003csee cref=\"CaseworkerProcessesCaseEvent\"/\u003e event.\n    /// \u003c/summary\u003e\n    public static class EventIds\n    {\n        /// \u003csummary\u003e\n        /// The event ID of the case assignment event.\n        /// \u003c/summary\u003e\n        public const string CaseAssignment = nameof(CaseAssignment);\n\n        /// \u003csummary\u003e\n        /// The event ID of the case document check event.\n        /// \u003c/summary\u003e\n        public const string CaseDocumentCheck = nameof(CaseDocumentCheck);\n    }\n\n```\n\n#### Builder\n\nComposite event builders define the interface for how composite events are configured. \n\nThis is the same process as detailed in the [Defining scenario events](#defining-scenario-events) section. The convention is to start the builder method names with `By`.\n\n```csharp\n/// \u003csummary\u003e\n/// A builder for the \u003csee cref=\"CaseworkerProcessesCaseEvent\"/\u003e.\n/// \u003c/summary\u003e\npublic class Builder : Builder\u003cCaseworkerProcessesCaseEvent\u003e\n{\n    /// \u003csummary\u003e\n    /// Initializes a new instance of the \u003csee cref=\"Builder\"/\u003e class.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"eventBuilderFactory\"\u003eThe event builder factory.\u003c/param\u003e\n    /// \u003cparam name=\"eventFactory\"\u003eThe event factory.\u003c/param\u003e\n    /// \u003cparam name=\"eventId\"\u003eThe ID of the event.\u003c/param\u003e\n    public Builder(EventBuilderFactory eventBuilderFactory, EventFactory eventFactory, string eventId)\n        : base(eventBuilderFactory, eventFactory, eventId)\n    {\n    }\n\n    /// \u003csummary\u003e\n    /// Configures the assigning of the Case.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"configurator\"\u003eThe configurator.\u003c/param\u003e\n    /// \u003creturns\u003eThe builder.\u003c/returns\u003e\n    public Builder ByAssigningTheCase(Action\u003cCaseworkerAssignsCaseEvent.Builder\u003e? configurator = null)\n    {\n        this.ConfigureEvent\u003cCaseworkerAssignsCaseEvent, CaseworkerAssignsCaseEvent.Builder\u003e(EventIds.CaseAssignment, configurator);\n\n        return this;\n    }\n\n    /// \u003csummary\u003e\n    /// Configures the setting of the Case approval.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"configurator\"\u003eThe configurator.\u003c/param\u003e\n    /// \u003creturns\u003eThe builder.\u003c/returns\u003e\n    public Builder BySettingCaseApproval(Action\u003cCaseworkerCompletesDocumentCheckEvent.Builder\u003e? configurator = null)\n    {\n        this.ConfigureEvent\u003cCaseworkerCompletesDocumentCheckEvent, CaseworkerCompletesDocumentCheckEvent.Builder\u003e(EventIds.CaseDocumentCheck, configurator);\n\n        return this;\n    }\n}\n```\n\n## Contributing\n\nRefer to the [Contributing](./CONTRIBUTING.md) guide.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fewingjm%2Fscenario-builder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fewingjm%2Fscenario-builder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fewingjm%2Fscenario-builder/lists"}