UnitTestEx provides .NET testing extensions for MSTest, NUnit and xUnit. It hosts your real application (API, Azure Function, or any DI-composed code) inside the test, lets you replace or mock its dependencies, and gives you fluent Arrange, Act, Assert testing with captured logging and detailed JSON-diff assertions.
v6 targets .NET 8, 9, 10 and (from v6.2) 11, and the Azure Functions v4 isolated worker model only. Upgrading? See Migrating from v5 to v6.
The build status is with the NuGet package status as follows, including links to the underlying source code and documentation:
| Project/Package | Description | Source | Docs |
|---|---|---|---|
UnitTestEx |
Core API and DI-composed service testers, with HTTP-client mocking, dependency overrides, fluent assertions and captured logging. Use with a test-framework integration package. | Source | API testing, Generic testing, HTTP mocking, DI/configuration, Assertions/logging, Set-up/extensibility |
UnitTestEx.Azure.Functions |
Tests isolated-worker Functions using their normal application startup, scoped services and worker middleware. Invokes function methods without starting the Azure Functions worker/runtime. | Source | Azure Functions, DI/configuration, Assertions/logging, Set-up/extensibility |
UnitTestEx.Azure.Functions.Durable |
Tests real orchestration bodies through the in-memory Durable Task SDK host, with activity mocks, fluent assertions and diagnostics. Also includes a separate method-level virtual-time context; neither mode exercises the Azure Functions worker/runtime. | Source | Durable testing, Support matrix, Design/implementation |
UnitTestEx.Aspire |
Runs multi-service tests against a real .NET Aspire distributed application. Provides resource readiness checks, HTTP helpers, captured resource logs and external HTTP mocking. | Source | Aspire testing |
UnitTestEx.MSTest |
Connects UnitTestEx assertions and test output to MSTest. Use alongside the core or specialized tester packages. | Source | Set-up/extensibility, Assertions/logging |
UnitTestEx.NUnit |
Connects UnitTestEx assertions and test output to NUnit. Use alongside the core or specialized tester packages. | Source | Set-up/extensibility, Assertions/logging |
UnitTestEx.Xunit |
Connects UnitTestEx assertions and test output to xUnit, including its output-helper integration. Use alongside the core or specialized tester packages. | Source | Set-up/extensibility, Assertions/logging |
The change log details all key changes per published version.
| Testing... | Use | Package(s) | Hosts | Docs |
|---|---|---|---|---|
| An ASP.NET Core API (controllers or minimal APIs) | ApiTester |
UnitTestEx + framework package |
WebApplicationFactory, full HTTP pipeline |
API testing |
| An Azure Function (isolated worker) | FunctionTester |
UnitTestEx.Azure.Functions |
Your real Program.cs host, built but not started; the function is invoked directly |
Azure Functions |
| Durable orchestration bodies with replay, activities and children | FunctionTester.DurableTask() |
UnitTestEx.Azure.Functions.Durable (preview) |
In-memory Durable Task SDK host; no Functions worker or backend | Durable testing |
| Durable timer/event branches with instant time advancement | VirtualTimeOrchestrationContext |
UnitTestEx.Azure.Functions.Durable (preview) |
Method-level context; no engine history/replay, activities or children | Timers and virtual time |
| Any DI-composed class or service | GenericTester |
UnitTestEx |
Lightweight Generic Host | Generic testing |
| Multiple real services together | AspireTester |
UnitTestEx.Aspire |
.NET Aspire DistributedApplication |
Aspire |
| Durable Functions worker, bindings and backend interactions | AspireTester |
UnitTestEx.Aspire |
Core Tools, isolated worker and a backend/emulator; requires Core Tools and containers for the repository suite | Real Functions integration |
The framework package (UnitTestEx.NUnit, UnitTestEx.MSTest or UnitTestEx.Xunit) wires output and assertions into your test framework.
Functions proof boundary:
FunctionTesterproves invocation behavior, not trigger indexing, binding conversion, broker delivery or final runtime HTTP behavior. Use Aspire/Core Tools with the backing service or emulator for those checks. Posting a BlobCreated webhook simulates notification delivery; it does not test Azure Event Grid. Start with the Functions guide and runnable Blob example.
API: send a real HTTP request through the pipeline, with a downstream HttpClient mocked.
var mcf = MockHttpClientFactory.Create();
mcf.CreateClient("XXX", new Uri("https://somesys"))
.Request(HttpMethod.Get, "products/abc").Respond.WithJson(new { id = "Abc", description = "A blue carrot" });
using var test = ApiTester.Create<Startup>();
test.ReplaceHttpClientFactory(mcf)
.Controller<ProductController>()
.Run(c => c.Get("abc"))
.AssertOK()
.AssertValue(new { id = "Abc", description = "A blue carrot" });Azure Function: Program.cs runs as-is, with no test-only structure needed.
using var test = FunctionTester.Create<ProductFunction>();
test.ReplaceHttpClientFactory(mcf)
.HttpTrigger<ProductFunction>()
.Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc"))
.AssertOK()
.AssertValue(new { id = "Abc", description = "A blue carrot" });
test.ServiceBusTrigger<ServiceBusFunction>()
.Run(f => f.Run2(test.CreateServiceBusMessageFromValue(new Person { FirstName = "Bob", LastName = "Smith" })))
.AssertSuccess();Durable orchestration (preview): execute real fan-out/fan-in through the SDK host, with input-matched activity responses released in reverse order. This uses the repository's sample types; item processing and the finalization child are substituted, and no Functions worker or backend is started.
using UnitTestEx;
using UnitTestEx.Azure.Functions;
using UnitTestEx.Azure.Functions.Durable.TestHost;
using UnitTestEx.DurableFunction;
using var processItems = ActivityMock.ByMatch(
[
ActivityResponse.Match<WorkItem, ItemResult>(item => item.Id == "A", item => new ItemResult(item.Id, item.Amount * 2m)).Pending(),
ActivityResponse.Match<WorkItem, ItemResult>(item => item.Id == "B", item => new ItemResult(item.Id, item.Amount * 2m)).Pending(),
ActivityResponse.Match<WorkItem, ItemResult>(item => item.Id == "C", item => new ItemResult(item.Id, item.Amount * 2m)).After(TimeSpan.FromMilliseconds(100))
]);
using var functions = FunctionTester.Create<WorkflowActivities>();
var request = new BatchRequest(" batch-7 ", [new("A", 10m), new("B", 20m), new("C", 30m)]);
await using var test = await functions.DurableTask()
.Orchestrator<BatchRequest, BatchResult>(nameof(BatchOrchestration.ProcessBatch), (context, _) => BatchOrchestration.ProcessBatch(context))
.Activity<string, string>(nameof(ActivityTransforms.NormalizeReference), (_, reference) => ActivityTransforms.NormalizeReference(reference))
.Activity<WorkItem, ItemResult>(nameof(WorkflowActivities.ProcessItem), processItems.InvokeAsync)
.Activity<ItemResult[], decimal>(nameof(ActivityTransforms.TotalItems), (_, results) => ActivityTransforms.TotalItems(results))
.SubOrchestrator<BatchResult, BatchResult>(nameof(BatchOrchestration.FinalizeBatch), (_, input) => Task.FromResult(input))
.StartAsync(request);
await processItems.WaitForAllInvocationsAsync();
await processItems[2].WaitForCompletionAsync(TimeSpan.FromSeconds(5));
await processItems[1].ReleaseAsync(TimeSpan.FromSeconds(5));
await processItems[0].ReleaseAsync(TimeSpan.FromSeconds(5));
var expected = new BatchResult("BATCH-7", 120m, [new("A", 20m), new("B", 40m), new("C", 60m)]);
await test.WaitForCompletionAsync(TimeSpan.FromSeconds(10), result => result.AssertCompleted().AssertOutput(expected));
processItems.Verify();Fan-in retains A/B/C input order despite C/B/A handler completion. The activity delay is real time, not a virtual-clock advance. See the complete showcase for completion-order assertions and activity-count verification, or use the real-worker integration tier to exercise the worker, extension and backend.
Generic: any service, with expectations declared up front.
using var test = GenericTester.Create<EntryPoint>();
test.ExpectException().Type<DivideByZeroException>()
.Run<Gin>(gin => gin.Stir());Each row links to the docs and to a test that shows it working.
- API testing
- Azure Functions testing, including limitations
- Durable Functions testing (preview), including a commented fan-out/fan-in showcase
- Generic testing
- Dependency injection and configuration
- HTTP client mocking
- Assertions, expectations and logging
- Test set-up and extensibility
- Aspire multi-host testing
- Migrating from v5 to v6
The tests in this repository are the reference examples. Core and Aspire examples are grouped by test framework:
- UnitTestEx.NUnit.Test
- UnitTestEx.MSTest.Test
- UnitTestEx.Xunit.Test
- UnitTestEx.Aspire.Xunit.Test, UnitTestEx.Aspire.NUnit.Test, UnitTestEx.Aspire.MSTest.Test
Durable reference examples use NUnit:
- SDK-host and method-level context suite - .NET 10/11; no Core Tools, containers or backend.
- Real Functions worker/backend integration suite - .NET 10; requires Core Tools and a container runtime.
The optional UnitTestEx.Azure.Functions.Durable preview package combines the SDK-host facade and the separate method-level virtual-time context. Its version follows Common.targets with -preview appended: 6.2.0-preview, alongside regular 6.2.0 UnitTestEx packages. Libraries target .NET 8/9/10/11; the dedicated SDK-host/context suite targets .NET 10/11, while real-worker integration targets .NET 10. The SDK host pins Microsoft's preview Microsoft.DurableTask.InProcessTestHost 0.2.3-preview.1; preview APIs may change between releases.
The Durable guide shows FunctionTester.DurableTask(), ActivityMock, assertions and diagnostics. The runnable showcase runs the real batch orchestration with controlled activity responses; see the guide's support matrix for APIs and limitations.
| Construct | Test path | Behavior | Limitations |
|---|---|---|---|
| Activity functions | Existing FunctionTester.Trigger<T>() |
Real static/instance method invocation, DI, logs and original exceptions | No Functions trigger binding, runtime delivery or Durable failure translation. |
Static/instance [Function] orchestration methods |
In-memory SDK host | Explicit delegates execute real bodies with SDK serialization, replay and failure details | No automatic discovery, Functions worker or extension input conversion. |
Typed SDK TaskOrchestrator<TIn,TOut> |
Fluent SDK host or low-level registry | Explicit root/child instance registration with typed input/output, managed diagnostics and nested failures | Caller owns instances and dependencies; no automatic discovery or Functions class-based worker integration. |
| Activity calls and fan-out/fan-in | SDK host and ActivityMock |
Input-matched/ordered responses, controlled completion, input-order fan-in and opt-in invocation verification | Mocked activity bodies are not tested; handler completion is not engine result-processing order. |
| Sub-orchestrations | SDK host | Real/delegate child registration; selected success, failure and retry scenarios | Not general cancellation propagation or Functions child delivery. |
| Durable timers | SDK host or method-level virtual context | Host timers use real time with a warning; the narrow context advances timer/event logic instantly | No host virtual clock. Method-level context has no engine history/replay and rejects activities/children. |
| External events/custom status | SDK host | Selected buffered-event, event-before-timeout and in-flight status scenarios | No Functions delivery/serializer parity or established same-deadline arbitration. |
| Replay | SDK host | SDK history replay and detection of divergent durable calls | Not a universal detector of invalid awaits or arbitrary I/O; virtual context never replays. |
| Entities | Outside the Durable tester's supported surface | Existing generic trigger invocation remains unchanged | No entity execution/state API through the host facade. |
| Client/start/status operations | SDK host/client | Schedule, bounded waits, typed events, metadata and output assertions | Not production DurableTaskClient, Functions starter triggers or HTTP status endpoints. |
ContinueAsNew |
SDK host | New generations with serialized input; final output and activity counts across generations | Buffered-event preservation is unsupported by the pinned host (upstream issue); no method-level virtual-context support or host virtual clock. |
| Custom retry handlers | SDK host | SDK synchronous/asynchronous handlers for activities and children, including failure details and attempt limits | Handler code must be deterministic; timer waits remain wall-clock. |
| Durable HTTP | Outside the Durable tester's supported surface | Use a Functions runtime integration for extension-specific HTTP behavior | No built-in SDK-host HTTP implementation or method-level context support. |
| Functions worker/backend integration | Separate Aspire/Core Tools/Azurite suite | Real batch activities/child, failure recovery, approval-event delivery and continuation with buffered-event preservation/discard | Requires Core Tools and containers; local emulator integration excludes deployed Azure authentication, scale and reliability. |
These other Avanade repositories leverage UnitTestEx to provide unit testing capabilities:
- CoreEx - Enriched capabilities for building business services by extending the core capabilities of .NET.
- Beef - Business Entity Execution Framework to enable industralisation of API development.
UnitTestEx is open source under the MIT license and is free for commercial use.
One of the easiest ways to contribute is to participate in discussions on GitHub issues. You can also contribute by submitting pull requests (PR) with code changes. Contributions are welcome. See information on contributing, as well as our code of conduct.
See our security disclosure policy.
Avanade is the leading provider of innovative digital and cloud services, business solutions and design-led experiences on the Microsoft ecosystem, and the power behind the Accenture Microsoft Business Group.
