{"id":15411050,"url":"https://github.com/oskardudycz/ogooreck","last_synced_at":"2025-04-10T06:37:05.195Z","repository":{"id":37853917,"uuid":"474734281","full_name":"oskardudycz/Ogooreck","owner":"oskardudycz","description":"Sneaky Testing Library in BDD style","archived":false,"fork":false,"pushed_at":"2024-05-03T06:39:40.000Z","size":154,"stargazers_count":124,"open_issues_count":9,"forks_count":1,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-05-03T11:32:31.655Z","etag":null,"topics":["bdd","behaviour-driven-development","dotnet","testing","testing-library"],"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/oskardudycz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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,"publiccode":null,"codemeta":null},"funding":{"github":["oskardudycz"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2022-03-27T19:10:46.000Z","updated_at":"2024-05-03T06:26:42.000Z","dependencies_parsed_at":"2024-05-03T07:25:51.008Z","dependency_job_id":"11a8f5d8-bb4b-44cf-b221-0527bc52bf48","html_url":"https://github.com/oskardudycz/Ogooreck","commit_stats":{"total_commits":55,"total_committers":1,"mean_commits":55.0,"dds":0.0,"last_synced_commit":"5c512ef04d33673304fb76bba14c4d1511a75219"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oskardudycz%2FOgooreck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oskardudycz%2FOgooreck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oskardudycz%2FOgooreck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oskardudycz%2FOgooreck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oskardudycz","download_url":"https://codeload.github.com/oskardudycz/Ogooreck/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248170992,"owners_count":21059287,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["bdd","behaviour-driven-development","dotnet","testing","testing-library"],"created_at":"2024-10-01T16:47:36.754Z","updated_at":"2025-04-10T06:37:05.144Z","avatar_url":"https://github.com/oskardudycz.png","language":"C#","funding_links":["https://github.com/sponsors/oskardudycz"],"categories":[],"sub_categories":[],"readme":"[![Twitter Follow](https://img.shields.io/twitter/follow/oskar_at_net?style=social)](https://twitter.com/oskar_at_net) ![Github Actions](https://github.com/oskardudycz/Ogooreck/actions/workflows/build.dotnet.yml/badge.svg?branch=main) [![blog](https://img.shields.io/badge/blog-event--driven.io-brightgreen)](https://event-driven.io/?utm_source=event_sourcing_net) [![blog](https://img.shields.io/badge/%F0%9F%9A%80-Architecture%20Weekly-important)](https://www.architecture-weekly.com/?utm_source=event_sourcing_net)\n[![Nuget Package](https://badgen.net/nuget/v/ogooreck)](https://www.nuget.org/packages/Ogooreck/)\n[![Nuget](https://img.shields.io/nuget/dt/ogooreck)](https://www.nuget.org/packages/Ogooreck/)\n\n# 🥒 Ogooreck\n\nOgooreck is a Sneaky Test library. It helps to write readable and self-documenting tests. It's both C# and F# friendly!\n\nMain assumptions:\n- write tests seamlessly,\n- make them readable,\n- cut needed boilerplate by the set of helpful extensions and wrappers,\n- don't create a full-blown BDD framework,\n- no Domain-Specific Language,\n- don't replace testing frameworks (works with all, so XUnit, NUnit, MSTests, etc.),\n- testing frameworks and assert library agnostic,\n- keep things simple, but allow compositions and extension.\n\nCurrent available for API testing.\n\n\nCurrent available for testing:\n- [Business Logic](#business-logic-testing),\n- [API](#api-testing).\n\nCheck also my articles: \n- [Ogooreck introduction](https://event-driven.io/en/ogooreck_sneaky_bdd_testing_framework/),\n- [Testing business logic in Event Sourcing, and beyond!](https://event-driven.io/en/testing_event_sourcing/),\n- [Writing and testing business logic in F#](https://event-driven.io/en/writing_and_testing_business_logic_in_fsharp/).\n\n## Support\n\nFeel free to [create an issue](https://github.com/oskardudycz/Ogooreck/issues/new) if you have any questions or request for more explanation or samples. I also take **Pull Requests**!\n\n💖 If this tool helped you - I'd be more than happy if you **join** the group of **my official supporters** at:\n\n👉 [Github Sponsors](https://github.com/sponsors/oskardudycz)\n\n⭐ Star on GitHub or sharing with your friends will also help!\n\n## Business Logic Testing\n\nOgooreck provides a set of helpers to set up business logic tests. It's recommended to add such using to your tests:\n\n```csharp\nusing Ogooreck.BusinessLogic;\n```\n\nRead more in the [Testing business logic in Event Sourcing, and beyond!](https://event-driven.io/en/testing_event_sourcing/) article.\n\n### Decider and Command Handling tests\n\nYou can use `DeciderSpecification` to run decider and command handling tests. See the example:\n\n**C#**\n```csharp\nusing FluentAssertions;\nusing Ogooreck.BusinessLogic;\n\nnamespace Ogooreck.Sample.BusinessLogic.Tests.Deciders;\n\nusing static BankAccountEventsBuilder;\n\npublic class BankAccountTests\n{\n    private readonly Random random = new();\n    private static readonly DateTimeOffset now = DateTimeOffset.UtcNow;\n\n    private readonly DeciderSpecification\u003cBankAccount\u003e Spec = Specification.For\u003cBankAccount\u003e(\n        (command, bankAccount) =\u003e BankAccountDecider.Handle(() =\u003e now, command, bankAccount),\n        BankAccount.Evolve\n    );\n\n    [Fact]\n    public void GivenNonExistingBankAccount_WhenOpenWithValidParams_ThenSucceeds()\n    {\n        var bankAccountId = Guid.NewGuid();\n        var accountNumber = Guid.NewGuid().ToString();\n        var clientId = Guid.NewGuid();\n        var currencyISOCode = \"USD\";\n\n        Spec.Given()\n            .When(new OpenBankAccount(bankAccountId, accountNumber, clientId, currencyISOCode))\n            .Then(new BankAccountOpened(bankAccountId, accountNumber, clientId, currencyISOCode, now, 1));\n    }\n\n    [Fact]\n    public void GivenOpenBankAccount_WhenRecordDepositWithValidParams_ThenSucceeds()\n    {\n        var bankAccountId = Guid.NewGuid();\n\n        var amount = (decimal)random.NextDouble();\n        var cashierId = Guid.NewGuid();\n\n        Spec.Given(BankAccountOpened(bankAccountId, now, 1))\n            .When(new RecordDeposit(amount, cashierId))\n            .Then(new DepositRecorded(bankAccountId, amount, cashierId, now, 2));\n    }\n\n    [Fact]\n    public void GivenClosedBankAccount_WhenRecordDepositWithValidParams_ThenFailsWithInvalidOperationException()\n    {\n        var bankAccountId = Guid.NewGuid();\n\n        var amount = (decimal)random.NextDouble();\n        var cashierId = Guid.NewGuid();\n\n        Spec.Given(\n                BankAccountOpened(bankAccountId, now, 1),\n                BankAccountClosed(bankAccountId, now, 2)\n            )\n            .When(new RecordDeposit(amount, cashierId))\n            .ThenThrows\u003cInvalidOperationException\u003e(exception =\u003e exception.Message.Should().Be(\"Account is closed!\"));\n    }\n}\n\npublic static class BankAccountEventsBuilder\n{\n    public static BankAccountOpened BankAccountOpened(Guid bankAccountId, DateTimeOffset now, long version)\n    {\n        var accountNumber = Guid.NewGuid().ToString();\n        var clientId = Guid.NewGuid();\n        var currencyISOCode = \"USD\";\n\n        return new BankAccountOpened(bankAccountId, accountNumber, clientId, currencyISOCode, now, version);\n    }\n\n    public static BankAccountClosed BankAccountClosed(Guid bankAccountId, DateTimeOffset now, long version)\n    {\n        var reason = Guid.NewGuid().ToString();\n\n        return new BankAccountClosed(bankAccountId, reason, now, version);\n    }\n}\n```\n\nSee full sample in [tests](/src/Ogooreck.Sample.BusinessLogic.Tests/Deciders/BankAccountTests.cs).\n\n**F#**\n```fsharp\nmodule BankAccountTests\n\nopen System\nopen Deciders.BankAccount\nopen Deciders.BankAccountPrimitives\nopen Deciders.BankAccountDecider\nopen Ogooreck.BusinessLogic\nopen FsCheck.Xunit\n\nlet random = Random()\n\nlet spec =\n    Specification.For(decide, evolve, Initial)\n\nlet BankAccountOpenedWith bankAccountId now version =\n    let accountNumber =\n        AccountNumber.parse (Guid.NewGuid().ToString())\n\n    let clientId = ClientId.newId ()\n\n    let currencyISOCode =\n        CurrencyIsoCode.parse \"USD\"\n\n    BankAccountOpened\n        { BankAccountId = bankAccountId\n          AccountNumber = accountNumber\n          ClientId = clientId\n          CurrencyIsoCode = currencyISOCode\n          CreatedAt = now\n          Version = version }\n\nlet BankAccountClosedWith bankAccountId now version =\n    BankAccountClosed\n        { BankAccountId = bankAccountId\n          Reason = Guid.NewGuid().ToString()\n          ClosedAt = now\n          Version = version }\n\n[\u003cProperty\u003e]\nlet ``GIVEN non existing bank account WHEN open with valid params THEN bank account is opened``\n    bankAccountId\n    accountNumber\n    clientId\n    currencyISOCode\n    now\n    =\n    let notExistingAccount = Array.empty\n\n    spec\n        .Given(notExistingAccount)\n        .When(\n            OpenBankAccount\n                { BankAccountId = bankAccountId\n                  AccountNumber = accountNumber\n                  ClientId = clientId\n                  CurrencyIsoCode = currencyISOCode\n                  Now = now }\n        )\n        .Then(\n            BankAccountOpened\n                { BankAccountId = bankAccountId\n                  AccountNumber = accountNumber\n                  ClientId = clientId\n                  CurrencyIsoCode = currencyISOCode\n                  CreatedAt = now\n                  Version = 1 }\n        )\n    |\u003e ignore\n\n[\u003cProperty\u003e]\nlet ``GIVEN open bank account WHEN record deposit with valid params THEN deposit is recorded``\n    bankAccountId\n    amount\n    cashierId\n    now\n    =\n    spec\n        .Given(BankAccountOpenedWith bankAccountId now 1)\n        .When(\n            RecordDeposit\n                { Amount = amount\n                  CashierId = cashierId\n                  Now = now }\n        )\n        .Then(\n            DepositRecorded\n                { BankAccountId = bankAccountId\n                  Amount = amount\n                  CashierId = cashierId\n                  RecordedAt = now\n                  Version = 2 }\n        )\n    |\u003e ignore\n\n[\u003cProperty\u003e]\nlet ``GIVEN closed bank account WHEN record deposit with valid params THEN fails with invalid operation exception``\n    bankAccountId\n    amount\n    cashierId\n    now\n    =\n    spec\n        .Given(\n            BankAccountOpenedWith bankAccountId now 1,\n            BankAccountClosedWith bankAccountId now 2\n        )\n        .When(\n            RecordDeposit\n                { Amount = amount\n                  CashierId = cashierId\n                  Now = now }\n        )\n        .ThenThrows\u003cInvalidOperationException\u003e\n    |\u003e ignore\n```\n\nSee full sample in [tests](/src/Ogooreck.Sample.BusinessLogic.FSharp.Tests/Deciders/BankAccountTests.fs).\n\n### Event-Sourced command handlers\n\nYou can use `HandlerSpecification` to run event-sourced command handling tests for pure functions and entities. See the example:\n\n```csharp\nusing Ogooreck.BusinessLogic;\n\nnamespace Ogooreck.Sample.BusinessLogic.Tests.Functions.EventSourced;\n\nusing static IncidentEventsBuilder;\nusing static IncidentService;\n\npublic class IncidentTests\n{\n    private static readonly DateTimeOffset now = DateTimeOffset.UtcNow;\n\n    private static readonly Func\u003cIncident, object, Incident\u003e evolve =\n        (incident, @event) =\u003e\n        {\n            return @event switch\n            {\n                IncidentLogged logged =\u003e Incident.Create(logged),\n                IncidentCategorised categorised =\u003e incident.Apply(categorised),\n                IncidentPrioritised prioritised =\u003e incident.Apply(prioritised),\n                AgentRespondedToIncident agentResponded =\u003e incident.Apply(agentResponded),\n                CustomerRespondedToIncident customerResponded =\u003e incident.Apply(customerResponded),\n                IncidentResolved resolved =\u003e incident.Apply(resolved),\n                ResolutionAcknowledgedByCustomer acknowledged =\u003e incident.Apply(acknowledged),\n                IncidentClosed closed =\u003e incident.Apply(closed),\n                _ =\u003e incident\n            };\n        };\n\n    private readonly HandlerSpecification\u003cIncident\u003e Spec = Specification.For\u003cIncident\u003e(evolve);\n\n    [Fact]\n    public void GivenNonExistingIncident_WhenOpenWithValidParams_ThenSucceeds()\n    {\n        var incidentId = Guid.NewGuid();\n        var customerId = Guid.NewGuid();\n        var contact = new Contact(ContactChannel.Email, EmailAddress: \"john@doe.com\");\n        var description = Guid.NewGuid().ToString();\n        var loggedBy = Guid.NewGuid();\n\n        Spec.Given()\n            .When(() =\u003e Handle(() =\u003e now, new LogIncident(incidentId, customerId, contact, description, loggedBy)))\n            .Then(new IncidentLogged(incidentId, customerId, contact, description, loggedBy, now));\n    }\n\n    [Fact]\n    public void GivenOpenIncident_WhenCategoriseWithValidParams_ThenSucceeds()\n    {\n        var incidentId = Guid.NewGuid();\n\n        var category = IncidentCategory.Database;\n        var categorisedBy = Guid.NewGuid();\n\n        Spec.Given(IncidentLogged(incidentId, now))\n            .When(incident =\u003e Handle(() =\u003e now, incident, new CategoriseIncident(incidentId, category, categorisedBy)))\n            .Then(new IncidentCategorised(incidentId, category, categorisedBy, now));\n    }\n}\n\npublic static class IncidentEventsBuilder\n{\n    public static IncidentLogged IncidentLogged(Guid incidentId, DateTimeOffset now)\n    {\n        var customerId = Guid.NewGuid();\n        var contact = new Contact(ContactChannel.Email, EmailAddress: \"john@doe.com\");\n        var description = Guid.NewGuid().ToString();\n        var loggedBy = Guid.NewGuid();\n\n        return new IncidentLogged(incidentId, customerId, contact, description, loggedBy, now);\n    }\n}\n```\n\nSee full sample in [tests](/src/Ogooreck.Sample.BusinessLogic.Tests/Functions/EventSourced/IncidentTests.cs).\n\n### State-based command handlers\n\nYou can use `HandlerSpecification` to run state-based command handling tests for pure functions and entities. See the example:\n\n```csharp\nusing Ogooreck.BusinessLogic;\n\nnamespace Ogooreck.Sample.BusinessLogic.Tests.Functions.StateBased;\n\nusing static IncidentEventsBuilder;\nusing static IncidentService;\n\npublic class IncidentTests\n{\n    private static readonly DateTimeOffset now = DateTimeOffset.UtcNow;\n\n    private readonly HandlerSpecification\u003cIncident\u003e Spec = Specification.For\u003cIncident\u003e();\n\n    [Fact]\n    public void GivenNonExistingIncident_WhenOpenWithValidParams_ThenSucceeds()\n    {\n        var incidentId = Guid.NewGuid();\n        var customerId = Guid.NewGuid();\n        var contact = new Contact(ContactChannel.Email, EmailAddress: \"john@doe.com\");\n        var description = Guid.NewGuid().ToString();\n        var loggedBy = Guid.NewGuid();\n\n        Spec.Given()\n            .When(() =\u003e Handle(() =\u003e now, new LogIncident(incidentId, customerId, contact, description, loggedBy)))\n            .Then(new Incident(incidentId, customerId, contact, loggedBy, now, description));\n    }\n\n    [Fact]\n    public void GivenOpenIncident_WhenCategoriseWithValidParams_ThenSucceeds()\n    {\n        var incidentId = Guid.NewGuid();\n        var loggedIncident = LoggedIncident(incidentId, now);\n\n        var category = IncidentCategory.Database;\n        var categorisedBy = Guid.NewGuid();\n\n        Spec.Given(loggedIncident)\n            .When(incident =\u003e Handle(() =\u003e now, incident, new CategoriseIncident(incidentId, category, categorisedBy)))\n            .Then(loggedIncident with { Category = category });\n    }\n}\n\npublic static class IncidentEventsBuilder\n{\n    public static Incident LoggedIncident(Guid incidentId, DateTimeOffset now)\n    {\n        var customerId = Guid.NewGuid();\n        var contact = new Contact(ContactChannel.Email, EmailAddress: \"john@doe.com\");\n        var description = Guid.NewGuid().ToString();\n        var loggedBy = Guid.NewGuid();\n\n        return new Incident(incidentId, customerId, contact, loggedBy, now, description);\n    }\n}\n\n```\n\nSee full sample in [tests](/src/Ogooreck.Sample.BusinessLogic.Tests/Functions/EventSourced/IncidentTests.cs).\n\n\n### Event-Driven Aggregate tests\n\nYou can use `HandlerSpecification` to run event-driven aggregat tests. See the example:\n\n```csharp\nusing Ogooreck.BusinessLogic;\nusing Ogooreck.Sample.BusinessLogic.Tests.Aggregates.EventSourced.Core;\nusing Ogooreck.Sample.BusinessLogic.Tests.Aggregates.EventSourced.Pricing;\nusing Ogooreck.Sample.BusinessLogic.Tests.Aggregates.EventSourced.Products;\nusing Ogooreck.Sample.BusinessLogic.Tests.Functions.EventSourced;\n\nnamespace Ogooreck.Sample.BusinessLogic.Tests.Aggregates.EventSourced;\n\nusing static ShoppingCartEventsBuilder;\nusing static ProductItemBuilder;\nusing static AggregateTestExtensions\u003cShoppingCart\u003e;\n\npublic class ShoppingCartTests\n{\n    private readonly Random random = new();\n\n    private readonly HandlerSpecification\u003cShoppingCart\u003e Spec =\n        Specification.For\u003cShoppingCart\u003e(Handle, ShoppingCart.Evolve);\n\n    private class DummyProductPriceCalculator: IProductPriceCalculator\n    {\n        private readonly decimal price;\n\n        public DummyProductPriceCalculator(decimal price) =\u003e this.price = price;\n\n        public IReadOnlyList\u003cPricedProductItem\u003e Calculate(params ProductItem[] productItems) =\u003e\n            productItems.Select(pi =\u003e PricedProductItem.For(pi, price)).ToList();\n    }\n\n    [Fact]\n    public void GivenNonExistingShoppingCart_WhenOpenWithValidParams_ThenSucceeds()\n    {\n        var shoppingCartId = Guid.NewGuid();\n        var clientId = Guid.NewGuid();\n\n        Spec.Given()\n            .When(() =\u003e ShoppingCart.Open(shoppingCartId, clientId))\n            .Then(new ShoppingCartOpened(shoppingCartId, clientId));\n    }\n\n    [Fact]\n    public void GivenOpenShoppingCart_WhenAddProductWithValidParams_ThenSucceeds()\n    {\n        var shoppingCartId = Guid.NewGuid();\n\n        var productItem = ValidProductItem();\n        var price = random.Next(1, 1000);\n        var priceCalculator = new DummyProductPriceCalculator(price);\n\n        Spec.Given(ShoppingCartOpened(shoppingCartId))\n            .When(cart =\u003e cart.AddProduct(priceCalculator, productItem))\n            .Then(new ProductAdded(shoppingCartId, PricedProductItem.For(productItem, price)));\n    }\n}\n\npublic static class ShoppingCartEventsBuilder\n{\n    public static ShoppingCartOpened ShoppingCartOpened(Guid shoppingCartId)\n    {\n        var clientId = Guid.NewGuid();\n\n        return new ShoppingCartOpened(shoppingCartId, clientId);\n    }\n}\n\npublic static class ProductItemBuilder\n{\n    private static readonly Random Random = new();\n\n    public static ProductItem ValidProductItem() =\u003e\n        ProductItem.From(Guid.NewGuid(), Random.Next(1, 100));\n}\n\npublic static class AggregateTestExtensions\u003cTAggregate\u003e where TAggregate : Aggregate\n{\n    public static DecideResult\u003cobject, TAggregate\u003e Handle(Handler\u003cobject, TAggregate\u003e handle, TAggregate aggregate)\n    {\n        var result = handle(aggregate);\n        var updatedAggregate = result.NewState ?? aggregate;\n        return DecideResult.For(updatedAggregate, updatedAggregate.DequeueUncommittedEvents());\n    }\n}\n```\n\nSee full sample in [tests](/src/Ogooreck.Sample.BusinessLogic.Tests/Aggregates/EventSourced/ShoppingCartTests.cs).\n\n### State-based Aggregate tests\n\nYou can use `HandlerSpecification` to run event-driven aggregat tests. See the example:\n\n```csharp\nusing FluentAssertions;\nusing Ogooreck.BusinessLogic;\nusing Ogooreck.Sample.BusinessLogic.Tests.Aggregates.StateBased.Pricing;\nusing Ogooreck.Sample.BusinessLogic.Tests.Aggregates.StateBased.Products;\n\nnamespace Ogooreck.Sample.BusinessLogic.Tests.Aggregates.StateBased;\n\nusing static ShoppingCartEventsBuilder;\nusing static ProductItemBuilder;\n\npublic class ShoppingCartTests\n{\n    private readonly Random random = new();\n\n    private readonly HandlerSpecification\u003cShoppingCart\u003e Spec = Specification.For\u003cShoppingCart\u003e();\n\n    private class DummyProductPriceCalculator: IProductPriceCalculator\n    {\n        private readonly decimal price;\n\n        public DummyProductPriceCalculator(decimal price) =\u003e this.price = price;\n\n        public IReadOnlyList\u003cPricedProductItem\u003e Calculate(params ProductItem[] productItems) =\u003e\n            productItems.Select(pi =\u003e PricedProductItem.For(pi, price)).ToList();\n    }\n\n    [Fact]\n    public void GivenNonExistingShoppingCart_WhenOpenWithValidParams_ThenSucceeds()\n    {\n        var shoppingCartId = Guid.NewGuid();\n        var clientId = Guid.NewGuid();\n\n        Spec.Given()\n            .When(() =\u003e ShoppingCart.Open(shoppingCartId, clientId))\n            .Then((state, _) =\u003e\n            {\n                state.Id.Should().Be(shoppingCartId);\n                state.ClientId.Should().Be(clientId);\n                state.ProductItems.Should().BeEmpty();\n                state.Status.Should().Be(ShoppingCartStatus.Pending);\n                state.TotalPrice.Should().Be(0);\n            });\n    }\n\n    [Fact]\n    public void GivenOpenShoppingCart_WhenAddProductWithValidParams_ThenSucceeds()\n    {\n        var shoppingCartId = Guid.NewGuid();\n\n        var productItem = ValidProductItem();\n        var price = random.Next(1, 1000);\n        var priceCalculator = new DummyProductPriceCalculator(price);\n\n        Spec.Given(OpenedShoppingCart(shoppingCartId))\n            .When(cart =\u003e cart.AddProduct(priceCalculator, productItem))\n            .Then((state, _) =\u003e\n            {\n                state.ProductItems.Should().NotBeEmpty();\n                state.ProductItems.Single().Should().Be(PricedProductItem.For(productItem, price));\n            });\n    }\n}\n\npublic static class ShoppingCartEventsBuilder\n{\n    public static ShoppingCart OpenedShoppingCart(Guid shoppingCartId)\n    {\n        var clientId = Guid.NewGuid();\n\n        return ShoppingCart.Open(shoppingCartId, clientId);\n    }\n}\n\npublic static class ProductItemBuilder\n{\n    private static readonly Random Random = new();\n\n    public static ProductItem ValidProductItem() =\u003e\n        ProductItem.From(Guid.NewGuid(), Random.Next(1, 100));\n}\n```\n\nSee full sample in [tests](/src/Ogooreck.Sample.BusinessLogic.Tests/Aggregates/StateBased/ShoppingCartTests.cs).\n\n## API Testing\n\nOgooreck provides a set of helpers to set up HTTP requests, Response assertions. It's recommended to add such usings to your tests:\n\n```csharp\nusing Ogooreck.API;\nusing static Ogooreck.API.ApiSpecification;\n```\n\nThanks to that, you'll get cleaner access to helper methods.\n\nSee more in samples below!\n\n### POST\n\nOgooreck provides a set of helpers to construct the request (e.g. `URI`, `BODY`) and check the standardised responses.\n\n```csharp\npublic Task POST_CreatesNewMeeting() =\u003e\n    API.Given()\n        .When(\n            POST\n            URI(\"/api/meetings/),\n            BODY(new CreateMeeting(Guid.NewGuid(), \"Event Sourcing Workshop\"))\n        )\n        .Then(CREATED);\n```\n\n### PUT\n\nYou can also specify headers, e.g. `IF_MATCH` to perform an optimistic concurrency check.\n\n```csharp\npublic Task PUT_ConfirmsShoppingCart() =\u003e\n    API.Given()\n        .When(\n            PUT,\n            URI($\"/api/ShoppingCarts/{API.ShoppingCartId}/confirmation\"),\n            HEADERS(IF_MATCH(1))\n        )\n        .Then(OK);\n```\n\n### GET\n\nYou can also do response body assertions, to, e.g. out of the box check if the response body is equivalent to the expected one:\n\n```csharp\npublic Task GET_ReturnsShoppingCartDetails() =\u003e\n    API.Given()\n        .When(GET, URI($\"/api/ShoppingCarts/{API.ShoppingCartId}\"))\n        .Then(\n            OK,\n            RESPONSE_BODY(new ShoppingCartDetails\n            {\n                Id = API.ShoppingCartId,\n                Status = ShoppingCartStatus.Confirmed,\n                ProductItems = new List\u003cPricedProductItem\u003e(),\n                ClientId = API.ClientId,\n                Version = 2,\n            }));\n```\n\nYou can also use `GET_UNTIL` helper to check API that has eventual consistency.\n\nYou can use various conditions, e.g. `RESPONSE_SUCCEEDED` waits until a response has one of the 2xx statuses. That's useful for new resource creation scenarios.\n\n```csharp\npublic Task GET_ReturnsShoppingCartDetails() =\u003e\n    API.Given()\n        .When(GET, URI($\"/api/ShoppingCarts/{API.ShoppingCartId}\"))\n        .Until(RESPONSE_SUCCEEDED)\n        .Then(\n            OK,\n            RESPONSE_BODY(new ShoppingCartDetails\n            {\n                Id = API.ShoppingCartId,\n                Status = ShoppingCartStatus.Confirmed,\n                ProductItems = new List\u003cPricedProductItem\u003e(),\n                ClientId = API.ClientId,\n                Version = 2,\n            }));\n```\n\nYou can also use `RESPONSE_ETAG_IS` helper to check if ETag matches your expected version. That's useful for state change verification.\n\n```csharp\npublic Task GET_ReturnsShoppingCartDetails() =\u003e\n    API.Given()\n        .When(GET, URI($\"/api/ShoppingCarts/{API.ShoppingCartId}\"))\n        .Until(RESPONSE_ETAG_IS(2))\n        .Then(\n            OK,\n            RESPONSE_BODY(new ShoppingCartDetails\n            {\n                Id = API.ShoppingCartId,\n                Status = ShoppingCartStatus.Confirmed,\n                ProductItems = new List\u003cPricedProductItem\u003e(),\n                ClientId = API.ClientId,\n                Version = 2,\n            }));\n```\n\nYou can also do more advanced filtering via `RESPONSE_BODY_MATCHES`. That's useful for testing filtering scenarios with eventual consistency (e.g. having `Elasticsearch` as storage).\n\nYou can also do custom checks on the body, providing expression.\n\n```csharp\npublic Task GET_ReturnsShoppingCartDetails() =\u003e\n    API.Given()\n        .When(\n            GET,\n            URI($\"{MeetingsSearchApi.MeetingsUrl}?filter={MeetingName}\")\n        )\n        .UNTIL(\n            RESPONSE_BODY_MATCHES\u003cIReadOnlyCollection\u003cMeeting\u003e\u003e(\n                meetings =\u003e meetings.Any(m =\u003e m.Id == MeetingId))\n        )\n        .Then(\n            RESPONSE_BODY\u003cIReadOnlyCollection\u003cMeeting\u003e\u003e(meetings =\u003e\n                meetings.Should().Contain(meeting =\u003e\n                    meeting.Id == MeetingId\n                    \u0026\u0026 meeting.Name == MeetingName\n                )\n            ));\n```\n\n### DELETE\n\nOf course, the delete keyword is also supported.\n\n```csharp\npublic Task DELETE_ShouldRemoveProductFromShoppingCart() =\u003e\n    API.Given()\n        .When(\n            DELETE, \n            URI($\"/api/ShoppingCarts/{API.ShoppingCartId}/products/{API.ProductItem.ProductId}?quantity={RemovedCount}\u0026unitPrice={API.UnitPrice}\"),\n            HEADERS(IF_MATCH(1))\n        )\n        .Then(NO_CONTENT);\n```\n\n### Using data from results of the previous tests\n\nFor instance created id to shape proper URI.\n\n```csharp\npublic class CancelShoppingCartTests: IClassFixture\u003cApiSpecification\u003cProgram\u003e\u003e\n{\n    private readonly ApiSpecification\u003cProgram\u003e API;\n    public CancelShoppingCartTests(ApiSpecification\u003cProgram\u003e api) =\u003e API = api;\n\n    public readonly Guid ClientId = Guid.NewGuid();\n\n    [Fact]\n    [Trait(\"Category\", \"Acceptance\")]\n    public Task Delete_Should_Return_OK_And_Cancel_Shopping_Cart() =\u003e\n        API\n            .Given(\n                \"Opened ShoppingCart\",\n                POST,\n                URI(\"/api/ShoppingCarts\"),\n                BODY(new OpenShoppingCartRequest(clientId: Guid.NewGuid()))\n            )\n            .When(\n                \"Cancel Shopping Cart\",\n                DELETE,\n                URI(ctx =\u003e $\"/api/ShoppingCarts/{ctx.GetCreatedId()}\"),\n                HEADERS(IF_MATCH(0))\n            )\n            .Then(OK);\n}\n```\n\n### Scenarios and advanced composition\n\nOgooreck supports various ways of composing the API, e.g.\n\n**Classic Async/Await**\n\n```csharp\npublic async Task POST_WithExistingSKU_ReturnsConflictStatus() =\u003e\n{\n    // Given\n    var request = new RegisterProductRequest(\"AA2039485\", ValidName, ValidDescription);\n\n    // first one should succeed\n    await API.Given()\n        .When(\n            POST,\n            URI(\"/api/products/\"),\n            BODY(request)\n        )\n        .Then(CREATED);\n\n    // second one will fail with conflict\n    await API.Given()\n        .When(\n            POST,\n            URI(\"/api/products/\"),\n            BODY(request)\n        )\n        .Then(CONFLICT);\n}\n```\n\n**Joining with `And`**\n\n```csharp\npublic async Task POST_WithExistingSKU_ReturnsConflictStatus() =\u003e\n{\n    // Given\n    var request = new RegisterProductRequest(\"AA2039485\", ValidName, ValidDescription);\n\n    // first one should succeed\n    await API.Given()\n        .When(\n            POST,\n            URI(\"/api/products/\"),\n            BODY(request)\n        )\n        .Then(CREATED)\n        .And()\n        .When(\n            POST,\n            URI(\"/api/products/\"),\n            BODY(request)\n        )\n        .Then(CONFLICT);\n}\n```\n\n**Chained Api Scenario**\n\n```csharp\npublic async Task Post_ShouldReturn_CreatedStatus_With_CartId()\n{\n    var createdReservationId = Guid.Empty;\n\n    await API.Scenario(\n        // Create Reservations\n        API.Given()\n            .When(\n                POST,        \n                URI(\"/api/Reservations/\"),\n                BODY(new CreateTentativeReservationRequest { SeatId = SeatId })\n            )\n            .Then(CREATED,\n                response =\u003e\n                {\n                    createdReservationId = response.GetCreatedId\u003cGuid\u003e();\n                    return ValueTask.CompletedTask;\n                }),\n\n        // Get reservation details\n        _ =\u003e API.Given()\n            .When(\n                GET\n                URI($\"/api/Reservations/{createdReservationId}\")\n            )\n            .Then(\n                OK,\n                RESPONSE_BODY\u003cReservationDetails\u003e(reservation =\u003e\n                {\n                    reservation.Id.Should().Be(createdReservationId);\n                    reservation.Status.Should().Be(ReservationStatus.Tentative);\n                    reservation.SeatId.Should().Be(SeatId);\n                    reservation.Number.Should().NotBeEmpty();\n                    reservation.Version.Should().Be(1);\n                })),\n\n        // Get reservations list\n        _ =\u003e API.Given()\n            .When(GET, URI(\"/api/Reservations/\"))\n            .Then(\n                OK,\n                RESPONSE_BODY\u003cPagedListResponse\u003cReservationShortInfo\u003e\u003e(reservations =\u003e\n                {\n                    reservations.Should().NotBeNull();\n                    reservations.Items.Should().NotBeNull();\n\n                    reservations.Items.Should().HaveCount(1);\n                    reservations.TotalItemCount.Should().Be(1);\n                    reservations.HasNextPage.Should().Be(false);\n\n                    var reservationInfo = reservations.Items.Single();\n\n                    reservationInfo.Id.Should().Be(createdReservationId);\n                    reservationInfo.Number.Should().NotBeNull().And.NotBeEmpty();\n                    reservationInfo.Status.Should().Be(ReservationStatus.Tentative);\n                })),\n\n        // Get reservation history\n        _ =\u003e API.Given()\n            .When(GET, URI($\"/api/Reservations/{createdReservationId}/history\"))\n            .Then(\n                OK,\n                RESPONSE_BODY\u003cPagedListResponse\u003cReservationHistory\u003e\u003e(reservations =\u003e\n                {\n                    reservations.Should().NotBeNull();\n                    reservations.Items.Should().NotBeNull();\n\n                    reservations.Items.Should().HaveCount(1);\n                    reservations.TotalItemCount.Should().Be(1);\n                    reservations.HasNextPage.Should().Be(false);\n\n                    var reservationInfo = reservations.Items.Single();\n\n                    reservationInfo.ReservationId.Should().Be(createdReservationId);\n                    reservationInfo.Description.Should().StartWith(\"Created tentative reservation with number\");\n                }))\n    );\n}\n```\n\n### XUnit setup\n\n### Injecting as Class Fixture\n\nBy default, it's recommended to inject `ApiSpecification\u003cYourProgram\u003e` instance as `ClassFixture` to ensure that all dependencies (e.g. `HttpClient`) will be appropriately disposed.\n\n```csharp\npublic class CreateMeetingTests: IClassFixture\u003cApiSpecification\u003cProgram\u003e\u003e\n{\n    private readonly ApiSpecification\u003cProgram\u003e API;\n\n    public CreateMeetingTests(ApiSpecification\u003cProgram\u003e api) =\u003e API = api;\n\n    [Fact]\n    public Task CreateCommand_ShouldPublish_MeetingCreateEvent() =\u003e\n        API.Given()\n            .When(\n                POST, \n                URI(\"/api/meetings/),\n                BODY(new CreateMeeting(Guid.NewGuid(), \"Event Sourcing Workshop\"))\n            )\n            .Then(CREATED);\n}\n```\n\n\n### Setting up data with `IAsyncLifetime`\n\nSometimes you need to set up test data asynchronously (e.g. open a shopping cart before cancelling it). You might not want to pollute your tests code with test case setup or do more extended preparation. For that XUnit provides `IAsyncLifetime` interface. You can create a fixture derived from the `APISpecification` to benefit from built-in helpers and use it later in your tests.\n\n```csharp\npublic class GetProductDetailsFixture: ApiSpecification\u003cProgram\u003e, IAsyncLifetime\n{\n    public ProductDetails ExistingProduct = default!;\n\n    public GetProductDetailsFixture(): base(new WarehouseTestWebApplicationFactory()) { }\n\n    public async Task InitializeAsync()\n    {\n        var registerProduct = new RegisterProductRequest(\"IN11111\", \"ValidName\", \"ValidDescription\");\n        var productId = await Given()\n            .When(POST, URI(\"/api/products\"), BODY(registerProduct))\n            .Then(CREATED)\n            .GetCreatedId\u003cGuid\u003e();\n\n        var (sku, name, description) = registerProduct;\n        ExistingProduct = new ProductDetails(productId, sku!, name!, description);\n    }\n\n    public Task DisposeAsync() =\u003e Task.CompletedTask;\n}\n\npublic class GetProductDetailsTests: IClassFixture\u003cGetProductDetailsFixture\u003e\n{\n    private readonly GetProductDetailsFixture API;\n\n    public GetProductDetailsTests(GetProductDetailsFixture api) =\u003e API = api;\n\n    [Fact]\n    public Task ValidRequest_With_NoParams_ShouldReturn_200() =\u003e\n        API.Given()\n            .When(GET, URI($\"/api/products/{API.ExistingProduct.Id}\"))\n            .Then(OK, RESPONSE_BODY(API.ExistingProduct));\n\n    [Theory]\n    [InlineData(12)]\n    [InlineData(\"not-a-guid\")]\n    public Task InvalidGuidId_ShouldReturn_404(object invalidId) =\u003e\n        API.Given()\n            .When(GET, URI($\"/api/products/{invalidId}\"))\n            .Then(NOT_FOUND);\n\n    [Fact]\n    public Task NotExistingId_ShouldReturn_404() =\u003e\n        API.Given()\n            .When(GET, URI($\"/api/products/{Guid.NewGuid()}\"))\n            .Then(NOT_FOUND);\n}\n```\n\n## Credits\n\nSpecial thanks go to:\n- Simon Cropp for [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets) that I'm using for plugging snippets to markdown,\n- Adam Ralph for [BullsEye](https://github.com/adamralph/bullseye), which I'm using to make the build process seamless,\n- [Babu Annamalai](https://mysticmind.dev/) that did a similar build setup in [Marten](https://martendb.io/) which I inspired a lot,\n- Dennis Doomen for [Fluent Assertions](https://fluentassertions.com/), which I'm using for internal assertions, especially checking the response body.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foskardudycz%2Fogooreck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foskardudycz%2Fogooreck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foskardudycz%2Fogooreck/lists"}