From 94b96fbd634618c12e391b97b30bc6e865756b98 Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Wed, 21 Oct 2026 18:26:59 -0700 Subject: [PATCH] Document public APIs with examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 6 +- docs/AutoMocker.md | 242 ++++++++++++++++++++ docs/CompilerIntegration.md | 125 ++++++++++ docs/Diagnostics.md | 105 +++++++++ docs/GeneratedExtensions.md | 92 ++++++++ docs/HttpClient.md | 433 +++++++++++++++-------------------- docs/Moq.AutoMock.md | 27 +-- docs/README.md | 17 ++ docs/Resolvers.md | 162 +++++++++++++ docs/SetupAndVerification.md | 135 +++++++++++ docs/SourceGenerators.md | 4 +- 11 files changed, 1081 insertions(+), 267 deletions(-) create mode 100644 docs/AutoMocker.md create mode 100644 docs/CompilerIntegration.md create mode 100644 docs/Diagnostics.md create mode 100644 docs/GeneratedExtensions.md create mode 100644 docs/README.md create mode 100644 docs/Resolvers.md create mode 100644 docs/SetupAndVerification.md diff --git a/README.md b/README.md index 462017ef..44015443 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ Documentation For more detailed documentation, including information about the built-in source generators that can automatically generate test boilerplate code, see the [docs folder](docs/). -- [AutoMocker API Reference](docs/Moq.AutoMock.md) +- [Public API documentation](docs/README.md) - Concise reference with examples and overloads grouped by method +- [AutoMocker](docs/AutoMocker.md) +- [Setup and verification](docs/SetupAndVerification.md) +- [Resolvers and contexts](docs/Resolvers.md) +- [Diagnostics and constructor tests](docs/Diagnostics.md) - [HttpClient Support](docs/HttpClient.md) - Mock HTTP dependencies with fluent setup and verification - [Source Generators](docs/SourceGenerators.md) - Learn about automatic code generation for constructor tests, options configuration, logging, and more diff --git a/docs/AutoMocker.md b/docs/AutoMocker.md new file mode 100644 index 00000000..a4bff287 --- /dev/null +++ b/docs/AutoMocker.md @@ -0,0 +1,242 @@ +# AutoMocker + +Namespace: `Moq.AutoMock`. [Documentation index](README.md). + +## Constructors and properties + +```csharp +AutoMocker() +AutoMocker(MockBehavior mockBehavior) +AutoMocker(MockBehavior mockBehavior, DefaultValue defaultValue) +AutoMocker(MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase) +AutoMocker( + MockBehavior mockBehavior, DefaultValue defaultValue, + DefaultValueProvider? defaultValueProvider, bool callBase) +``` + +Defaults: `MockBehavior.Default` (loose), `DefaultValue.Empty`, no custom provider, `callBase: false`. These settings apply to automatically created mocks, not explicitly supplied mocks. + +| Property | Type | Meaning | +| --- | --- | --- | +| `MockBehavior` | `MockBehavior` | Behavior of created mocks | +| `DefaultValue` | `DefaultValue` | Default return-value strategy for loose mocks | +| `DefaultValueProvider` | `DefaultValueProvider?` | Optional custom return-value provider | +| `CallBase` | `bool` | Call real virtual implementations when no setup matches | +| `Resolvers` | `IList` | Mutable, ordered [resolution pipeline](Resolvers.md) | +| `ResolvedObjects` | `IReadOnlyDictionary` | Snapshot of cached values; mocked entries contain `Mock` wrappers, not their `.Object` | + +The setting properties are read-only. + +```csharp +var mocker = new AutoMocker(MockBehavior.Strict, DefaultValue.Empty); +Mock store = mocker.GetMock(); +object? cachedMock = mocker.ResolvedObjects[typeof(IStore)]; +bool callsBase = mocker.CallBase; +``` + +## CreateInstance + +```csharp +T CreateInstance() where T : class +T CreateInstance(bool enablePrivate) where T : class +object CreateInstance(Type type) +object CreateInstance(Type type, bool enablePrivate) +``` + +Constructs a real object using the longest resolvable constructor. Dependencies come from registrations or resolvers. Each call creates a new root object; dependencies are normally cached. `enablePrivate` defaults to `false`; `true` includes non-public constructors, but does not bypass proxy accessibility restrictions. + +Throws `ArgumentNullException` for a null type, or [ObjectCreationException](Diagnostics.md#objectcreationexception) when no constructor can be used. Exceptions thrown by the constructor propagate. + +```csharp +var service = mocker.CreateInstance(); +var another = (OrderService)mocker.CreateInstance(typeof(OrderService)); +var internalService = mocker.CreateInstance(enablePrivate: true); +``` + +## Get + +```csharp +TService Get() +TService Get(bool enablePrivate) +object Get(Type serviceType) +object Get(Type serviceType, bool enablePrivate) +``` + +Returns the registered or cached service object, otherwise resolves and caches one. Unlike `CreateInstance`, an unregistered class normally resolves to a mock's `.Object`, not a real instance. `enablePrivate` defaults to `false`. Registered nulls are returned as null (or `default(TService)` by generic overloads). + +Throws `ArgumentNullException` for a null type or `ArgumentException` if resolution fails. + +```csharp +IStore store = mocker.Get(); +object sameStore = mocker.Get(typeof(IStore)); +CancellationToken token = mocker.Get(); +``` + +### IServiceProvider.GetService + +`AutoMocker` explicitly implements `IServiceProvider.GetService(Type)`. It uses the same pipeline and cache, returning null when resolution fails rather than throwing the unresolved-service `ArgumentException`. + +```csharp +IServiceProvider provider = mocker; +var store = (IStore?)provider.GetService(typeof(IStore)); +``` + +## GetMock + +```csharp +Mock GetMock() where TService : class +Mock GetMock(bool enablePrivate) where TService : class +Mock GetMock(Type serviceType) +Mock GetMock(Type serviceType, bool enablePrivate) +``` + +Returns or creates the cached `Mock` wrapper. `enablePrivate` defaults to `false`. Throws `ArgumentException` if the service is not stored as a mock and `ArgumentNullException` for a null type. Register supplied mocks with `Use(Mock)`, rather than only registering their `.Object`. + +```csharp +Mock store = mocker.GetMock(); +store.Setup(x => x.Find(42)).Returns("item"); +IStore dependency = store.Object; +``` + +## Use + +All overloads return the same `AutoMocker` for chaining. + +```csharp +AutoMocker Use(TService? service) +AutoMocker Use(Type type, object? service) +AutoMocker Use(Mock mockedService) where TService : class +AutoMocker Use(Expression> setup) where TService : class +AutoMocker Use(Func factory) where TService : class +AutoMocker Use(Func factory) where TService : class +AutoMocker Use() + where TService : class + where TImplementation : class, TService +``` + +Instance overloads register immediately, including null, replacing an existing different value. Registering the same instance twice under the same type throws `InvalidOperationException`; incompatible values throw `ArgumentException`. The expression overload uses `Mock.Of` setup syntax. + +Factories run on first resolution and cache their result. Existing cached values take precedence. `Use()` defers to `Get()`, so the implementation is normally mocked. Use `With` to construct a real implementation. + +```csharp +mocker.Use(new Store()) + .Use("test"); +mocker.Use(typeof(int), 42); +mocker.Use(new Mock()); +mocker.Use(x => x.Name == "test"); +mocker.Use(m => new OrderService(m.Get())); +mocker.Use(() => new Worker()); +mocker.Use(); +``` + +## With + +```csharp +TImplementation With() where TImplementation : class +TImplementation With() + where TImplementation : class, TService +object With(Type implementationType) +object With(Type serviceType, Type implementationType) +``` + +Constructs a real implementation immediately, registers it, and returns it. One-type overloads register under the implementation type; two-type overloads register only under the service type. + +```csharp +Store store = mocker.With(); +OrderService service = mocker.With(); +object clock = mocker.With(typeof(IClock), typeof(Clock)); +``` + +## CreateSelfMock + +```csharp +T CreateSelfMock() where T : class +T CreateSelfMock(bool enablePrivate) where T : class +T CreateSelfMock( + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + DefaultValueProvider? defaultValueProvider = null, + bool? callBase = null) where T : class +``` + +Creates a class proxy with constructor dependencies resolved by the container. The root is not registered. Virtual and abstract members can be configured through `Mock.Get(result)`; non-virtual members run normally. Unspecified settings inherit the container's settings. To run real virtual implementations when no setup matches, pass `callBase: true`. + +```csharp +var worker = mocker.CreateSelfMock(callBase: true); +Mock.Get(worker).Setup(x => x.Load()).Returns("test"); +``` + +## WithSelfMock + +```csharp +T WithSelfMock( + bool enablePrivate = false, MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + DefaultValueProvider? defaultValueProvider = null, bool? callBase = null) + where T : class +Mock WithSelfMock( + bool enablePrivate = false, MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + DefaultValueProvider? defaultValueProvider = null, bool? callBase = null) + where TImplementation : class, TService + where TService : class +object WithSelfMock( + Type implementationType, + bool enablePrivate = false, MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + DefaultValueProvider? defaultValueProvider = null, bool? callBase = null) +object WithSelfMock( + Type serviceType, Type implementationType, + bool enablePrivate = false, MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + DefaultValueProvider? defaultValueProvider = null, bool? callBase = null) +``` + +Like `CreateSelfMock`, but registers the created proxy. Two-type overloads register it under both types; the service type must be an interface suitable for `Mock.As()`. Note the return difference: the two-type generic overload returns `Mock`; all other overloads return the service object. + +```csharp +Mock worker = mocker.WithSelfMock(callBase: true); +worker.Setup(x => x.Load()).Returns("test"); +Worker sameWorker = mocker.Get(); +``` + +## Combine + +```csharp +void Combine(Type type, params Type[] forwardTo) +void Combine() +// Generic overloads accept 2 through 11 type arguments. +``` + +Forwards all supplied types to the first type's mock. Additional types must be interfaces supported by Moq's `As()`. Call before accessing the mock's `.Object` or resolving consumers; Moq cannot add new interfaces after the proxy is initialized. The first service must resolve to a mock, not a real instance. + +```csharp +mocker.Combine(); +IReader reader = mocker.Get(); +IWriter writer = mocker.Get(); +bool same = ReferenceEquals(reader, writer); // true + +// Equivalent runtime-type form, on a separate container: +var runtimeMocker = new AutoMocker(); +runtimeMocker.Combine(typeof(IReader), typeof(IWriter)); +``` + +## AsDisposable + +```csharp +IDisposable AsDisposable() +``` + +Returns a cleanup handle for values in `ResolvedObjects` implementing `IDisposable`. Dispose it after exercising the test. Mocked entries are `Mock` wrappers, so their disposable `.Object` values are not disposed by this handle. It also excludes uncached roots from `CreateInstance` and does not perform asynchronous disposal. + +```csharp +var mocker = new AutoMocker(); +using var cleanup = mocker.AsDisposable(); +mocker.Use(new MemoryStream()); +var service = mocker.CreateInstance(); +``` + +## Setup, verification, and resolver insertion + +See [Setup and verification](SetupAndVerification.md) for all setup and verification overloads, and [Resolvers](Resolvers.md#pipeline) for `InsertResolverBefore` and `InsertResolverAfter`. diff --git a/docs/CompilerIntegration.md b/docs/CompilerIntegration.md new file mode 100644 index 00000000..18f7efed --- /dev/null +++ b/docs/CompilerIntegration.md @@ -0,0 +1,125 @@ +# Compiler integration + +Namespace/assembly: `Moq.AutoMocker.Generators`. [Documentation index](README.md). + +These public types support Roslyn hosts and generator tests. Normal test projects consume the [generated extensions](GeneratedExtensions.md) and attributes instead; the package loads the generators automatically. + +## Generators and analyzer + +Each generator is sealed, has a public parameterless constructor, implements `IIncrementalGenerator`, and exposes `void Initialize(IncrementalGeneratorInitializationContext context)`: + +| Type | Output | +| --- | --- | +| `UnitTestSourceGenerator` | Constructor null-argument tests | +| `OptionsExtensionSourceGenerator` | Options registration | +| `FakeLoggingExtensionSourceGenerator` | Fake logging registration | +| `FakeTimeProviderExtensionSourceGenerator` | Fake time registration | +| `ApplicationInsightsExtensionSourceGenerator` | Version-specific telemetry capture | +| `KeyedServicesExtensionSourceGenerator` | Keyed service registration and resolution | +| `MeterFactoryExtensionSourceGenerator` | Meter factory registration | +| `HttpClientFactoryResolverSourceGenerator` | Named HTTP client factory registration | + +Roslyn calls `Initialize`; do not invoke it yourself. + +```csharp +using Moq.AutoMocker.Generators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +IIncrementalGenerator generator = new UnitTestSourceGenerator(); +GeneratorDriver driver = CSharpGeneratorDriver.Create(generator.AsSourceGenerator()); +// compilation is a CSharpCompilation containing test sources and references. +driver = driver.RunGenerators(compilation); +GeneratorDriverRunResult result = driver.GetRunResult(); +``` + +`UseOptionsAnalyzer()` derives from `DiagnosticAnalyzer`. `SupportedDiagnostics` is an `ImmutableArray` containing `AMA0001`. `void Initialize(AnalysisContext context)` registers analysis of `Use(IOptions)` when `WithOptions` is available. + +```csharp +var analyzer = new UseOptionsAnalyzer(); +var descriptor = analyzer.SupportedDiagnostics[0]; +``` + +## Diagnostics + +`Diagnostics` contains static nested classes exposing `const string DiagnosticId` and a `Diagnostic Create(...)` factory: + +| Nested class | ID | Severity | Factory | +| --- | --- | --- | --- | +| `TestClassesMustBePartial` | `AMG0001` | Error | `Create(Location? location, string className)` | +| `MustReferenceAutoMock` | `AMG0002` | Warning | `Create()` | +| `MustSpecifyTargetType` | `AMG0003` | Error | `Create(Location? location, string className)` | +| `DuplicateTargetType` | `AMG0004` | Warning | `Create(Location? location, string targetTypeName)` | +| `PreferWithOptions` | `AMA0001` | Info | `Create(Location? location, string optionsTypeName)` | + +```csharp +Diagnostic diagnostic = Diagnostics.MustSpecifyTargetType.Create( + Location.None, "OrderServiceTests"); +string id = Diagnostics.MustSpecifyTargetType.DiagnosticId; +``` + +## Generator models + +`GeneratorTargetClass()`, `SutClass()`, `NullConstructorParameterTest()`, and `SyntaxReceiver()` have public parameterless constructors. + +| Type | Members | +| --- | --- | +| `GeneratorTargetClass` | Static get-only `Empty`; get/set `string? Namespace`, `string? TestClassName`, `SutClass? Sut`, `bool SkipNullableParameters`, `Location? AttributeLocation` | +| `SutClass` | Get/set `string? Name`, `string? FullName`; get-only mutable `List NullConstructorParameterTests` | +| `NullConstructorParameterTest` | Get/set `List? Parameters`, `int NullParameterIndex`, `string? NullTypeName`; get-only `string? NullParameterName` from the indexed parameter | +| `Parameter` | Constructor `Parameter(IParameterSymbol symbol)`; get-only `bool IsValueType`, `bool IsNullable`, `string Name`, `string ParameterType` | +| `SyntaxReceiver` | Get-only mutable `List TestClasses`, `List DiagnosticMessages`; `void OnVisitSyntaxNode(GeneratorSyntaxContext context)` | + +`Empty` is a shared mutable model; create a new model for independent state. `Parameter` snapshots the symbol's name, fully qualified type, value-type status, and nullable annotation/null default. `NullParameterIndex` must be valid when reading `NullParameterName` from a non-null parameter list. + +```csharp +var target = new GeneratorTargetClass +{ + Namespace = "Example.Tests", + TestClassName = "OrderServiceTests", + SkipNullableParameters = true, + Sut = new SutClass { Name = "OrderService", FullName = "global::Example.OrderService" } +}; +// parameterSymbol is a constructor's IParameterSymbol. +var parameter = new Parameter(parameterSymbol); +target.Sut.NullConstructorParameterTests.Add(new NullConstructorParameterTest +{ + Parameters = new List { parameter }, + NullParameterIndex = 0, + NullTypeName = parameter.ParameterType +}); +``` + +`SyntaxReceiver` implements `ISyntaxContextReceiver`; Roslyn supplies syntax contexts to `OnVisitSyntaxNode`, which collects attributed test classes and diagnostics. + +```csharp +var receiver = new SyntaxReceiver(); +// In a Roslyn host callback: +receiver.OnVisitSyntaxNode(context); +var classes = receiver.TestClasses; +var diagnostics = receiver.DiagnosticMessages; +``` + +`TargetTestingFramework` values are `Unknown` (0), `MSTest` (1), `XUnit` (2), `NUnit` (3), and `TUnit` (4). + +```csharp +TargetTestingFramework framework = TargetTestingFramework.XUnit; +``` + +## AutoMock constants + +`AutoMock` exposes these `const string` fields: + +| Field | Value | +| --- | --- | +| `ConstructorTestsAttribute` | `"ConstructorTestsAttribute"` | +| `TargetTypePropertyName` | `"TargetType"` | +| `AssemblyName` | `"Moq.AutoMock"` | +| `NuGetPackageName` | `"Moq.AutoMock"` | +| `IgnoreNullableParametersEnumValue` | `"IgnoreNullableParameters"` | + +```csharp +string runtimeAssemblyName = AutoMock.AssemblyName; +``` + +The repository-only `Generators.CombineGenerator` also has a parameterless constructor and `Initialize(IncrementalGeneratorInitializationContext)`. It generates the runtime library's `Combine` overloads (2-11 type arguments), not consuming-project extensions. diff --git a/docs/Diagnostics.md b/docs/Diagnostics.md new file mode 100644 index 00000000..e74e06ba --- /dev/null +++ b/docs/Diagnostics.md @@ -0,0 +1,105 @@ +# Diagnostics and constructor tests + +Namespace: `Moq.AutoMock`. [Documentation index](README.md). + +## ObjectGraphContext + +Tracks one construction graph for [resolvers](Resolvers.md). + +```csharp +ObjectGraphContext(bool enablePrivate) +ObjectGraphContext(bool enablePrivate, bool isMockCreation) +ObjectGraphContext(ObjectGraphContext context) +ObjectGraphContext(ObjectGraphContext context, ParameterInfo targetParameter) +void AddDiagnosticMessage(string message) +``` + +`enablePrivate` includes non-public instance constructors. Copy constructors copy `BindingFlags` and clone `VisitedTypes`; they do not copy diagnostics or `IsMockCreation`. The parameter overload also sets `ParameterInfo`. Null copy sources or target parameters throw `ArgumentNullException`. + +| Property | Type | Meaning | +| --- | --- | --- | +| `BindingFlags` | `BindingFlags` | Constructor search flags | +| `VisitedTypes` | `HashSet` | Mutable cycle-detection state | +| `IsMockCreation` | `bool` | Explicit mock creation request | +| `ParameterInfo` | `ParameterInfo?` | Constructor parameter currently being resolved, otherwise null | +| `DiagnosticMessages` | `IReadOnlyList` | Messages recorded during resolution | + +```csharp +var graph = new ObjectGraphContext(enablePrivate: false); +graph.AddDiagnosticMessage("Custom resolver could not provide the service."); +var copy = new ObjectGraphContext(graph); +var flags = copy.BindingFlags; +foreach (string message in graph.DiagnosticMessages) + Console.WriteLine(message); +``` + +## ObjectCreationException + +Thrown when a root instance or self-mock cannot be constructed. `DiagnosticMessages` describes failed resolution attempts. + +```csharp +ObjectCreationException(IReadOnlyList diagnosticMessages) +ObjectCreationException(string message, IReadOnlyList diagnosticMessages) +ObjectCreationException( + string message, Exception innerException, IReadOnlyList diagnosticMessages) +protected ObjectCreationException(SerializationInfo info, StreamingContext context) +``` + +The public constructors require non-null diagnostics. The protected overload supports legacy exception serialization. + +```csharp +try +{ + var service = mocker.CreateInstance(); +} +catch (ObjectCreationException exception) +{ + Console.WriteLine(exception.Message); + foreach (string message in exception.DiagnosticMessages) + Console.WriteLine(message); + throw; +} +``` + +## ConstructorTestsAttribute + +Marks a partial test class for generated constructor null-argument tests. Apply once per class. + +```csharp +ConstructorTestsAttribute() +ConstructorTestsAttribute(Type targetType) +ConstructorTestsAttribute(Type targetType, TestGenerationBehavior behavior) +Type? TargetType { get; set; } +TestGenerationBehavior Behavior { get; set; } +``` + +Set the target through the constructor or `TargetType`. `Behavior` defaults to `TestGenerationBehavior.Default`. + +```csharp +using Moq.AutoMock; + +[ConstructorTests(typeof(OrderService), TestGenerationBehavior.IgnoreNullableParameters)] +public partial class OrderServiceTests +{ +} +``` + +Equivalent named-property form: + +```csharp +[ConstructorTests( + TargetType = typeof(OrderService), + Behavior = TestGenerationBehavior.IgnoreNullableParameters)] +public partial class OrderServiceTests +{ +} +``` + +## TestGenerationBehavior + +| Value | Meaning | +| --- | --- | +| `Default` (`0`) | Generate null checks for all eligible parameters | +| `IgnoreNullableParameters` (`1`) | Skip nullable references, nullable value types, and parameters with a null default | + +See [Unit Test Generator](SourceGenerators/UnitTestGenerator.md) for supported test frameworks, eligibility rules, and customization hooks. diff --git a/docs/GeneratedExtensions.md b/docs/GeneratedExtensions.md new file mode 100644 index 00000000..7e406df7 --- /dev/null +++ b/docs/GeneratedExtensions.md @@ -0,0 +1,92 @@ +# Generated extensions + +Namespace: `Moq.AutoMock`. [Documentation index](README.md). + +These APIs are generated into the consuming test project only when their prerequisite packages are referenced. Call them before resolving affected dependencies. Extension receivers (`this AutoMocker mocker`) are omitted below. + +| Method | Required reference | Result / details | +| --- | --- | --- | +| `AutoMocker WithOptions(Action? configure = null) where TClass : class` | `Microsoft.Extensions.Options` | Registers options infrastructure and the configured options value; [guide](SourceGenerators/OptionsExtensionGenerator.md) | +| `AutoMocker WithFakeLogging()` | `Microsoft.Extensions.Diagnostics.Testing` | Resolves logging services through `FakeLoggerProvider`; [guide](SourceGenerators/FakeLoggingExtensionGenerator.md) | +| `AutoMocker WithFakeTimeProvider()` | `Microsoft.Extensions.TimeProvider.Testing` | Registers one `FakeTimeProvider` as itself and `TimeProvider`; [guide](SourceGenerators/FakeTimeProviderExtensionGenerator.md) | +| `AutoMocker WithMeterFactory()` | `System.Diagnostics.DiagnosticSource` 10+ | Registers a real `IMeterFactory` that tracks created meters; [guide](SourceGenerators/MeterFactoryExtensionGenerator.md) | +| `AutoMocker WithHttpClientFactory()` | `Microsoft.Extensions.Http` | Registers `IHttpClientFactory`, caching one client per name with a shared testable handler; [guide](SourceGenerators/HttpClientFactoryExtensionGenerator.md) | +| `AutoMocker WithApplicationInsights()` | `Microsoft.ApplicationInsights` | Configures captured telemetry rather than network export; [guide](SourceGenerators/ApplicationInsightsExtensionGenerator.md) | + +All methods in this table return the same container for chaining. + +```csharp +var mocker = new AutoMocker() + .WithOptions(settings => settings.Name = "test") + .WithFakeLogging() + .WithFakeTimeProvider() + .WithMeterFactory() + .WithHttpClientFactory(); +``` + +Each call requires the corresponding reference. See the linked guides for package namespaces and examples reading logs, controlling time, recording metrics, and configuring clients. + +## WithKeyedService overloads + +Requires `Microsoft.Extensions.DependencyInjection.Abstractions` 8+. + +```csharp +void WithKeyedService(TService service, object? key) + where TService : class +void WithKeyedService(TImplementation service, object? key) + where TImplementation : class, TService +void WithKeyedService(object? key) where TService : class +void WithKeyedService(object? key) + where TImplementation : class, TService +``` + +Instance overloads register immediately. Key-only overloads lazily construct and cache the implementation using `CreateInstance`; the one-type form therefore needs a constructible class. Methods return `void`, not the container. Services resolve through `IKeyedServiceProvider` or `[FromKeyedServices]` constructor parameters. + +```csharp +mocker.WithKeyedService(new Store(), "primary"); +mocker.WithKeyedService("secondary"); +mocker.WithKeyedService("concrete"); +``` + +See [keyed services](SourceGenerators/KeyedServicesExtensionGenerator.md) for retrieval and constructor injection. + +## Application Insights accessors + +Call `WithApplicationInsights()` first. Application Insights 2.x generates: + +```csharp +IReadOnlyList GetSentTelemetry() +``` + +```csharp +mocker.WithApplicationInsights(); +mocker.Get().TrackEvent("test"); +var sent = mocker.GetSentTelemetry(); +``` + +The generated internal partial `AutoMockerApplicationInsightsExtensions` also contains public nested `FakeTelemetryChannel()`: `DeveloperMode` (`bool?`, get/set), `EndpointAddress` (`string?`, get/set), and `SentItems` (`IReadOnlyList`, get). `Send(ITelemetry)` records non-null items; `Flush()` and `Dispose()` are no-ops. Prefer the accessor above. + +Application Insights 3.x with `OpenTelemetry.Exporter.InMemory` generates: + +```csharp +IList GetApplicationInsightsLogRecords() +IList GetApplicationInsightsMetrics() +IList GetApplicationInsightsActivities() +``` + +```csharp +mocker.WithApplicationInsights(); +// Exercise the service, then flush before reading metrics. +mocker.Get().Flush(); +var logs = mocker.GetApplicationInsightsLogRecords(); +var metrics = mocker.GetApplicationInsightsMetrics(); +var activities = mocker.GetApplicationInsightsActivities(); +``` + +Without the in-memory exporter, the 3.x generator emits an obsolete `WithApplicationInsights()` stub that throws `InvalidOperationException`. + +## Configuration and visibility + +Most extension classes are internal partials. `AutoMockerHttpClientFactoryExtensions` is public. Avoid enabling the same generated extensions in mutually visible test projects; see [generator configuration](SourceGenerators.md#disabling-source-generators). + +Constructor-test attributes are covered in [Diagnostics](Diagnostics.md#constructortestsattribute); Roslyn host entry points are covered in [Compiler integration](CompilerIntegration.md). diff --git a/docs/HttpClient.md b/docs/HttpClient.md index 6ec069a4..cefa3073 100644 --- a/docs/HttpClient.md +++ b/docs/HttpClient.md @@ -1,320 +1,263 @@ -# HttpClient Support +# HTTP -Moq.AutoMocker provides built-in support for testing code that depends on `HttpClient`. When your class accepts an `HttpClient` constructor parameter, AutoMocker automatically resolves it with a mocked `HttpMessageHandler` — no manual setup required. +Extensions: `Moq.AutoMock.MockHttpMessageHandlerExtensions`. Handler contract and matchers: `Moq.AutoMock.Http.IHttpMessageHandler`, `Moq.AutoMock.Http.RequestMatcher`. [Documentation index](README.md). -## Features - -- **Automatic resolution** of `HttpClient` dependencies via `HttpClientResolver` -- **Named `IHttpClientFactory` support** with one testable client cached per client name -- **Verb-specific setup methods** for GET, POST, PUT, PATCH, DELETE, and HEAD -- **Flexible request matching** by URI, expression predicate, or custom logic -- **Fluent response builders** for string, byte array, stream, and custom content types -- **Sequential responses** for testing retry logic and multi-call scenarios -- **Verification helpers** to assert that specific HTTP requests were made -- **Custom response configuration** including headers, status codes, and media types -- **Default behavior** returns HTTP 200 OK with empty content (loose mock) - -## How It Works - -AutoMocker registers an `HttpClientResolver` that intercepts `HttpClient` dependencies. When `CreateInstance()` encounters an `HttpClient` parameter, the resolver: - -1. Creates (or retrieves) a `Mock` -2. Configures a default value provider that returns HTTP 200 OK responses -3. Wraps the handler in a new `HttpClient` instance - -This means you can immediately create and test classes that use `HttpClient` without any explicit setup. The extension methods on `AutoMocker` and `Mock` then let you customize request matching and response behavior. - -When the consuming project references `Microsoft.Extensions.Http`, the source generator adds `WithHttpClientFactory()`. Call this generated extension to enable testable `IHttpClientFactory` resolution. Calling `CreateClient` with the same name returns the same `HttpClient` instance; different names receive separate instances. All named clients use the same testable handler, so the existing setup and verification helpers apply: +`AutoMocker` resolves `HttpClient` with a mocked `HttpMessageHandler`. Unconfigured requests return HTTP 200 with empty content in loose mode; strict mode requires explicit setups. ```csharp -var mocker = new AutoMocker() - .WithHttpClientFactory(); -var factory = mocker.Get(); -var catalogClient = factory.CreateClient("catalog"); -var sameCatalogClient = factory.CreateClient("catalog"); -var ordersClient = factory.CreateClient("orders"); - -Assert.AreSame(catalogClient, sameCatalogClient); -Assert.AreNotSame(catalogClient, ordersClient); -``` - -See the [HTTP Client Factory Extension Generator](SourceGenerators/HttpClientFactoryExtensionGenerator.md) page for advanced usage and details on disabling this generator via the `EnableMoqAutoMockerHttpClientFactoryGenerator` MSBuild property. +using System.Net; +using System.Net.Http; +using Moq; +using Moq.AutoMock; +using Moq.AutoMock.Http; -## Usage +var mocker = new AutoMocker(); +mocker.SetupHttpGet("/users") + .ReturnsHttpResponse(HttpStatusCode.OK, "[]", mediaType: "application/json"); -### Basic Setup +using var response = await mocker.Get() + .GetAsync("https://example.test/users"); +mocker.VerifyHttpGet("/users", Times.Once()); +``` -Create an `AutoMocker`, set up an HTTP response, and test your service: +## Verb-specific setup -```csharp -[TestMethod] -public async Task GetUsers_ReturnsUserList() -{ - // Arrange - AutoMocker mocker = new(); - mocker.SetupHttpGet("/users") - .ReturnsHttpResponse(HttpStatusCode.OK, """{"users": []}"""); - - var service = mocker.CreateInstance(); - - // Act - var response = await service.GetUsersAsync(); - - // Assert - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); -} -``` +Each method below has overloads on **both** `AutoMocker` and `Mock`. All return `ISetup>`. -Without any setup, requests return HTTP 200 OK with empty content by default. +Receivers are omitted: -### Setup by URL +| Method | Overloads | +| --- | --- | +| `SetupHttpGet` | `(string? requestUri = null)`; `(Expression> match)` | +| `SetupHttpPost` | `(string? requestUri = null, string? content = null)`; `(Expression> match)` | +| `SetupHttpPut` | `(string? requestUri = null, string? content = null)`; `(Expression> match)` | +| `SetupHttpPatch` | `(string? requestUri = null, string? content = null)`; `(Expression> match)` | +| `SetupHttpDelete` | `(string? requestUri = null)`; `(Expression> match)` | +| `SetupHttpHead` | `(string? requestUri = null)`; `(Expression> match)` | -Match requests by a URL substring. The match checks whether the request URI contains the specified string: +Verb helpers restrict the HTTP method. URI matching is a case-sensitive substring of the URI text; body matching is exact text. Null URI/content filters match anything. ```csharp -mocker.SetupHttpGet("/users") - .ReturnsHttpResponse(HttpStatusCode.OK, """{"users": []}"""); - mocker.SetupHttpPost("/orders", "order data") - .ReturnsHttpResponse(HttpStatusCode.Created, """{"id": 1}"""); - -mocker.SetupHttpPut("/users/1", "updated data") - .ReturnsHttpResponse(HttpStatusCode.OK, """{"updated": true}"""); - -mocker.SetupHttpPatch("/users/1", "partial update") - .ReturnsHttpResponse(HttpStatusCode.OK, """{"updated": true}"""); - -mocker.SetupHttpDelete("/users/1") + .ReturnsHttpResponse(HttpStatusCode.Created); +mocker.SetupHttpPut("/orders/1", "replacement") + .ReturnsHttpResponse(HttpStatusCode.OK); +mocker.SetupHttpPatch("/orders/1", "change") + .ReturnsHttpResponse(HttpStatusCode.OK); +mocker.SetupHttpDelete("/orders/1") .ReturnsHttpResponse(HttpStatusCode.NoContent); - mocker.SetupHttpHead("/health") .ReturnsHttpResponse(HttpStatusCode.OK); -``` - -### Setup by Expression - -Use lambda expressions for more complex request matching: - -```csharp -mocker.SetupHttpGet(r => r.RequestUri!.AbsoluteUri.EndsWith("/people")) - .ReturnsHttpResponse(HttpStatusCode.OK, """[{"name": "Alice"}]"""); -mocker.SetupHttpPost(r => r.RequestUri!.AbsoluteUri.Contains("/api/")) - .ReturnsHttpResponse(HttpStatusCode.Accepted); +mocker.GetMock() + .SetupHttpGet(r => r.RequestUri!.AbsolutePath == "/users") + .ReturnsHttpResponse("[]"); ``` -### Multiple URLs with Different Responses +## SetupHttp, SetupSequence, SetupHttpSequence, and conditional Setup ```csharp -mocker.SetupHttpGet("/users") - .ReturnsHttpResponse(HttpStatusCode.OK, """{"users": []}"""); - -mocker.SetupHttpGet("/products") - .ReturnsHttpResponse(HttpStatusCode.OK, """{"products": []}"""); - -var service = mocker.CreateInstance(); - -var usersResponse = await service.GetUsersAsync(); // matches /users -var productsResponse = await service.GetProductsAsync(); // matches /products +ISetup SetupHttp( + this Mock handler, + Expression> expression) +ISetupSequentialResult SetupSequence( + this Mock handler, + Expression> expression) +ISetupSequentialResult SetupHttpSequence( + this AutoMocker mocker, + Expression> expression) +ISetup> Setup( + this ISetupConditionResult handler, + Expression>> expression) ``` -### Error Responses +`IHttpMessageHandler` exposes `Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)` as a public analogue of the handler's protected method. Use it in expressions; it is not a separately registered service. ```csharp -mocker.SetupHttpGet() - .ReturnsHttpResponse(HttpStatusCode.InternalServerError, "Server Error"); +var handler = mocker.GetMock(); +handler.SetupHttp(x => x.SendAsync( + It.IsAny(), It.IsAny())) + .ReturnsHttpResponse(HttpStatusCode.OK); -mocker.SetupHttpPost() - .ReturnsHttpResponse(HttpStatusCode.BadRequest, """{"error": "Invalid input"}"""); +mocker.SetupHttpSequence(x => x.SendAsync( + It.IsAny(), It.IsAny())) + .ReturnsHttpResponse(HttpStatusCode.ServiceUnavailable) + .ReturnsResponse("recovered"); ``` -### Custom Headers - -Use the `configure` callback to modify response headers or other properties: +`SetupSequence` returns successive responses for one setup. Conditional `Setup` instead supports `When(...)` or Moq's `InSequence(...)`: ```csharp -mocker.SetupHttpGet() - .ReturnsHttpResponse(HttpStatusCode.OK, "Response body", configure: response => - { - response.Headers.Add("X-Custom-Header", "CustomValue"); - response.Headers.Add("X-Request-Id", "abc-123"); - }); +bool enabled = true; +handler.When(() => enabled) + .Setup(x => x.SendAsync( + It.IsAny(), It.IsAny())) + .ReturnsHttpResponse(HttpStatusCode.OK); ``` -### Binary Content +Conditional expressions must directly call `SendAsync` on their `IHttpMessageHandler` parameter; invalid expression shapes throw `ArgumentException`. -Return byte array responses with a specified media type: +## RequestMatcher.Is overloads -```csharp -var pdfBytes = File.ReadAllBytes("sample.pdf"); +These static matchers return `HttpRequestMessage` for use **inside Moq setup/verification expressions**, not for sending requests. URI matching uses equality, unlike the substring filters on verb helpers. String URL overloads create a `Uri` and therefore require a valid absolute URL. -mocker.SetupHttpGet("/documents/1") - .ReturnsResponse(HttpStatusCode.OK, pdfBytes, "application/pdf"); +```csharp +HttpRequestMessage Is(Predicate match) +HttpRequestMessage Is(Func> match) +HttpRequestMessage Is(Uri requestUri) +HttpRequestMessage Is(string requestUrl) +HttpRequestMessage Is(Uri requestUri, Predicate match) +HttpRequestMessage Is(Uri requestUri, Func> match) +HttpRequestMessage Is(string requestUrl, Predicate match) +HttpRequestMessage Is(string requestUrl, Func> match) +HttpRequestMessage Is(HttpMethod method, Predicate match) +HttpRequestMessage Is(HttpMethod method, Func> match) +HttpRequestMessage Is(HttpMethod method, Uri requestUri) +HttpRequestMessage Is(HttpMethod method, string requestUrl) +HttpRequestMessage Is( + HttpMethod method, Uri requestUri, Predicate match) +HttpRequestMessage Is( + HttpMethod method, Uri requestUri, Func> match) +HttpRequestMessage Is( + HttpMethod method, string requestUrl, Predicate match) +HttpRequestMessage Is( + HttpMethod method, string requestUrl, Func> match) ``` -### Stream Content - -Return stream-based responses. Seekable streams are automatically wrapped so each request gets an independent stream position: +Predicates are memoized per request so content need not be read repeatedly. Asynchronous predicates are evaluated synchronously by Moq's matching pipeline; avoid callbacks that depend on a captured synchronization context. Null predicates throw `ArgumentNullException`. ```csharp -using var stream = new MemoryStream(Encoding.UTF8.GetBytes("streamed content")); - -mocker.SetupHttpGet("/download") - .ReturnsResponse(HttpStatusCode.OK, stream, "application/octet-stream"); +handler.SetupHttp(x => x.SendAsync( + RequestMatcher.Is(HttpMethod.Get, "https://example.test/users"), + It.IsAny())) + .ReturnsHttpResponse("[]"); + +Func> matchesContent = async request => + await request.Content!.ReadAsStringAsync().ConfigureAwait(false) == "order data"; +handler.SetupHttp(x => x.SendAsync( + RequestMatcher.Is(HttpMethod.Post, "https://example.test/orders", matchesContent), + It.IsAny())) + .ReturnsHttpResponse(HttpStatusCode.Created); ``` -### Sequential Responses +## Response builders -Set up different responses for consecutive calls — useful for testing retry logic: +For ordinary setups, the receiver is `ISetup>` and the return type is `IReturnsResult`. Sequential variants receive and return `ISetupSequentialResult>`. -```csharp -mocker.SetupHttpSequence(x => x.SendAsync( - It.IsAny(), It.IsAny())) - .ReturnsHttpResponse(HttpStatusCode.ServiceUnavailable) - .ReturnsHttpResponse(HttpStatusCode.OK, "Success"); +Receivers and return types are omitted. `configure` changes headers or other response properties. -var service = mocker.CreateInstance(); +### ReturnsHttpResponse -var first = await service.CallApiAsync(); // 503 Service Unavailable -var second = await service.CallApiAsync(); // 200 OK +```csharp +ReturnsHttpResponse( + HttpStatusCode statusCode, Action? configure = null) +ReturnsHttpResponse( + HttpStatusCode statusCode, HttpContent content, + Action? configure = null) +ReturnsHttpResponse( + HttpStatusCode statusCode, string content, + string? mediaType = null, Encoding? encoding = null, + Action? configure = null) +ReturnsHttpResponse( + string content, string? mediaType = null, Encoding? encoding = null, + Action? configure = null) ``` -### Strict Mode +The first three signatures support both receivers. The string-only `ReturnsHttpResponse` supports **ordinary setups only**; its sequential counterpart is named **`ReturnsResponse(string, ...)`**. -With `MockBehavior.Strict`, any request without an explicit setup throws a `MockException`: +String content defaults to UTF-8 and `text/plain`; status-free overloads use HTTP 200. ```csharp -AutoMocker mocker = new(MockBehavior.Strict); -var service = mocker.CreateInstance(); - -// Throws MockException — no setup for this request -await Assert.ThrowsAsync( - () => service.GetUsersAsync()); +mocker.SetupHttpGet("/users") + .ReturnsHttpResponse(HttpStatusCode.OK, "[]", "application/json", + configure: response => response.Headers.Add("X-Test", "true")); +mocker.SetupHttpGet("/custom") + .ReturnsHttpResponse(HttpStatusCode.OK, new StringContent("custom")); ``` -This is useful for ensuring your code only makes expected HTTP calls. - -### Verb Isolation - -Each setup method only matches its specific HTTP method. A `SetupHttpGet` will not match POST requests, and vice versa: +### ReturnsResponse ```csharp -AutoMocker mocker = new(MockBehavior.Strict); -mocker.SetupHttpGet("/people") - .ReturnsHttpResponse(HttpStatusCode.OK, "[]"); +ReturnsResponse( + HttpStatusCode statusCode, byte[] content, string? mediaType = null, + Action? configure = null) +ReturnsResponse( + byte[] content, string? mediaType = null, + Action? configure = null) +ReturnsResponse( + HttpStatusCode statusCode, Stream content, string? mediaType = null, + Action? configure = null) +ReturnsResponse( + Stream content, string? mediaType = null, + Action? configure = null) +// Sequential receiver only: +ReturnsResponse( + string content, string? mediaType = null, Encoding? encoding = null, + Action? configure = null) +``` -var service = mocker.CreateInstance(); +Byte and stream overloads support both receivers. Status-free overloads use HTTP 200. Ordinary setups create a response per call; supplied `HttpContent` is still shared. Seekable streams are wrapped with independent positions. Sequential builders pre-create each response. All content overloads require non-null content. -// This POST throws because only GET is set up -await Assert.ThrowsAsync( - () => service.CreatePersonAsync("Alice")); +```csharp +mocker.SetupHttpGet("/bytes") + .ReturnsResponse(new byte[] { 1, 2, 3 }, "application/octet-stream"); +mocker.SetupHttpGet("/stream") + .ReturnsResponse(HttpStatusCode.OK, new MemoryStream(new byte[] { 4, 5 })); ``` ## Verification -Verify that specific HTTP requests were made during the test: +Verb-specific methods extend **both** `AutoMocker` and `Mock`. Receivers are omitted: ```csharp -var service = mocker.CreateInstance(); +void VerifyHttpGet(string requestUri, Times? times = null, string? failMessage = null) +void VerifyHttpPost( + string requestUri, string? content = null, Times? times = null, string? failMessage = null) +void VerifyHttpPut( + string requestUri, string? content = null, Times? times = null, string? failMessage = null) +void VerifyHttpPatch( + string requestUri, string? content = null, Times? times = null, string? failMessage = null) +void VerifyHttpDelete(string requestUri, Times? times = null, string? failMessage = null) +void VerifyHttpHead(string requestUri, Times? times = null, string? failMessage = null) +``` -await service.SendNotificationAsync("Hello"); +Lower-level methods extend **Mock<HttpMessageHandler> only**: -// Verify a GET was made to a specific URL -mocker.VerifyHttpGet("https://example.com/api/status", Times.Once()); +```csharp +void VerifyHttp( + Expression> match, + Times? times = null, string? failMessage = null) +void VerifyHttp( + Expression> expression, + Times? times = null, string? failMessage = null) +void Verify( + Expression> expression, + Times? times = null, string? failMessage = null) +``` -// Verify a POST with specific content -mocker.VerifyHttpPost("https://example.com/api/notify", "Hello", Times.Once()); +Matching follows setup rules. Omitted `times` means at least once. `failMessage` customizes the `MockException` raised on failure. -// Verify PUT, PATCH, DELETE, HEAD -mocker.VerifyHttpPut("https://example.com/api/config", "new value", Times.Once()); -mocker.VerifyHttpPatch("https://example.com/api/config", "partial value", Times.Once()); -mocker.VerifyHttpDelete("https://example.com/api/cache", Times.Once()); -mocker.VerifyHttpHead("https://example.com/api/health", Times.Once()); +```csharp +mocker.VerifyHttpPost("/orders", "order data", Times.Once()); +mocker.VerifyHttpPut("/orders/1", "replacement", Times.Once()); +mocker.VerifyHttpPatch("/orders/1", "change", Times.Once()); +mocker.VerifyHttpDelete("/orders/1", Times.Never()); +mocker.VerifyHttpHead("/health", Times.Once()); +handler.VerifyHttp(r => r.Method == HttpMethod.Get, Times.Once()); +handler.Verify(x => x.SendAsync( + It.IsAny(), It.IsAny()), Times.Once()); ``` -## Lower-Level Access - -For advanced scenarios, access the `Mock` directly: +## CreateClient ```csharp -// Setup via the mock directly -mocker.GetMock() - .SetupHttp(x => x.SendAsync( - It.IsAny(), It.IsAny())) - .ReturnsHttpResponse(HttpStatusCode.OK, "Hello, World!"); +HttpClient CreateClient(this Mock handler) +``` -// Create an HttpClient from the mock -HttpClient client = mocker.GetMock().CreateClient(); +Creates a client without transferring handler ownership. Dispose the client separately. Unlike resolving `HttpClient` through the container, this helper alone does not configure fallback responses. -// Verify via the mock directly -mocker.GetMock() - .Verify(x => x.SendAsync( - It.Is(r => r.Method == HttpMethod.Get), - It.IsAny()), Times.Once()); +```csharp +using HttpClient client = mocker.GetMock().CreateClient(); ``` -## API Reference - -### Setup Methods - -All setup methods are available as extension methods on both `AutoMocker` and `Mock`. - -| Method | Description | -|--------|-------------| -| `SetupHttpGet(string?)` | Setup GET requests, optionally matching a URL substring | -| `SetupHttpGet(Expression)` | Setup GET requests matching an expression predicate | -| `SetupHttpPost(string?, string?)` | Setup POST requests with optional URL and content matching | -| `SetupHttpPost(Expression)` | Setup POST requests matching an expression predicate | -| `SetupHttpPut(string?, string?)` | Setup PUT requests with optional URL and content matching | -| `SetupHttpPut(Expression)` | Setup PUT requests matching an expression predicate | -| `SetupHttpPatch(string?, string?)` | Setup PATCH requests with optional URL and content matching | -| `SetupHttpPatch(Expression)` | Setup PATCH requests matching an expression predicate | -| `SetupHttpDelete(string?)` | Setup DELETE requests, optionally matching a URL substring | -| `SetupHttpDelete(Expression)` | Setup DELETE requests matching an expression predicate | -| `SetupHttpHead(string?)` | Setup HEAD requests, optionally matching a URL substring | -| `SetupHttpHead(Expression)` | Setup HEAD requests matching an expression predicate | -| `SetupHttp(Expression)` | Generic setup on `IHttpMessageHandler` (on `Mock`) | -| `SetupHttpSequence(Expression)` | Setup sequential responses (on `AutoMocker`) | -| `SetupSequence(Expression)` | Setup sequential responses (on `Mock`) | - -### Response Methods - -| Method | Description | -|--------|-------------| -| `ReturnsHttpResponse(HttpStatusCode)` | Return a response with the given status code | -| `ReturnsHttpResponse(HttpStatusCode, string)` | Return a string response with status code | -| `ReturnsHttpResponse(HttpStatusCode, HttpContent)` | Return custom content with status code | -| `ReturnsHttpResponse(string)` | Return a string response with HTTP 200 OK | -| `ReturnsResponse(HttpStatusCode, byte[], string?)` | Return byte array content with media type | -| `ReturnsResponse(HttpStatusCode, Stream, string?)` | Return stream content with media type | -| `ReturnsResponse(byte[], string?)` | Return byte array content with HTTP 200 OK | -| `ReturnsResponse(Stream, string?)` | Return stream content with HTTP 200 OK | - -All response methods accept an optional `Action? configure` parameter for customizing headers and other response properties. Sequential variants (`ISetupSequentialResult`) are also available for all response methods. - -### Verification Methods - -| Method | Target | -|--------|--------| -| `VerifyHttpGet(string, Times?, string?)` | Verify GET requests to a URL | -| `VerifyHttpPost(string, string?, Times?, string?)` | Verify POST requests with optional content | -| `VerifyHttpPut(string, string?, Times?, string?)` | Verify PUT requests with optional content | -| `VerifyHttpPatch(string, string?, Times?, string?)` | Verify PATCH requests with optional content | -| `VerifyHttpDelete(string, Times?, string?)` | Verify DELETE requests to a URL | -| `VerifyHttpHead(string, Times?, string?)` | Verify HEAD requests to a URL | -| `VerifyHttp(Expression, Times?, string?)` | Verify with a custom expression predicate | - -### Utility Methods - -| Method | Description | -|--------|-------------| -| `CreateClient()` | Create an `HttpClient` backed by a `Mock` | - -## Best Practices - -- **Use verb-specific methods** (`SetupHttpGet`, `SetupHttpPost`, etc.) instead of the generic `SetupHttp` for clearer test intent and automatic HTTP method filtering -- **Match by URL substring** for simple cases (`"/users"`) and **expressions** for complex matching logic -- **Use strict mode** (`MockBehavior.Strict`) when you want to ensure no unexpected HTTP calls are made -- **Verify requests** to confirm your code sends the right HTTP method, URL, and content -- **Use sequential responses** to test retry patterns, circuit breakers, and degraded service scenarios -- **Leverage the `configure` callback** to test code that inspects response headers +For named clients, see [WithHttpClientFactory](SourceGenerators/HttpClientFactoryExtensionGenerator.md). The generated factory caches one client per name; named clients share the testable handler. diff --git a/docs/Moq.AutoMock.md b/docs/Moq.AutoMock.md index 1b33b10c..e5fff234 100644 --- a/docs/Moq.AutoMock.md +++ b/docs/Moq.AutoMock.md @@ -1,24 +1,11 @@ -# Moq.AutoMock assembly +# Moq.AutoMock API reference -## Moq.AutoMock namespace +The [documentation index](README.md) links the current public API reference, with examples and overloads grouped by method. -| public type | description | +| Namespace | Reference | | --- | --- | -| class [AutoMocker](Moq.AutoMock/AutoMocker.md) | An auto-mocking IoC container that generates mock objects using Moq. | -| static class [MockExtensions](Moq.AutoMock/MockExtensions.md) | Extensions for Mock objects. | -| class [ObjectGraphContext](Moq.AutoMock/ObjectGraphContext.md) | Handles state while creating an object graph. | +| `Moq.AutoMock` | [AutoMocker](AutoMocker.md), [setup and verification / MockExtensions](SetupAndVerification.md), [ObjectGraphContext, ObjectCreationException, ConstructorTestsAttribute, TestGenerationBehavior](Diagnostics.md) | +| `Moq.AutoMock.Resolvers` | [All resolvers and MockResolutionContext](Resolvers.md) | +| `Moq.AutoMock.Http` | [IHttpMessageHandler and RequestMatcher](HttpClient.md) | -## Moq.AutoMock.Resolvers namespace - -| public type | description | -| --- | --- | -| class [AutoMockerDisposableResolver](Moq.AutoMock.Resolvers/AutoMockerDisposableResolver.md) | A resolver that resolves instances for IAutoMockerDisposable. | -| class [EnumerableResolver](Moq.AutoMock.Resolvers/EnumerableResolver.md) | A resolver that resolves IEnumerable<T> requested types. | -| class [FuncResolver](Moq.AutoMock.Resolvers/FuncResolver.md) | A resolver that resolves Func<> dependency types | -| interface [IMockResolver](Moq.AutoMock.Resolvers/IMockResolver.md) | Base interface for all mock resolvers. | -| class [LazyResolver](Moq.AutoMock.Resolvers/LazyResolver.md) | A resolver that resolves Lazy<T> requested types. | -| class [MockResolutionContext](Moq.AutoMock.Resolvers/MockResolutionContext.md) | The context used to resolve types from an `AutoMocker` instance. | -| class [MockResolver](Moq.AutoMock.Resolvers/MockResolver.md) | A resolver that resolves requested types with Mock<T> instances. | -| class [SelfResolver](Moq.AutoMock.Resolvers/SelfResolver.md) | Resolves calls to retireve AutoMocker with itself. | - - +The older generated pages under `Moq.AutoMock/` and `Moq.AutoMock.Resolvers/` are retained for existing links. Use the references above for current signatures and behavior. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..23034fad --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# Moq.AutoMock documentation + +| Reference | APIs | +| --- | --- | +| [AutoMocker](AutoMocker.md) | Construction, registration, retrieval, self-mocks, combining services, disposal | +| [Setup and verification](SetupAndVerification.md) | `Setup`, `SetupSequence`, `SetupAllProperties`, `SetupWithAny`, `Verify`, `VerifyAll`, `MockExtensions` | +| [Resolvers](Resolvers.md) | Resolver pipeline, built-in resolvers, resolution contexts | +| [Diagnostics and constructor tests](Diagnostics.md) | `ObjectCreationException`, `ConstructorTestsAttribute`, `TestGenerationBehavior` | +| [HTTP](HttpClient.md) | `MockHttpMessageHandlerExtensions`, `IHttpMessageHandler`, `RequestMatcher` | +| [Generated extensions](GeneratedExtensions.md) | Conditional extension signatures, grouped keyed-service overloads, telemetry accessors | +| [Source generators](SourceGenerators.md) | Conditional options, logging, time, telemetry, keyed services, metrics, HTTP factory extensions | +| [Compiler integration](CompilerIntegration.md) | Public Roslyn generator, analyzer, diagnostic, and model APIs | +| [Package validation](PackageValidation.md) | Contributor guidance for compatibility checks | + +Overloads are grouped by method. Signatures omit `public` and extension receivers where noted. Examples use application-specific types such as `IStore` and `Store`; replace these with your own types. + +Start with `using Moq.AutoMock;` and `using Moq;`. HTTP extensions also require `using Moq.AutoMock.Http;`. diff --git a/docs/Resolvers.md b/docs/Resolvers.md new file mode 100644 index 00000000..1c86b97d --- /dev/null +++ b/docs/Resolvers.md @@ -0,0 +1,162 @@ +# Resolvers + +Namespace: `Moq.AutoMock.Resolvers`. [Documentation index](README.md). + +## Pipeline + +`AutoMocker.Resolvers` is an ordered `IList`. The first resolver that assigns `context.Value` wins, even if it assigns null. Leave `Value` untouched to decline a request. + +```csharp +AutoMocker InsertResolverBefore(IMockResolver resolver) + where TResolver : IMockResolver +AutoMocker InsertResolverAfter(IMockResolver resolver) + where TResolver : IMockResolver +``` + +Both return the container and insert relative to the first matching resolver. A missing target throws `InvalidOperationException`. Put custom resolvers after `CacheResolver` to preserve explicit registrations. + +```csharp +using Moq.AutoMock; +using Moq.AutoMock.Resolvers; + +var mocker = new AutoMocker(); +mocker.InsertResolverBefore(new TestIdResolver()); +Guid id = mocker.Get(); + +public sealed class TestIdResolver : IMockResolver +{ + public void Resolve(MockResolutionContext context) + { + if (context.RequestType != typeof(Guid)) + return; + + context.Value = Guid.NewGuid(); + context.NoCache = true; + } +} +``` + +Resolver exceptions are recorded in `ObjectGraphContext.DiagnosticMessages` and resolution continues. Inspect diagnostics when troubleshooting unsuccessful construction. + +## IMockResolver.Resolve + +```csharp +void Resolve(MockResolutionContext context) +``` + +Implement this interface to supply custom values, as above. Built-in implementations expose the same method. + +## MockResolutionContext + +```csharp +MockResolutionContext( + AutoMocker autoMocker, Type requestType, ObjectGraphContext objectGraphContext) +void Deconstruct(out AutoMocker autoMocker, out Type type, out object? value) +``` + +All constructor arguments must be non-null. + +| Property | Access | Meaning | +| --- | --- | --- | +| `AutoMocker` | get | Container servicing this request | +| `RequestType` | get | Requested service type | +| `ObjectGraphContext` | get | Shared construction state and target parameter | +| `Value` | get/set | Resolved service object or `Mock`; assigning null also completes resolution | +| `NoCache` | get/set | Skip caching this result; defaults to `false` | + +```csharp +var context = new MockResolutionContext( + mocker, typeof(Guid), new ObjectGraphContext(enablePrivate: false)); +new TestIdResolver().Resolve(context); +var (container, requestedType, value) = context; +``` + +## Built-in resolvers + +Every concrete resolver has a public parameterless constructor except `MockResolver`, whose overloads are below. Each implements `Resolve(MockResolutionContext)` (directly or through `SimpleTypeResolver`). + +Default order: + +| Resolver | Resolution | +| --- | --- | +| `CacheResolver` | Previously registered or cached value | +| `CallbackResolver` | Factory registered with `Use` or `AddCallback` | +| `SelfResolver` | The current `AutoMocker`; skips explicit mock creation | +| `ArrayResolver` | Array containing a resolved element, or empty if the element cannot resolve | +| `AutoMockerDisposableResolver` | Internal cleanup handle returned by `AsDisposable()` | +| `EnumerableResolver` | `IEnumerable` containing one resolved `T` | +| `LazyResolver` | `Lazy` that calls the container on first value access | +| `FuncResolver` | `Func<..., TResult>` that calls `Get()` on invocation; input arguments are ignored | +| `CancellationTokenResolver` | Token from a cached `CancellationTokenSource`, otherwise `CancellationToken.None` | +| `HttpClientResolver` | `HttpClient` backed by a handler mock; unconfigured requests return HTTP 200 in loose mode | +| `MockResolver` | Moq mock using its constructor settings; does not resolve `string` | + +`InstanceResolver()` is opt-in: it constructs real objects with resolved constructor dependencies. Insert it before `MockResolver` when real instances should take precedence. + +```csharp +var mocker = new AutoMocker(); +var callbacks = mocker.Resolvers.OfType().Single(); +callbacks.AddCallback(_ => "test"); +string text = mocker.Get(); + +using var source = new CancellationTokenSource(); +mocker.Use(source); +CancellationToken token = mocker.Get(); + +var realObjects = new AutoMocker(); +realObjects.InsertResolverBefore(new InstanceResolver()); +Store store = realObjects.Get(); +``` + +### CallbackResolver.AddCallback + +```csharp +void AddCallback(Func callback) +``` + +Registers or replaces a factory for the requested type. The normal pipeline caches its result, so register before first resolution. Prefer `AutoMocker.Use` unless directly customizing this resolver. + +### MockResolver constructors + +```csharp +MockResolver(MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase) +MockResolver( + MockBehavior mockBehavior, DefaultValue defaultValue, + DefaultValueProvider? defaultValueProvider, bool callBase) +``` + +The shorter overload uses no custom default-value provider. + +```csharp +var resolver = new MockResolver( + MockBehavior.Loose, DefaultValue.Empty, callBase: false); +``` + +### SimpleTypeResolver<T> + +An abstract base for resolvers centered on one type. + +```csharp +public virtual void Resolve(MockResolutionContext context) +protected abstract T GetValue(MockResolutionContext context) +protected bool IncludeBaseTypes { get; set; } // default: true +protected bool IncludeInterfaces { get; set; } // default: true +``` + +`Resolve` matches `T` exactly, implemented interfaces when `IncludeInterfaces` is true, and requests satisfying `typeof(T).IsAssignableFrom(context.RequestType)` when `IncludeBaseTypes` is true. Disable both flags for exact matching. + +```csharp +public sealed class ConnectionStringResolver : SimpleTypeResolver +{ + public ConnectionStringResolver() + { + IncludeBaseTypes = false; + IncludeInterfaces = false; + } + + protected override string GetValue(MockResolutionContext context) + => "test-connection"; +} +``` + +`SelfResolver` and `HttpClientResolver` override `GetValue`; `SelfResolver` also overrides `Resolve`. diff --git a/docs/SetupAndVerification.md b/docs/SetupAndVerification.md new file mode 100644 index 00000000..e9413a9d --- /dev/null +++ b/docs/SetupAndVerification.md @@ -0,0 +1,135 @@ +# Setup and verification + +Namespace: `Moq.AutoMock`. [Documentation index](README.md). + +These `AutoMocker` shortcuts retrieve or create the service's mock, then delegate to Moq. All service type parameters are constrained to `class`. Null setup/verification expressions throw `ArgumentNullException`; verification failures throw `MockException`. + +## Setup + +```csharp +ISetup Setup(Expression> setup) +ISetup Setup( + Expression> setup) +``` + +Use the first overload for void methods and the second for return values or properties. Both return Moq's fluent setup API. + +```csharp +mocker.Setup(x => x.Save("item")).Verifiable(); +mocker.Setup(x => x.Find(42)).Returns("item"); +``` + +## SetupSequence + +```csharp +ISetupSequentialResult SetupSequence( + Expression> setup) +``` + +Configures successive return values or exceptions. + +```csharp +mocker.SetupSequence(x => x.Find(42)) + .Returns("first") + .Returns("second") + .Throws(new InvalidOperationException("No more items.")); +``` + +## SetupAllProperties + +```csharp +Mock SetupAllProperties() +``` + +Enables property value tracking and returns the mock. + +```csharp +Mock settings = mocker.SetupAllProperties(); +settings.Object.Name = "test"; +string name = mocker.Get().Name; // "test" +``` + +## SetupWithAny / MockExtensions.SetupWithAny + +```csharp +// AutoMocker methods: +ISetup SetupWithAny(string methodName) +ISetup SetupWithAny(string methodName) + +// MockExtensions extension methods: +ISetup SetupWithAny(this Mock mock, string methodName) +ISetup SetupWithAny(this Mock mock, string methodName) +``` + +Sets up a method by name, supplying `It.IsAny()` for each argument. Use the return-type overload for non-void methods. The method must not be overloaded: missing methods throw `MissingMethodException`, ambiguous names throw `AmbiguousMatchException`, and null names or mock receivers throw `ArgumentNullException`. + +```csharp +mocker.SetupWithAny(nameof(IStore.Save)).Verifiable(); +mocker.SetupWithAny(nameof(IStore.Find)).Returns("item"); + +var store = new Mock(); +store.SetupWithAny(nameof(IStore.Find)).Returns("item"); +``` + +## Verify + +```csharp +void Verify() +void Verify() where T : class +``` + +`Verify()` checks verifiable setups on every tracked mock. `Verify()` checks only the requested mock. Neither means "verify all setups"; use `VerifyAll` for that. + +Expression overloads verify matching calls. All support `where T : class`: + +```csharp +void Verify(Expression> expression) +void Verify(Expression> expression, Times times) +void Verify(Expression> expression, Func times) +void Verify(Expression> expression, string failMessage) +void Verify(Expression> expression, Times times, string failMessage) +void Verify(Expression> expression, Func times, string failMessage) + +void Verify(Expression> expression) +void Verify(Expression> expression, Times times) +void Verify(Expression> expression, Func times) +void Verify(Expression> expression, string failMessage) +void Verify(Expression> expression, Times times, string failMessage) + +void Verify(Expression> expression) +void Verify(Expression> expression, Times times) +void Verify(Expression> expression, Func times) +void Verify(Expression> expression, string failMessage) +void Verify( + Expression> expression, Times times, string failMessage) +``` + +Without `times`, expression overloads require at least one call. `Func` supports deferred counts. Use `Verify` for value-type returns: the `object` expression overloads reject boxing a value-type result with `NotSupportedException`. + +```csharp +mocker.Setup(x => x.Save("item")).Verifiable(); +mocker.Get().Save("item"); +mocker.Verify(x => x.Save("item"), Times.Once(), "Save was not called."); +mocker.Verify(); +mocker.Verify(); + +mocker.Get().Next(); +mocker.Verify(x => x.Next(), Times.Once()); +``` + +## VerifyAll + +```csharp +void VerifyAll(bool ignoreMissingSetups = false) +``` + +Checks every setup on every tracked mock, including setups not marked verifiable. If no tracked mock has any setups, throws `InvalidOperationException` unless `ignoreMissingSetups` is true. That flag only permits an empty set; it does not suppress unmet expectations. + +```csharp +mocker.Setup(x => x.Find(42)).Returns("item"); +mocker.Get().Find(42); +mocker.VerifyAll(); + +var empty = new AutoMocker(); +empty.VerifyAll(ignoreMissingSetups: true); +``` diff --git a/docs/SourceGenerators.md b/docs/SourceGenerators.md index 06e0ff57..10342717 100644 --- a/docs/SourceGenerators.md +++ b/docs/SourceGenerators.md @@ -1,5 +1,7 @@ # Moq.AutoMocker Source Generators +For concise signatures with overloads grouped together, see [Generated extensions](GeneratedExtensions.md). See the [documentation index](README.md) for the core API. + Moq.AutoMock includes built-in source generators that provide compile-time code generation to enhance your testing experience. These generators automatically create boilerplate test code and extension methods, saving you time and reducing repetitive testing code. ## Available Generators @@ -171,7 +173,7 @@ var client = factory.CreateClient("catalog"); ## Important: Generated Classes Are Internal Partials -All extension classes created by these source generators are generated as **partial classes** with **internal visibility**. For example, the Keyed Services generator produces: +Most extension classes created by these source generators are **internal partial classes**. The HTTP client factory extension class is public. For example, the Keyed Services generator produces: ```csharp internal static partial class AutoMockerKeyedServicesExtensions