Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions example/Example1/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,26 @@
<PackageVersion Include="Confluent.Kafka" Version="2.6.0" />
<PackageVersion Include="FluentValidation" Version="11.11.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.3.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.9" />
</ItemGroup>
<!-- Tests' dependencies -->
<ItemGroup>
Expand All @@ -44,9 +44,10 @@
<PackageVersion Include="Testcontainers.Kafka" Version="4.6.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.6.0" />
<PackageVersion Include="Testcontainers.Redis" Version="4.6.0" />
<PackageVersion Include="Mokkit" Version="0.2.3" />
<PackageVersion Include="Mokkit.Containers.Microsoft.Extensions.DependencyInjection" Version="0.2.2" />
<PackageVersion Include="Mokkit.Containers.Moq" Version="0.2.2" />
<PackageVersion Include="Mokkit" Version="0.3.0-preview.1" />
<PackageVersion Include="Mokkit.Containers.Bag" Version="0.3.0-preview.1" />
<PackageVersion Include="Mokkit.Containers.Microsoft.Extensions.DependencyInjection" Version="0.3.0-preview.1" />
<PackageVersion Include="Mokkit.Containers.Moq" Version="0.3.0-preview.1" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NUnit" Version="4.3.2" />
<PackageVersion Include="NUnit.Analyzers" Version="4.6.0">
Expand Down
4 changes: 2 additions & 2 deletions example/Example1/src/Mokkit.Example1.Api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy project files
Expand All @@ -26,7 +26,7 @@ FROM build AS publish
RUN dotnet publish "Mokkit.Example1.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app

# Create non-root user
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Net;
using Mokkit;
using Mokkit.Arrange;
using Mokkit.Example1.Domain.Entities;
using Mokkit.Example1.E2E.Tests.Contracts;
using Capture = Mokkit.Capture;

namespace Mokkit.Example1.E2E.Tests.Clients;

Expand All @@ -20,9 +20,9 @@ public static class ArrangeClientApi
/// the precondition couldn't be established — not the assertion under test.
/// </summary>
public static ITestArrange NewClient(
this ITestArrange arrange, out Capture<Guid> idCapture, params ClientFieldFn[] fields)
this ITestArrange arrange, out Trapture<Guid> idCapture, params ClientFieldFn[] fields)
{
var capture = Capture.Start(out idCapture);
var capture = Trapture.Start(out idCapture);
return arrange.Then(async host =>
{
await host.ExecuteAsync<HttpClient>(async http =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Mokkit;
using Mokkit.Arrange;
using Mokkit.Example1.E2E.Tests.Contracts;

namespace Mokkit.Example1.E2E.Tests.Clients;

/// <summary>
/// Arrange helpers whose bodies are supplied by the <c>[MokkitCapture]</c> source generator — no hand-written
/// <c>Trapture.Start(...)</c> / <c>Then(Set(new ...))</c> boilerplate. The generator builds the message from the
/// method's parameters (object-initializer here, since <see cref="StatusChangedMessage"/> uses init properties).
/// </summary>
public static partial class ArrangeMessages
{
[MokkitCapture]
public static partial ITestArrange ArrangeStatusChanged(
this ITestArrange arrange,
out Trapture<StatusChangedMessage> message,
Guid clientId,
string? name,
string? email,
string? phone,
int status);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Mokkit.Example1.Domain.Entities;
using Mokkit.Inspect;
using static Mokkit.Example1.E2E.Tests.Clients.ArrangeClientApi;

namespace Mokkit.Example1.E2E.Tests.Clients;
Expand All @@ -16,17 +17,20 @@ public async Task Create_ViaApi_IsRetrievable_Persisted_AndAnnounced()
// ACT — creating the client is the action under test; it yields the write-result artifact
var result = await Act(WithName("Acme Corporation"), WithEmail("acme@e2e.test"));

// INSPECT — assert the result, then observe downstream effects by its id
// INSPECT — assert the result, capture its id once (guarded non-empty), then observe the three
// independent downstream effects concurrently: the API read, the DB row and the published event.
await Inspect
.WriteResult(result).Created()
.ApiClient(result.ClientId!.Value, c =>
{
c.Name.ShouldBe("Acme Corporation");
c.Email.ShouldBe("acme@e2e.test");
c.Status.ShouldBe((int)ClientStatus.Active);
})
.DbClient(result.ClientId!.Value, c => c.ShouldNotBeNull())
.EventPublished("clients.created", result.ClientId!.Value);
.Ensure(result, r => r.ClientId, out var clientId)
.ThenAll(
b => b.ApiClient(clientId, c =>
{
c.Name.ShouldBe("Acme Corporation");
c.Email.ShouldBe("acme@e2e.test");
c.Status.ShouldBe((int)ClientStatus.Active);
}),
b => b.DbClient(clientId, c => c.ShouldNotBeNull()),
b => b.EventPublished("clients.created", clientId));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ public StatusChangeFlowTests(E2EStack stack) : base(stack)
[Fact]
public async Task StatusChangedMessage_IsConsumed_AndReflectedEverywhere()
{
// ARRANGE — a real client created through the public API
// ARRANGE — a real client created through the public API, then the status-changed message that will be
// fed to it. ArrangeStatusChanged's body is source-generated from [MokkitCapture] (see ArrangeMessages).
await Arrange
.NewClient(out var clientId, WithName(Name), WithEmail(Email), WithPhone(Phone), WithStatus(ClientStatus.Active));

// ACT — an upstream system emits a status-changed message onto Kafka (carrying the full record,
await Arrange
.ArrangeStatusChanged(out var message, clientId, Name, Email, Phone, (int)ClientStatus.Suspended);

// ACT — an upstream system emits the status-changed message onto Kafka (carrying the full record,
// which the consumer validates before applying)
await Act(clientId, ClientStatus.Suspended);
await Act(clientId, message);

// INSPECT — the service consumes it; the change shows up via the API (eventually — it's async),
// is persisted, and a confirmation event is published
Expand All @@ -36,18 +40,9 @@ await Inspect
.EventPublished("clients.updated", clientId);
}

private Task Act(Guid clientId, ClientStatus status) =>
private Task Act(Guid clientId, StatusChangedMessage message) =>
Stage.ExecuteAsync<IProducer<string, string>>(async producer =>
{
var message = new StatusChangedMessage
{
ClientId = clientId,
Name = Name,
Email = Email,
Phone = Phone,
Status = (int)status
};

await producer.ProduceAsync("clients.status-changed", new Message<string, string>
{
Key = clientId.ToString(),
Expand Down
27 changes: 12 additions & 15 deletions example/Example1/src/Mokkit.Example1.E2E.Tests/E2EStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Images;
using DotNet.Testcontainers.Networks;
using Microsoft.Extensions.DependencyInjection;
using Mokkit.Containers;
using Mokkit.Containers.Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Mokkit.Containers.Bag;
using Mokkit.Example1.Db.Postgres;
using Mokkit.Example1.Db.Postgres.Options;
using Mokkit.Suite;
using Npgsql;
using Respawn;
using Respawn.Graph;
using StackExchange.Redis;
using Testcontainers.Kafka;
using Testcontainers.PostgreSql;
using Testcontainers.Redis;
Expand Down Expand Up @@ -108,28 +105,28 @@ public async Task InitializeAsync()

_pgConnectionString = _postgres.GetConnectionString();
var apiBaseAddress = new UriBuilder("http", _api.Hostname, _api.GetMappedPublicPort(8080)).Uri;
var redisConnection = _redis.GetConnectionString();

var external = new ServiceProviderContainerBuilder().UseInit(services =>
// The lightweight Bag container just holds the pre-built external clients this suite resolves — no
// Microsoft.Extensions.DependencyInjection dependency, no auto-wiring, no options indirection.
var external = new BagContainerBuilder().UseInit(bag =>
{
services.AddSingleton(new HttpClient { BaseAddress = apiBaseAddress });
services.AddSingleton<IProducer<string, string>>(_ =>
bag.AddInstance(new HttpClient { BaseAddress = apiBaseAddress });
bag.AddInstance<IProducer<string, string>>(
new ProducerBuilder<string, string>(new ProducerConfig
{
BootstrapServers = kafkaBootstrap,
MessageTimeoutMs = 10000 // fail fast instead of librdkafka's 5-minute default
}).Build());
services.AddSingleton(new KafkaProbe(kafkaBootstrap));
services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect($"{redisConnection},abortConnect=false"));
bag.AddInstance(new KafkaProbe(kafkaBootstrap));

services.Configure<DatabaseOptions>(options => options.Primary = _pgConnectionString);
services.AddPostgresDbContext();
// Created fresh per stage and disposed when the stage ends (matching the previous scoped DbContext lifetime).
bag.AddFactory(() => new ExampleContext(
new DbContextOptionsBuilder<ExampleContext>().UseNpgsql(_pgConnectionString).Options));

return Task.CompletedTask;
});

_setup = await TestStageSetup.Create(new IDependencyContainerBuilder[] { external });
_setup = await TestStageSetup.Create(external);

await using var connection = new NpgsqlConnection(_pgConnectionString);
await connection.OpenAsync();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -14,8 +14,6 @@
<PackageReference Include="Confluent.Kafka" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Mokkit" />
<PackageReference Include="Mokkit.Containers.Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Respawn" />
<PackageReference Include="Shouldly" />
<PackageReference Include="StackExchange.Redis" />
Expand All @@ -25,6 +23,10 @@
<PackageReference Include="Testcontainers.Redis" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<!-- Published Mokkit packages. The Mokkit package bundles the [MokkitCapture] source generator
(analyzers/dotnet/cs), so no separate generator reference is needed. -->
<PackageReference Include="Mokkit" />
<PackageReference Include="Mokkit.Containers.Bag" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Mokkit.Arrange;
using Mokkit.Containers;
using Mokkit.Containers.Common;
using Mokkit.Containers.Microsoft.Extensions.DependencyInjection;
using Mokkit.Containers.Moq;
using Mokkit.Inspect;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Mokkit;
using Mokkit.Arrange;
using Mokkit.Example1.Application.Features.Client.SaveClient;
using Capture = Mokkit.Capture;

namespace Mokkit.Example1.Integration.Tests.Features.Client.SaveClient;

Expand All @@ -18,7 +18,7 @@ public static class ArrangeSaveClient
/// <summary>Builds a <see cref="SaveOperationKind.Create"/> command over the defaults.</summary>
public static ITestArrange CreateClientCommand(
this ITestArrange arrange,
out Capture<SaveClientCommand> commandCapture,
out Trapture<SaveClientCommand> commandCapture,
params ClientMutateFn[] mutateFns)
{
return arrange.Command(out commandCapture, SaveOperationKind.Create, clientId: null, mutateFns);
Expand All @@ -27,7 +27,7 @@ public static ITestArrange CreateClientCommand(
/// <summary>Builds a <see cref="SaveOperationKind.Update"/> command targeting an existing client.</summary>
public static ITestArrange UpdateClientCommand(
this ITestArrange arrange,
out Capture<SaveClientCommand> commandCapture,
out Trapture<SaveClientCommand> commandCapture,
Guid clientId,
params ClientMutateFn[] mutateFns)
{
Expand All @@ -36,12 +36,12 @@ public static ITestArrange UpdateClientCommand(

private static ITestArrange Command(
this ITestArrange arrange,
out Capture<SaveClientCommand> commandCapture,
out Trapture<SaveClientCommand> commandCapture,
SaveOperationKind operation,
Guid? clientId,
ClientMutateFn[] mutateFns)
{
var capture = Capture.Start(out commandCapture);
var capture = Trapture.Start(out commandCapture);

return arrange.Then(_ =>
{
Expand Down
Loading
Loading