diff --git a/README.md b/README.md index bde1e7a..8dc0eb1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mokkit — write tests that read like a story, in plain C#

-[![NuGet](https://img.shields.io/nuget/vpre/Mokkit.svg)](https://www.nuget.org/packages/Mokkit) +[![NuGet](https://img.shields.io/nuget/v/Mokkit.svg)](https://www.nuget.org/packages/Mokkit) [![CI](https://github.com/GrafGenerator/mokkit/actions/workflows/ci.yml/badge.svg)](https://github.com/GrafGenerator/mokkit/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -19,32 +19,111 @@ end-to-end run. ```csharp // A Mokkit test reads like the scenario it describes. [Fact] -public async Task Suspending_a_client_reflects_everywhere() +public async Task CalculateDiscount_ForVipUser_AppliesTieredRate() { await Arrange - .NewClient(out var clientId, WithName("Acme Corporation")) - .CacheHasClient(clientId); + .UserExists(out var user, WithStatus(UserStatus.Vip)) // build the user, set up its repository + .DiscountRateIs(UserStatus.Vip, rate: 0.15m); // set up the rates repository - await Act(clientId, ClientStatus.Suspended); + var result = await Act.CalculateDiscount(user, orderTotal: 100m); await Inspect - .ApiClientEventually(clientId, c => c.Status == Suspended) - .DbClient(clientId, c => c!.Status.ShouldBe(Suspended)) - .EventPublished("clients.updated", clientId); + .OkResult(result).DiscountAppliedFor(user, expectedAmount: 15m) + .Ensure(result, r => r.UserId, out var userId) // guard the id, then reuse it + .ThenAll( // these three run in parallel + b => b.UserRepositoryQueried(userId), + b => b.UserCalculationRepositoryQueried(userId), + b => b.RateRepositoryQueried(UserStatus.Vip)); } ``` -`NewClient`, `CacheHasClient`, `ApiClientEventually`, `DbClient`, `EventPublished` aren't Mokkit APIs — they're -your verbs. Mokkit provides the Arrange / Act / Inspect shape and the machinery underneath. +`UserExists`, `DiscountRateIs`, `UserRepositoryQueried` aren't Mokkit APIs — they're **your verbs**. Mokkit +provides the Arrange / Act / Inspect shape and the machinery underneath. + +## What a verb costs you + +That's the fair question, so here's the whole answer. A verb is an extension method that does the coupled +setup (or the coupled verification) once, and hides it behind a name from your domain: + +```csharp +public static class ArrangeDiscount +{ + public static ITestArrange UserExists( + this ITestArrange arrange, + out Capture userCapture, + Action? mutate = null) + { + var capture = Capture.Start(out userCapture); + + return arrange.Then(host => + { + var user = new User { Id = Guid.NewGuid() }; + mutate?.Invoke(user); + + host.Execute(repo => + repo.GetByIdAsync(user.Id, Arg.Any()).Returns(user)); + + capture.Set(user); + }); + } +} + +public static class InspectDiscount +{ + public static ITestInspect UserRepositoryQueried(this ITestInspect inspect, Guid userId) + { + return inspect.Then(host => + host.Execute(repo => + repo.Received(1).GetByIdAsync(userId, Arg.Any()))); + } +} +``` + +Written once per fixture and living next to the tests. They start paying off quickly — most get reused across +every test that touches the same area, and a good half end up reused far beyond it. + +Your mocking and assertion libraries stay yours: the snippets above use NSubstitute, but Moq, FakeItEasy, +Shouldly, FluentAssertions or plain `Assert` all work the same way. + +## The same vocabulary, from unit to end-to-end + +Because the verbs are just extension methods over a context Mokkit hands them, the *same* test can run against +mocks or against real infrastructure — you swap the helper implementations, not the test: + +```csharp +[Fact] +public async Task CalculateDiscount_ForVipUser_AppliesTieredRate() +{ + await Arrange + .UserExists(out var user, WithStatus(UserStatus.Vip)) // ← now creates the user via the API + .DiscountRateIs(UserStatus.Vip, 0.15m); // ← now sets the rate via the API + + var result = await Act.CalculateDiscount(user, orderTotal: 100m); // ← now calls the real endpoint + + await Inspect + .OkResult(result).DiscountAppliedFor(user, expectedAmount: 15m) + .Ensure(result, r => r.UserId, out var userId) + .Ensure(result, r => r.CalculationId, out var calculationId) + .UserCalculationStored(userId, calculationId, 15m) // e2e-only: assert against the database + .DiscountEventPublishedFor(user, 15m); // e2e-only: assert against the broker +} +``` + +The Arrange and Act lines are identical; end-to-end just adds the inspects that only make sense against real +infrastructure. + +A worked three-tier example — the same feature covered by unit, integration (Testcontainers + Postgres) and +end-to-end (API + Kafka) suites, across xUnit, NUnit and TUnit — lives in +**[`example/Example1`](example/Example1)**. ## Install -Core + a DI adapter + a mock adapter (prerelease for now): +Core + a DI adapter + a mock adapter: ```bash -dotnet add package Mokkit --prerelease -dotnet add package Mokkit.Containers.Microsoft.Extensions.DependencyInjection --prerelease -dotnet add package Mokkit.Containers.NSubstitute --prerelease +dotnet add package Mokkit +dotnet add package Mokkit.Containers.Microsoft.Extensions.DependencyInjection +dotnet add package Mokkit.Containers.NSubstitute ``` ## Packages @@ -56,6 +135,19 @@ dotnet add package Mokkit.Containers.NSubstitute --prerelease | `Mokkit.Containers.Moq` · `.NSubstitute` · `.FakeItEasy` | Mock library adapters | | `Mokkit.Containers.Bag` | Dependency-free "hold a few instances" container | +No package is needed for your test framework — Mokkit runs inside xUnit, NUnit, MSTest or TUnit as-is. + +## Status + +**v0.4.0**, MIT, targets .NET Standard 2.0. It has been in daily use on a production codebase for two months, +and it is actively developed: expect API changes before 1.0, with verbose output and test-report integration +among the things still missing. Adapters for other containers and mock libraries are straightforward to add. + +What it most needs right now is feedback that isn't mine. Would you write your tests this way — and if not, +what puts you off? [Discussions](https://github.com/GrafGenerator/mokkit/discussions) is the place for that, +and for anything open-ended; a reproducible bug or a concrete feature request is best as an +[issue](https://github.com/GrafGenerator/mokkit/issues). + ## Documentation Full guides, concepts and API reference: **[mokkit.net](https://mokkit.net)** @@ -63,8 +155,6 @@ Full guides, concepts and API reference: **[mokkit.net](https://mokkit.net)** - [Introduction](https://mokkit.net/introduction/) · [Why Mokkit? (vs BDD/DSL)](https://mokkit.net/why-mokkit/) · [Quickstart](https://mokkit.net/quickstart/) - [Building your test vocabulary](https://mokkit.net/concepts/vocabulary/) — the idea Mokkit is built around -A worked, three-tier example (unit / integration / e2e) lives in [`example/Example1`](example/Example1). - ## License [MIT](LICENSE) © Nikita Ivanov diff --git a/docs/src/content/docs/concepts/captures.md b/docs/src/content/docs/concepts/captures.md index 091d570..f841a0c 100644 --- a/docs/src/content/docs/concepts/captures.md +++ b/docs/src/content/docs/concepts/captures.md @@ -49,6 +49,50 @@ await Inspect.ApiClient(clientId, c => c.Name.ShouldBe("Acme Holdings")); Reach for `Capture` when you *want* the read to stand out — for instance a result whose `.Value` you unpack and assert on deliberately. +## Guarded reads: `EnsureValue` and `Prop` + +`Value` is nullable, because a capture legitimately starts out empty. Read it before the chain has filled it +and you get a `NullReferenceException` from somewhere deep in the test — so in practice a null-forgiving +operator ends up in every read: + +```csharp +await StoreClient(client.Value!); // the ! is load-bearing and unchecked +var result = await GetClient(client.Value!.Id); +``` + +`ICapture` offers guarded versions of both — so they work on `Capture` and `Trapture` alike: + +```csharp +await StoreClient(client.EnsureValue); // the whole value +var result = await GetClient(client.Prop(c => c.Id)); // a member off it +``` + +`EnsureValue` is the capture-level member of the [`Ensure`](/guides/ensure/) family and uses that family's +definition of "empty": an unfilled capture, or one holding `null` / `""` / `0` / `Guid.Empty` / an empty +collection, fails loudly right at the read. That's what lets it catch an unfilled **value-type** capture too, +where there is no `null` to check. + +`Prop` is exactly `propFn(EnsureValue)`: the *capture* is guarded, and the projected member comes back as-is, +so a nullable member may still be `null`. It nests as far as you like — `message.Prop(m => m.Message.Value)`. + +Reach for `Prop` on a single member read, and `EnsureValue` when a step needs the whole artifact or several +members off it: + +```csharp +// One guarded read, then plain member access — better than five separate Prop calls. +var expected = message.EnsureValue; + +handler.Received(1).Handle(Arg.Is(c => + c.ClientData.Id == expected.ClientId && + c.ClientData.Name == expected.Name && + c.ClientData.Email == expected.Email), Arg.Any()); +``` + +:::note +`Value` stays useful where empty is a legitimate outcome — snapshotting a `Capture` that models +"not found", for instance. Use it deliberately there; use the guarded pair everywhere else. +::: + ## Producing a capture from a verb An Arrange (or Act) verb that creates an artifact starts a capture, then sets it inside the deferred step: @@ -69,8 +113,9 @@ public static ITestArrange NewClient( } ``` -The interfaces behind this are small: `ICapture` exposes `Value`; `ICaptureInitializer` exposes -`Set(T)`. A verb takes the *initializer* to write and hands back the *capture* to read. +The interfaces behind this are small: `ICapture` exposes `Value`, `EnsureValue` and `Prop`; +`ICaptureInitializer` exposes `Set(T)`. A verb takes the *initializer* to write and hands back the +*capture* to read. ## Deriving one value from another: `Ensure` diff --git a/docs/src/content/docs/guides/ensure.md b/docs/src/content/docs/guides/ensure.md index 015c4d3..532607a 100644 --- a/docs/src/content/docs/guides/ensure.md +++ b/docs/src/content/docs/guides/ensure.md @@ -24,6 +24,41 @@ await Inspect a clear failure instead of letting a bogus value flow onward. There's also a direct form when you already hold the value — `.Ensure(someValue, out var captured)`. +The selector form comes in three shapes, picked by what you project: + +| Source | Selector | Use for | +| --- | --- | --- | +| a plain object | `r => r.ClientId` (`Guid?`) | unwrapping a nullable **struct** member | +| a plain object | `r => r.Name` (`string?`) | guarding a nullable **reference** member | +| a [capture](/concepts/captures/) | `c => c.Id` (anything) | projecting off a capture Arrange already filled | + +The capture-shaped form is the one to reach for when the same id is read several times in one chain — guard it +once, then hand the plain value to every step that follows: + +```csharp +await Inspect + .Ensure(seeded, c => c.Id, out var clientId) // one guarded read ... + .GetResult(result).Found(clientId) // ... reused from here on + .CacheUpdated(clientId); +``` + +It reports an uninitialized capture as its own failure, rather than as a merely "empty" value. For a **single** +read, [`Prop`](/concepts/captures/) is lighter: `.CacheQueried(client.Prop(c => c.Id))`. + +## On the capture itself + +`Ensure` is a *chain* verb — it guards a value and threads it onward. The same guard is available directly on +any [capture](/concepts/captures/), for when you just need to read it: + +| | Guards | Hands back | +| --- | --- | --- | +| `capture.EnsureValue` | the captured value, non-empty | the whole value | +| `capture.Prop(c => c.Id)` | the capture | the projected member, as-is | +| `.Ensure(capture, c => c.Id, out var id)` | both, inside the chain | a reusable, guarded local | + +All three share one definition of "empty", so `client.EnsureValue` rejects exactly what +`.Ensure(client.Value, out _)` would. + ## In Arrange (deferred) The arrange overloads are **deferred** — they capture a `Trapture` that's filled when the chain runs, so you @@ -32,27 +67,29 @@ can derive from a value another arrange step produces: ```csharp public static ITestArrange Ensure( this ITestArrange arrange, ICapture source, Func selector, - out Trapture captured, string? because = null) where TSource : class + out Trapture captured, string? because = null) { var initializer = Trapture.Start(out captured); return arrange.Then(_ => { - var value = source.Value ?? throw new InvalidOperationException("Ensure: source capture is not initialized."); + if (source.Value is not { } value) throw new InvalidOperationException("Ensure: uninitialized capture."); initializer.Set(EnsureGuard.NotEmpty(selector(value), because)); }); } ``` Use it to guard a derived id before later arranges consume it. There's also a thunk form — -`.Ensure(() => client.Value!.Id, out var id)` — for values built from more than one capture. Both hand back a -[`Trapture`](/concepts/captures/), so the id flows transparently. +`.Ensure(() => client.Prop(c => c.Id) + suffix, out var key)` — for values built from more than one capture. +Both hand back a [`Trapture`](/concepts/captures/), so the id flows transparently. The source capture can +hold anything, a value type included: `.Ensure(clientId, g => g.ToString(), out var key)`. ## Why it's worth a verb Without `Ensure`, ids arrive as `result.ClientId!.Value` — a null-forgiving operator and an unchecked assumption in every test. `Ensure` replaces that with a single, self-documenting step that fails loudly and early if the precondition ("there *is* an id") doesn't hold. It's the idiomatic bridge between an artifact and -the [captures](/concepts/captures/) that carry it forward. +the [captures](/concepts/captures/) that carry it forward — `Ensure` being the guarded *derivation*, and +[`Prop`](/concepts/captures/) the guarded *read*. ## Next diff --git a/docs/src/content/docs/guides/kafka.md b/docs/src/content/docs/guides/kafka.md index 34df221..ac6cf5b 100644 --- a/docs/src/content/docs/guides/kafka.md +++ b/docs/src/content/docs/guides/kafka.md @@ -23,7 +23,7 @@ public async Task ValidMessage_UpdatesClient_AndPublishesConfirmation() // ACT — run the real processor over the raw payload. await Stage.Act().Then(host => - host.ExecuteAsync(p => p.ProcessAsync(KafkaMessageFaker.ToJson(message.Value!)))); + host.ExecuteAsync(p => p.ProcessAsync(KafkaMessageFaker.ToJson(message.EnsureValue)))); // INSPECT — it dispatched an Update and published the confirmation. await Inspect diff --git a/docs/src/content/docs/guides/unit-mocked-dependency.md b/docs/src/content/docs/guides/unit-mocked-dependency.md index a4da36e..02829ff 100644 --- a/docs/src/content/docs/guides/unit-mocked-dependency.md +++ b/docs/src/content/docs/guides/unit-mocked-dependency.md @@ -43,7 +43,7 @@ public static ITestArrange HandlerSucceedsFor( this ITestArrange arrange, Capture message) => arrange.Then(host => host.Execute>(handler => handler.Handle(Arg.Any(), Arg.Any()) - .Returns(new SaveClientCommandResult(true, message.Value!.ClientId)))); + .Returns(new SaveClientCommandResult(true, message.Prop(m => m.ClientId))))); ``` **Inspect verbs** verify how the SUT drove those doubles — they only *read*: @@ -55,13 +55,13 @@ public static ITestInspect HandledUpdate( handler.Received(1).Handle( Arg.Is(c => c.Operation == SaveOperationKind.Update && - c.ClientData.Id == message.Value!.ClientId), + c.ClientData.Id == message.Prop(m => m.ClientId)), Arg.Any()))); public static ITestInspect ConfirmationPublishedFor( this ITestInspect inspect, Capture message) => inspect.Then(host => host.Execute(publisher => - publisher.Received(1).PublishClientEventAsync(message.Value!.ClientId, "updated", Arg.Any()))); + publisher.Received(1).PublishClientEventAsync(message.Prop(m => m.ClientId), "updated", Arg.Any()))); ``` `host.Execute(...)` resolves `T` from the stage. When `T` is a substituted type, you get the substitute — the @@ -87,7 +87,7 @@ public sealed class ClientStatusChangedProcessorTests : BaseUnitTest - host.ExecuteAsync(p => p.ProcessAsync(KafkaMessageFaker.ToJson(message.Value!)))); + host.ExecuteAsync(p => p.ProcessAsync(KafkaMessageFaker.ToJson(message.EnsureValue)))); // INSPECT — it dispatched an Update and published the confirmation. await Inspect diff --git a/docs/src/content/docs/installation.mdx b/docs/src/content/docs/installation.mdx index 4b7b660..65cf578 100644 --- a/docs/src/content/docs/installation.mdx +++ b/docs/src/content/docs/installation.mdx @@ -8,11 +8,6 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; You install **three things**: the core, one **container adapter** for your DI container (or the trivial Bag), and — if your tests use mocks — one **mock adapter** for your mocking library. -:::note[Prerelease] -Mokkit is currently published as a prerelease (`0.3.0-preview.1`). Add `--prerelease` (CLI) or reference the -exact version so NuGet will resolve it. -::: - ## A minimal unit-test setup Core + Microsoft DI + NSubstitute is a common starting point: @@ -20,17 +15,17 @@ Core + Microsoft DI + NSubstitute is a common starting point: ```bash -dotnet add package Mokkit --prerelease -dotnet add package Mokkit.Containers.Microsoft.Extensions.DependencyInjection --prerelease -dotnet add package Mokkit.Containers.NSubstitute --prerelease +dotnet add package Mokkit +dotnet add package Mokkit.Containers.Microsoft.Extensions.DependencyInjection +dotnet add package Mokkit.Containers.NSubstitute ``` ```xml - - - + + + ``` diff --git a/example/Example1/Directory.Packages.props b/example/Example1/Directory.Packages.props index c370c80..195b138 100644 --- a/example/Example1/Directory.Packages.props +++ b/example/Example1/Directory.Packages.props @@ -44,11 +44,11 @@ - - - - - + + + + + diff --git a/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/GetClient/GetClientQueryHandlerTests.cs b/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/GetClient/GetClientQueryHandlerTests.cs index 6d88a8f..aa95253 100644 --- a/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/GetClient/GetClientQueryHandlerTests.cs +++ b/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/GetClient/GetClientQueryHandlerTests.cs @@ -1,3 +1,4 @@ +using Mokkit.Inspect; using Mokkit.Example1.Application.Features.Client.GetClient; using Mokkit.Example1.Common; @@ -14,11 +15,11 @@ await Arrange .CachedClient(out var cached); // ACT - var result = await Act.GetClient(new GetClientQuery { ClientId = cached.Value!.Id }); + var result = await Act.GetClient(new GetClientQuery { ClientId = cached.Prop(c => c.Id) }); // INSPECT — served from cache, so the handler does not write the cache again. await Inspect - .GetResult(result).Found(cached.Value!.Id) + .GetResult(result).Found(cached.Prop(c => c.Id)) .CacheNotUpdated(); } @@ -31,12 +32,13 @@ await Arrange .DbClient(out var seeded); // ACT - var result = await Act.GetClient(new GetClientQuery { ClientId = seeded.Value!.Id }); + var result = await Act.GetClient(new GetClientQuery { ClientId = seeded.Prop(c => c.Id) }); // INSPECT — served from the database and written back into the cache. await Inspect - .GetResult(result).Found(seeded.Value!.Id) - .CacheUpdated(seeded.Value!.Id); + .Ensure(seeded, c => c.Id, out var clientId) + .GetResult(result).Found(clientId) + .CacheUpdated(clientId); } [Test] diff --git a/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/SaveClient/SaveClientCommandHandlerTests.cs b/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/SaveClient/SaveClientCommandHandlerTests.cs index 38fc50d..bd90693 100644 --- a/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/SaveClient/SaveClientCommandHandlerTests.cs +++ b/example/Example1/src/Mokkit.Example1.Integration.Tests/Features/Client/SaveClient/SaveClientCommandHandlerTests.cs @@ -70,7 +70,7 @@ await Arrange await Arrange .Clock(updatedAt) - .UpdateClientCommand(out var command, existing.Value!.Id, + .UpdateClientCommand(out var command, existing.Prop(c => c.Id), WithName("Renamed Corporation"), WithStatus((int)ClientStatus.Suspended)); @@ -79,8 +79,9 @@ await Arrange // INSPECT — fields changed, CreatedAt preserved, UpdatedAt advanced, event published. await Inspect - .SaveResult(result).IsSuccess(existing.Value!.Id) - .DbClientById(existing.Value!.Id, out var saved, c => + .Ensure(existing, e => e.Id, out var clientId) + .SaveResult(result).IsSuccess(clientId) + .DbClientById(clientId, out var saved, c => { Assert.That(c!.Name, Is.EqualTo("Renamed Corporation")); Assert.That(c.Status, Is.EqualTo(ClientStatus.Suspended)); @@ -88,8 +89,8 @@ await Inspect Assert.That(c.UpdatedAt, Is.EqualTo(updatedAt)); }) .Verify(saved) - .CacheUpdated(existing.Value!.Id) - .EventPublished(existing.Value!.Id, "updated"); + .CacheUpdated(clientId) + .EventPublished(clientId, "updated"); } [Test] diff --git a/example/Example1/src/Mokkit.Example1.TUnit.Tests/Cache/ClientCacheServiceTests.cs b/example/Example1/src/Mokkit.Example1.TUnit.Tests/Cache/ClientCacheServiceTests.cs index 975401a..11c78e3 100644 --- a/example/Example1/src/Mokkit.Example1.TUnit.Tests/Cache/ClientCacheServiceTests.cs +++ b/example/Example1/src/Mokkit.Example1.TUnit.Tests/Cache/ClientCacheServiceTests.cs @@ -24,12 +24,12 @@ public async Task GetClient_WhenCached_ReturnsDeserializedClient() await Arrange.CacheHasClient(out var client); // ACT - var result = await GetClient(client.Value!.Id); + var result = await GetClient(client.Prop(c => c.Id)); // INSPECT await Inspect - .RetrievedClientMatching(result, client.Value!) - .CacheQueried(client.Value!.Id); + .RetrievedClientMatching(result, client.EnsureValue) + .CacheQueried(client.Prop(c => c.Id)); } [Test] @@ -68,10 +68,10 @@ public async Task SetClient_SerializesAndStoresWithExpiry() await Arrange.AClient(out var client); // ACT - await StoreClient(client.Value!); + await StoreClient(client.EnsureValue); // INSPECT - await Inspect.CacheStored(client.Value!); + await Inspect.CacheStored(client.EnsureValue); } [Test] diff --git a/example/Example1/src/Mokkit.Example1.Unit.Tests/Cache/ClientCacheServiceTests.cs b/example/Example1/src/Mokkit.Example1.Unit.Tests/Cache/ClientCacheServiceTests.cs index 76904cc..3091da9 100644 --- a/example/Example1/src/Mokkit.Example1.Unit.Tests/Cache/ClientCacheServiceTests.cs +++ b/example/Example1/src/Mokkit.Example1.Unit.Tests/Cache/ClientCacheServiceTests.cs @@ -21,12 +21,12 @@ public async Task GetClient_WhenCached_ReturnsDeserializedClient() await Arrange.CacheHasClient(out var client); // ACT - var result = await GetClient(client.Value!.Id); + var result = await GetClient(client.Prop(c => c.Id)); // INSPECT await Inspect - .RetrievedClientMatching(result, client.Value!) - .CacheQueried(client.Value!.Id); + .RetrievedClientMatching(result, client.EnsureValue) + .CacheQueried(client.Prop(c => c.Id)); } [Fact] @@ -65,10 +65,10 @@ public async Task SetClient_SerializesAndStoresWithExpiry() await Arrange.AClient(out var client); // ACT - await StoreClient(client.Value!); + await StoreClient(client.EnsureValue); // INSPECT - await Inspect.CacheStored(client.Value!); + await Inspect.CacheStored(client.EnsureValue); } [Fact] diff --git a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Consumer/InspectConsumer.cs b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Consumer/InspectConsumer.cs index 9ca363c..6222d07 100644 --- a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Consumer/InspectConsumer.cs +++ b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Consumer/InspectConsumer.cs @@ -26,7 +26,7 @@ public static ITestInspect ForwardedToProcessor(this ITestInspect inspect, Captu return inspect.Then(host => { host.Execute(processor => - processor.Received(1).ProcessAsync(message.Value!.Message.Value, Arg.Any())); + processor.Received(1).ProcessAsync(message.Prop(m => m.Message.Value), Arg.Any())); }); } @@ -35,7 +35,7 @@ public static ITestInspect OffsetCommitted(this ITestInspect inspect, Capture { host.Execute>(consumer => - consumer.Received(1).Commit(message.Value!)); + consumer.Received(1).Commit(message.EnsureValue)); }); } diff --git a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ArrangeProcessor.cs b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ArrangeProcessor.cs index 38c4269..d02b511 100644 --- a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ArrangeProcessor.cs +++ b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ArrangeProcessor.cs @@ -39,7 +39,7 @@ public static ITestArrange HandlerSucceedsFor( { host.Execute>(handler => handler.Handle(Arg.Any(), Arg.Any()) - .Returns(new SaveClientCommandResult(true, message.Value!.ClientId))); + .Returns(new SaveClientCommandResult(true, message.Prop(m => m.ClientId)))); }); } @@ -51,7 +51,7 @@ public static ITestArrange HandlerFailsFor( { host.Execute>(handler => handler.Handle(Arg.Any(), Arg.Any()) - .Returns(new SaveClientCommandResult(false, message.Value!.ClientId, + .Returns(new SaveClientCommandResult(false, message.Prop(m => m.ClientId), new InvalidOperationException("update failed")))); }); } diff --git a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ClientStatusChangedProcessorTests.cs b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ClientStatusChangedProcessorTests.cs index 609d12d..5771226 100644 --- a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ClientStatusChangedProcessorTests.cs +++ b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/ClientStatusChangedProcessorTests.cs @@ -76,7 +76,7 @@ await Arrange } private Task Process(Capture message) => - Process(KafkaMessageFaker.ToJson(message.Value!)); + Process(KafkaMessageFaker.ToJson(message.EnsureValue)); private Task Process(string json) => Stage.ExecuteAsync(processor => processor.ProcessAsync(json)); diff --git a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/InspectProcessor.cs b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/InspectProcessor.cs index cd7146c..3cb7528 100644 --- a/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/InspectProcessor.cs +++ b/example/Example1/src/Mokkit.Example1.Unit.Tests/Messaging/Processor/InspectProcessor.cs @@ -18,15 +18,18 @@ public static ITestInspect HandledUpdate(this ITestInspect inspect, Capture { + // One guarded read of the capture; the predicate then reads plain members off it. + var expected = message.EnsureValue; + host.Execute>(handler => handler.Received(1).Handle( Arg.Is(c => c.Operation == SaveOperationKind.Update && - c.ClientData.Id == message.Value!.ClientId && - c.ClientData.Name == message.Value!.Name && - c.ClientData.Email == message.Value!.Email && - c.ClientData.Phone == message.Value!.Phone && - c.ClientData.Status == message.Value!.Status), + c.ClientData.Id == expected.ClientId && + c.ClientData.Name == expected.Name && + c.ClientData.Email == expected.Email && + c.ClientData.Phone == expected.Phone && + c.ClientData.Status == expected.Status), Arg.Any())); }); } @@ -36,14 +39,16 @@ public static ITestInspect HandledWithEmptyContact(this ITestInspect inspect, Ca { return inspect.Then(host => { + var expected = message.EnsureValue; + host.Execute>(handler => handler.Received(1).Handle( Arg.Is(c => - c.ClientData.Id == message.Value!.ClientId && + c.ClientData.Id == expected.ClientId && c.ClientData.Name == string.Empty && c.ClientData.Email == string.Empty && c.ClientData.Phone == string.Empty && - c.ClientData.Status == message.Value!.Status), + c.ClientData.Status == expected.Status), Arg.Any())); }); } @@ -64,7 +69,7 @@ public static ITestInspect ConfirmationPublishedFor(this ITestInspect inspect, C return inspect.Then(host => { host.Execute(publisher => - publisher.Received(1).PublishClientEventAsync(message.Value!.ClientId, "updated", Arg.Any())); + publisher.Received(1).PublishClientEventAsync(message.Prop(m => m.ClientId), "updated", Arg.Any())); }); }