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
65 changes: 35 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,52 +11,57 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
9.0.x
10.0.x

- name: Setup Azure Functions Core Tools
run: npm install --global azure-functions-core-tools@4 --unsafe-perm true

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Explicit MSTest test
- name: MSTest (net8.0, net9.0, net10.0)
run: |
for framework in net8.0 net9.0 net10.0; do
dotnet test tests/UnitTestEx.MSTest.Test/UnitTestEx.MSTest.Test.csproj --framework "$framework" --configuration Debug --no-build --no-restore --verbosity normal
done

- name: NUnit (net8.0, net9.0, net10.0)
run: |
for framework in net8.0 net9.0 net10.0; do
dotnet test tests/UnitTestEx.NUnit.Test/UnitTestEx.NUnit.Test.csproj --framework "$framework" --configuration Debug --no-build --no-restore --verbosity normal
done

- name: xUnit (net8.0, net9.0, net10.0)
run: |
for framework in net8.0 net9.0 net10.0; do
dotnet test tests/UnitTestEx.Xunit.Test/UnitTestEx.Xunit.Test.csproj --framework "$framework" --configuration Debug --no-build --no-restore --verbosity normal
done

- name: Aspire MSTest (net8.0, net9.0, net10.0)
run: |
cp tests/UnitTestEx.Api/bin/Debug/net8.0/UnitTestEx.Api.deps.json tests/UnitTestEx.MSTest.Test/bin/Debug/net8.0
cd tests/UnitTestEx.MSTest.Test/bin/Debug/net8.0
dotnet test UnitTestEx.MSTest.Test.dll --no-build --verbosity normal
for framework in net8.0 net9.0 net10.0; do
dotnet test tests/UnitTestEx.Aspire.MSTest.Test/UnitTestEx.Aspire.MSTest.Test.csproj --framework "$framework" --configuration Debug --no-build --no-restore --verbosity normal
done

- name: Explicit NUnit test
- name: Aspire NUnit (net8.0, net9.0, net10.0)
run: |
cp tests/UnitTestEx.Api/bin/Debug/net8.0/UnitTestEx.Api.deps.json tests/UnitTestEx.NUnit.Test/bin/Debug/net8.0
cd tests/UnitTestEx.NUnit.Test/bin/Debug/net8.0
dotnet test UnitTestEx.NUnit.Test.dll --no-build --verbosity normal
for framework in net8.0 net9.0 net10.0; do
dotnet test tests/UnitTestEx.Aspire.NUnit.Test/UnitTestEx.Aspire.NUnit.Test.csproj --framework "$framework" --configuration Debug --no-build --no-restore --verbosity normal
done

- name: Explicit Xunit test
- name: Aspire xUnit (net8.0, net9.0, net10.0)
run: |
cp tests/UnitTestEx.Api/bin/Debug/net8.0/UnitTestEx.Api.deps.json tests/UnitTestEx.Xunit.Test/bin/Debug/net8.0
cd tests/UnitTestEx.Xunit.Test/bin/Debug/net8.0
dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests MockHttpClientTest
dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests PersonFunctionTest
dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests ProductFunctionTest
dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests ServiceBusFunctionTest
dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests PersonControllerTest
dotnet test UnitTestEx.Xunit.Test.dll --no-build --verbosity normal --tests ProductControllerTest

- name: Explicit Aspire MSTest test
run: dotnet test tests/UnitTestEx.Aspire.MSTest.Test/UnitTestEx.Aspire.MSTest.Test.csproj --no-build --verbosity normal

- name: Explicit Aspire NUnit test
run: dotnet test tests/UnitTestEx.Aspire.NUnit.Test/UnitTestEx.Aspire.NUnit.Test.csproj --no-build --verbosity normal

- name: Explicit Aspire Xunit test
run: dotnet test tests/UnitTestEx.Aspire.Xunit.Test/UnitTestEx.Aspire.Xunit.Test.csproj --no-build --verbosity normal
for framework in net8.0 net9.0 net10.0; do
dotnet test tests/UnitTestEx.Aspire.Xunit.Test/UnitTestEx.Aspire.Xunit.Test.csproj --framework "$framework" --configuration Debug --no-build --no-restore --verbosity normal
done
29 changes: 27 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,38 @@

Represents the **NuGet** versions.

## v6.0.0
- *Breaking:* Target frameworks are now `net8.0`, `net9.0` and `net10.0` only; `net6.0` and `net7.0` (both out of support) have been removed.
- *Breaking:* `UnitTestEx.Azure.Functions` supports the Azure Functions **v4 isolated worker** model only; in-process (WebJobs) support has been removed, including the `FunctionsStartup` entry point and `CreateWebJobsServiceBus*MessageActions`. See [Migrating from v5 to v6](./docs/migration-v6.md).
- *Breaking:* `UnitTestEx.Azure.ServiceBus` has been removed; its `CreateServiceBusMessage*` helpers now live in `UnitTestEx.Azure.Functions` (same `UnitTestEx` namespace).
- *Enhancement:* `FunctionTester` runs the function app's real `Program.cs` (top-level statements supported; no test-specific structure required), capturing the host at `Build()` without starting the worker; test configuration and service overrides are applied. `TEntryPoint` is any type within the function app assembly.
- *Enhancement:* `HttpTrigger<T>` supports both ASP.NET Core integration (`HttpRequest` → `IActionResult`/`IResult`) and built-in (`HttpRequestData` → `HttpResponseData`) HTTP models, asserted consistently via `HttpResponseMessageAssertor`; method and route are validated against the worker `HttpTriggerAttribute`.
- *Enhancement:* `Trigger<T>` enables any other trigger type to be tested, with `FunctionOutputAssertor` for multi-output bindings.
- *Enhancement:* Added Event Grid builders: `CreateEventGridEvent*` and `CreateCloudEvent*`.
- *Enhancement:* Added Event Hubs `EventData` builders for values, JSON, resources and raw bodies, with broker metadata support.
- *Enhancement:* Added Aspire integration coverage and documentation for a Service Bus-triggered Functions app using Microsoft's Service Bus emulator.
- *Enhancement:* `FunctionTester` now runs the function app's worker middleware pipeline (including the ASP.NET Core integration) around the invoked method, so middleware logging, short-circuiting and exception mapping can be asserted. Use `UseWorkerMiddleware(false)` to invoke the method directly.
- *Enhancement:* Added `SnapshotHttpResponse(HttpResponse)` to invocation assertors for already-written HTTP responses, without re-executing returned results; preserves buffered bytes, headers, stream ownership and position.
- *Fixed:* `TypeTester<TService>` now creates each tested service within the scope for that `Run`, preventing scoped dependencies from being reused after disposal.
- *Fixed:* UTF-8 request bodies created by `TesterBase.CreateHttpRequest` now report the encoded byte length rather than the UTF-16 character count.
- *Fixed:* `JsonElementComparer` hash codes now follow its configured semantic equality rules and tolerate valid numbers outside the decimal range.
- *Fixed:* `ApiTester` applies its test environment and settings only while the host is being built, then restores the process environment; host construction is serialized to prevent UnitTestEx API testers from racing. It also avoids building a duplicate service provider to capture `IHttpContextAccessor`.
- *Fixed:* `FunctionTester` now reads `local.settings.json` values into in-memory configuration and no longer writes a plaintext settings sidecar into the current directory.
- *Fixed:* Deferred MVC/Minimal API response conversion now restores the original response stream on success and failure rather than leaving a disposed temporary stream on the context. Content headers are preserved alongside response headers.
- *Breaking:* Added `IHttpMockRequest.WithNoBody()` (implemented by `MockHttpClientRequest` and `AspireHttpMockRequest`), which matches a request with no body: absent or zero-length. The two are indistinguishable on the wire, so both tiers treat them identically. As a result:
- Tier 1's default (no body configured) now also matches zero-length content, where previously only `Content == null` matched.
- The shared `IHttpMockClient.WithRequestsFromResourceAsync` loader now treats an omitted `body` as `WithNoBody()`, consistent with Tier 1's native loader. Previously it matched any body, so Aspire stubs that omit `body` no longer match requests that have one; use `body: ^` to match any non-empty body.
- `WithAnyBody()` and `body: ^` require a non-empty body, regardless of its content; absent and zero-length bodies match `WithNoBody()` instead.
- On Aspire, WireMock.Net does not read `GET`/`HEAD` request bodies by default. Therefore `WithAnyBody()` cannot match those methods, and `WithNoBody()` cannot enforce the no-body constraint for rules restricted exclusively to `GET`/`HEAD`. Unrestricted or mixed-method `WithNoBody()` rules apply the no-body constraint.
- *Documentation:* README restructured into a front door with a capability matrix linking to example tests; detailed documentation moved to [`docs/`](./docs).

## v5.12.0
- *Enhancement:* Added `UnitTestEx.Aspire` (NET8+) to enable testing of [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) multi-host (inter-domain) testing alongside existing in-process `WebApplicationFactory`. The API surface between the two is consistent where appropriate.
- `HttpClient` mocking of external dependencies is supported via `AspireHttpMockClient`, which stubs a real, out-of-process [WireMock.Net](https://wiremock.org/dotnet/) server resource - see the README's [Aspire multi-host testing](./README.md#Aspire-multi-host-testing) section for the recommended, self-hosted (Docker-free) pattern.
- `HttpClient` mocking of external dependencies is supported via `AspireHttpMockClient`, which stubs a real, out-of-process [WireMock.Net](https://wiremock.org/dotnet/) server resource - see the README's [Aspire multi-host testing](./docs/aspire.md) section for the recommended, self-hosted (Docker-free) pattern.
- `AspireTesterBase<TAppHost, TSelf>.BeforeStart(Func<DistributedApplication, Task>)` registers a callback that runs after the `DistributedApplication` is built, but before any resource is started; this enables use cases such as running database migrations/seeding, or clearing a cache, against a pre-existing (e.g. `AddConnectionString`) resource before dependent project resources come online. `AspireTesterBase.GetConnectionStringAsync(DistributedApplication, string, CancellationToken)` resolves such a resource's connection string directly (Aspire's own `DistributedApplication.GetConnectionStringAsync` testing extension throws until the application has started, so cannot be used from within a `BeforeStart` callback).
- `AspireTesterBase<TAppHost, TSelf>.AfterStart(Func<DistributedApplication, Task>)` registers a callback that runs once all resources have started (immediately after `DistributedApplication.StartAsync()` returns) - the natural place for `Aspire.Hosting.WaitForResourceAsync(this DistributedApplication, ...)` calls, since resources are guaranteed to exist by this point. As with `BeforeStart`, a throwing callback aborts start-up and propagates the exception. `AspireTesterBase.OnHostStartUp()` is `sealed` - it is a single-host-oriented hook that does not apply to a multi-host `DistributedApplication`; use `BeforeStart`/`AfterStart` instead.
- `AspireTesterBase.WaitForResourceAsync(string, TimeSpan?)`/`WaitForResourceAsync(string[], TimeSpan?)` delegate to `Aspire.Hosting.WaitForResourceAsync(this DistributedApplication, ...)` extension methods, discoverable alongside `AddProject`/`WithEnvironment` etc. within an AppHost project.
- `Aspire.Hosting.HttpMock(this DistributedApplication, string, string?, JsonElementComparerOptions?)` exposes the same fluent `AspireHttpMockClient` stubbing API directly against a started `DistributedApplication`, with no `AspireTester` required - enabling an `AppHost.cs` itself to pre-seed default request/response stubs so an exploratory/manual run (outside of any test) does not fail against an un-stubbed external dependency; see the README's [Aspire multi-host testing](./README.md#Aspire-multi-host-testing) "Pre-seeding stubs from `AppHost.cs`" subsection.
- `Aspire.Hosting.HttpMock(this DistributedApplication, string, string?, JsonElementComparerOptions?)` exposes the same fluent `AspireHttpMockClient` stubbing API directly against a started `DistributedApplication`, with no `AspireTester` required - enabling an `AppHost.cs` itself to pre-seed default request/response stubs so an exploratory/manual run (outside of any test) does not fail against an un-stubbed external dependency; see the README's [Aspire multi-host testing](./docs/aspire.md) "Pre-seeding stubs from `AppHost.cs`" subsection.
- `AspireTesterBase<TAppHost, TSelf>.ErrorWhenLogContains(LogLevel, string[]?, string[]?)` registers a continuous, streaming check that immediately fails the test the moment any resource logs an entry at or above the specified `LogLevel` (default `Error`) - checked every time resource log activity is drained, i.e. by every `Checkpoint`/`Delay`/`Http` call, not a single end-of-test scan. This is **enabled by default** at `LogLevel.Error` without ever needing to call it - pass `LogLevel.None` to opt out entirely for a test that deliberately induces a resource-level error/failure log as its subject. Accepts `*`/`?` wildcard `exclude` (known/expected noise to suppress; always wins) and `include` (narrows rather than widens what is checked) patterns, matched case-insensitively; each call fully replaces the prior configuration rather than merging with it. Since only a *drained* log entry is checked, a trailing `Test.Checkpoint("Final log check.")` is recommended at the end of a test relying on this to also catch background/hosted-service-only activity that no other call happens to drain.
- `AspireTesterBase<TAppHost, TSelf>.AssertLogContains(string, string?)`/`AssertLogNotContains(string, string?)` provide an immediate, one-shot positive/negative assertion (a Tier 2/3 equivalent of Tier 1's `ExpectLogContains`/`ExpectLogNotContains`) against **every** resource log entry captured for the lifetime of the test so far - across all resources, or scoped to one via the optional resource name - regardless of whether it has already been *drained* (unlike `ErrorWhenLogContains`'s `exclude`/`include`, which only ever narrow what counts as a *violation* of its own streaming check and never confirm any text was actually seen). Matched using the same `*`/`?` wildcard, case-insensitive "contains" semantics.
- `AspireTesterBase<TAppHost, TSelf>.ResetLogs()` discards every resource log entry captured so far, across all resources, along with the drain watermarks used by `Checkpoint`/`Delay` - relevant only where an `AspireTester`/`DistributedApplication` host is deliberately reused across multiple tests (e.g. via a shared fixture), so a later test's `ErrorWhenLogContains`/`AssertLogContains`/`AssertLogNotContains` checks are not tripped up (or falsely satisfied) by an earlier test's log activity. Deliberately explicit rather than automatic, since Tier 2/3 has no single per-test Act boundary to hang an automatic reset off, and background/hosted-service activity occurring between calls is exactly what this capture exists to surface.
Expand Down
4 changes: 2 additions & 2 deletions Common.targets
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>5.12.0</Version>
<LangVersion>preview</LangVersion>
<Version>6.0.0</Version>
<LangVersion>latest</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
<Copyright>Avanade (c)</Copyright>
Expand Down
Loading
Loading