diff --git a/.editorconfig b/.editorconfig index 5a911ad..9be2882 100644 --- a/.editorconfig +++ b/.editorconfig @@ -447,22 +447,22 @@ dotnet_naming_rule.parameters_rule.severity = warning # Meziantou # https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm -dotnet_diagnostic.MA0003.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0003.md -dotnet_diagnostic.MA0004.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0004.md -dotnet_diagnostic.MA0006.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0006.md +dotnet_diagnostic.MA0003.severity = suggestion # Add argument name to improve readability +dotnet_diagnostic.MA0004.severity = suggestion # Use Task.ConfigureAwait(false) +dotnet_diagnostic.MA0006.severity = none # Use String.Equals instead of equality operator dotnet_diagnostic.MA0011.severity = none # Duplicate of CA1305 -dotnet_diagnostic.MA0016.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0016.md -dotnet_diagnostic.MA0025.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0025.md -dotnet_diagnostic.MA0026.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0026.md -dotnet_diagnostic.MA0028.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0028.md +dotnet_diagnostic.MA0016.severity = error # Prefer return collection abstraction over implementation +dotnet_diagnostic.MA0025.severity = suggestion # Implement functionality instead of throwing NotImplementedException +dotnet_diagnostic.MA0026.severity = suggestion # Fix TODO comment +dotnet_diagnostic.MA0028.severity = none # Optimize StringBuilder usage dotnet_diagnostic.MA0038.severity = none # Duplicate of CA1822 -dotnet_diagnostic.MA0048.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0048.md +dotnet_diagnostic.MA0048.severity = error # File name must match type name # Microsoft - Code Analysis # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ -dotnet_diagnostic.CA1014.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1014.md -dotnet_diagnostic.CA1068.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1068.md +dotnet_diagnostic.CA1014.severity = none # Mark assemblies with CLSCompliantAttribute +dotnet_diagnostic.CA1068.severity = error # CancellationToken parameters must come last dotnet_diagnostic.CA1305.severity = error dotnet_diagnostic.CA1308.severity = suggestion # Normalize strings to uppercase dotnet_diagnostic.CA1510.severity = suggestion # Use ArgumentNullException throw helper @@ -471,7 +471,7 @@ dotnet_diagnostic.CA1512.severity = suggestion # Use ArgumentOutOfRangeExce dotnet_diagnostic.CA1513.severity = suggestion # Use ObjectDisposedException throw helper dotnet_diagnostic.CA1514.severity = error # Avoid redundant length argument dotnet_diagnostic.CA1515.severity = suggestion # Because an application's API isn't typically referenced from outside the assembly, types can be made internal (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1515) -dotnet_diagnostic.CA1707.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1707.md +dotnet_diagnostic.CA1707.severity = error # Identifiers should not contain underscores dotnet_diagnostic.CA1812.severity = none dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1849.severity = error # Call async methods when in an async method @@ -494,7 +494,7 @@ dotnet_diagnostic.CA1869.severity = suggestion # Cache and reuse 'JsonSeria dotnet_diagnostic.CA1870.severity = suggestion # Use a cached 'SearchValues' instance dotnet_diagnostic.CA1871.severity = suggestion # Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' dotnet_diagnostic.CA1872.severity = suggestion # Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' -dotnet_diagnostic.CA2007.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA2007.md +dotnet_diagnostic.CA2007.severity = suggestion # Do not directly await a Task dotnet_diagnostic.CA2017.severity = error # Parameter count mismatch dotnet_diagnostic.CA2018.severity = error # The count argument to Buffer.BlockCopy should specify the number of bytes to copy dotnet_diagnostic.CA2019.severity = error # ThreadStatic fields should not use inline initialization @@ -510,12 +510,12 @@ dotnet_diagnostic.CA2261.severity = error # Do not use ConfigureAwaitO dotnet_diagnostic.CA2262.severity = suggestion # Set 'MaxResponseHeadersLength' properly dotnet_diagnostic.CA2263.severity = suggestion # Prefer generic overload when type is known dotnet_diagnostic.CA2264.severity = error # Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' -dotnet_diagnostic.IDE0005.severity = warning # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/IDE0005.md +dotnet_diagnostic.IDE0005.severity = warning # Remove unnecessary using directives dotnet_diagnostic.IDE0010.severity = suggestion # Populate switch dotnet_diagnostic.IDE0028.severity = suggestion # Collection initialization can be simplified dotnet_diagnostic.IDE0021.severity = suggestion # Use expression body for constructor dotnet_diagnostic.IDE0055.severity = none # Fix formatting -dotnet_diagnostic.IDE0058.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/IDE0058.md +dotnet_diagnostic.IDE0058.severity = none # Remove unnecessary expression value dotnet_diagnostic.IDE0061.severity = suggestion # Use expression body for local function dotnet_diagnostic.IDE0130.severity = suggestion # Namespace does not match folder structure dotnet_diagnostic.IDE0290.severity = none # Use primary constructor @@ -525,7 +525,7 @@ dotnet_diagnostic.IDE0305.severity = suggestion # Collection initialization # Microsoft - Compiler Errors # https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/ -dotnet_diagnostic.CS4014.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCompilerErrors/CS4014.md +dotnet_diagnostic.CS4014.severity = error # Call is not awaited # SecurityCodeScan @@ -534,29 +534,29 @@ dotnet_diagnostic.CS4014.severity = error # https://github.com/atc-net/ # StyleCop # https://github.com/DotNetAnalyzers/StyleCopAnalyzers -dotnet_diagnostic.SA1009.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1009.md +dotnet_diagnostic.SA1009.severity = none # Closing parenthesis should be spaced correctly dotnet_diagnostic.SA1010.severity = none # False positive when using collection initializers -dotnet_diagnostic.SA1101.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1101.md -dotnet_diagnostic.SA1122.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1122.md -dotnet_diagnostic.SA1133.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1133.md -dotnet_diagnostic.SA1200.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1200.md -dotnet_diagnostic.SA1201.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1201.md -dotnet_diagnostic.SA1202.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1202.md -dotnet_diagnostic.SA1204.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1204.md -dotnet_diagnostic.SA1413.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1413.md -dotnet_diagnostic.SA1600.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1600.md +dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this +dotnet_diagnostic.SA1122.severity = error # Use string.Empty for empty strings +dotnet_diagnostic.SA1133.severity = error # Do not combine attributes +dotnet_diagnostic.SA1200.severity = none # Using directives should be placed correctly +dotnet_diagnostic.SA1201.severity = none # Elements should appear in the correct order +dotnet_diagnostic.SA1202.severity = none # Elements should be ordered by access +dotnet_diagnostic.SA1204.severity = none # Static elements should appear before instance elements +dotnet_diagnostic.SA1413.severity = error # Use trailing commas in multi-line initializers +dotnet_diagnostic.SA1600.severity = none # Elements should be documented dotnet_diagnostic.SA1601.severity = none -dotnet_diagnostic.SA1602.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1602.md -dotnet_diagnostic.SA1604.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1604.md -dotnet_diagnostic.SA1623.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1623.md -dotnet_diagnostic.SA1629.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1629.md -dotnet_diagnostic.SA1633.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1633.md -dotnet_diagnostic.SA1649.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1649.md +dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented +dotnet_diagnostic.SA1604.severity = none # Element documentation should have summary +dotnet_diagnostic.SA1623.severity = none # Property summary documentation should match accessors +dotnet_diagnostic.SA1629.severity = none # Documentation text should end with a period +dotnet_diagnostic.SA1633.severity = none # File should have header +dotnet_diagnostic.SA1649.severity = error # File name should match first type name # SonarAnalyzer.CSharp # https://rules.sonarsource.com/csharp -dotnet_diagnostic.S1135.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/SonarAnalyzerCSharp/S1135.md +dotnet_diagnostic.S1135.severity = suggestion # Track uses of TODO tags dotnet_diagnostic.S2629.severity = none # Don't use string interpolation in logging message templates. dotnet_diagnostic.S3358.severity = none # Extract this nested ternary operation into an independent statement. dotnet_diagnostic.S6602.severity = none # "Find" method should be used instead of the "FirstOrDefault" diff --git a/Directory.Build.props b/Directory.Build.props index 6ba4c77..8e0cb86 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,9 +50,9 @@ - + - + \ No newline at end of file diff --git a/README.md b/README.md index 1cb4f2a..94fd17b 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,99 @@ -# Introduction +# Atc.Test ![NuGet Version](https://img.shields.io/nuget/v/Atc.Test.svg?logo=nuget&style=for-the-badge) `Atc.Test` is a .NET helper library that streamlines authoring tests with xUnit v3, AutoFixture, NSubstitute, and AwesomeAssertions. It provides rich data attributes, automatic specimen customization, and ergonomic frozen value reuse to reduce ceremony and improve test readability. -## Why Atc.Test +## Atc.Test in 30 Seconds -> You can “just wire everything manually” with plain xUnit and hand‑rolled mocks—so why use this instead? +Every dependency your test does not care about is noise. `Atc.Test` lets you declare only the parameters the test is actually about — everything else is generated, substituted and wired for you. -| Problem Without | What You Gain With `Atc.Test` | Why It Matters Over Time | -|-----------------|-------------------------------|--------------------------| -| Repeating constructor/mocker boilerplate in every test | Parameter-only intent: you list just what the test cares about | Lower cognitive load; faster review – noise removed | -| Fragile refactors (add a ctor param ⇒ touch many files) | Fixture-driven auto‑supply of new dependencies | Constructor churn becomes O(1) instead of O(N tests) | -| Divergent ad‑hoc mock styles (naming, setup order) | Central factory + consistent frozen reuse semantics | Suite stays uniform; easier large-scale edits / audits | -| Accidental duplicate substitutes for logically single collaborator | `[Frozen]` exact-type reuse + early supplied promotion (member data) | Prevents subtle mismatch bugs & expectation gaps | -| Manual re-creation of “shared conventions” (recursion handling, generators) | One-time customization via `[AutoRegister]` | New test inherits standards automatically | -| AI-generated setup drifts over time | Declarative attributes act as a stable policy layer | Reduces maintenance & future prompt dependency | +**Without `Atc.Test`** -### When It Delivers the Most Value +```csharp +[Fact] +public void Should_Return_Order_When_Found() +{ + // Arrange + var repository = Substitute.For(); + var pricing = Substitute.For(); + var audit = Substitute.For(); + var clock = Substitute.For(); + var sut = new OrderService(repository, pricing, audit, clock); + + var orderId = Guid.NewGuid(); + var order = new Order { Id = orderId, Name = "Test order" }; + repository.Get(orderId).Returns(order); + + // Act + var result = sut.GetOrder(orderId); + + // Assert + result.Should().BeSameAs(order); +} +``` -* Mid/large test suites (hundreds+ of theory cases). -* Domain services with evolving constructor graphs / dependencies. -* Teams that value refactor safety and consistent test style. -* Situations where only a few parameters per test truly matter. +**With `Atc.Test`** -### When Bare xUnit (+ manual mocks) May Be Enough +```csharp +[Theory] +[AutoNSubstituteData] +public void Should_Return_Order_When_Found( + [Frozen] IOrderRepository repository, + Guid orderId, + Order order, + OrderService sut) +{ + // Arrange + repository.Get(orderId).Returns(order); -* Very small or short‑lived codebases. -* Highly bespoke object graphs where you override almost every generated value anyway. -* Educational contexts emphasizing explicit wiring for learning. + // Act + var result = sut.GetOrder(orderId); -### Summary + // Assert + result.Should().BeSameAs(order); +} +``` -`Atc.Test` trades a tiny amount of initial abstraction for compounding savings in refactors, readability, and consistency. AI can quickly generate boilerplate; this library’s value is eliminating the need for that boilerplate in the first place—and giving you a single, policy‑driven locus for customization and reuse. +`IPricingService`, `IAuditLog` and `TimeProvider` are still supplied to the constructor — they are simply no longer in your way. Add a fifth constructor parameter tomorrow and this test does not change. ## Table of Content -* [Introduction](#introduction) - * [Why Atc.Test](#why-atctest) -* [Table of Content](#table-of-content) - * [Features](#features) - * [Getting Started](#getting-started) - * [Install Package](#install-package) - * [Why xUnit Must Be Referenced Directly](#why-xunit-must-be-referenced-directly) - * [First Test Examples](#first-test-examples) - * [Advanced Usage](#advanced-usage) - * [Frozen Reuse Scenarios](#frozen-reuse-scenarios) - * [Auto Registration of Customizations](#auto-registration-of-customizations) - * [Helper Extensions](#helper-extensions) - * [Requirements](#requirements) - * [How to Contribute](#how-to-contribute) +* [Atc.Test in 30 Seconds](#atctest-in-30-seconds) +* [Cheat Sheet](#cheat-sheet) +* [Features](#features) +* [Getting Started](#getting-started) + * [Install Package](#install-package) + * [Version Compatibility](#version-compatibility) + * [Why xUnit Must Be Referenced Directly](#why-xunit-must-be-referenced-directly) + * [First Test Examples](#first-test-examples) +* [Common Recipes](#common-recipes) +* [Built-in Specimen Support](#built-in-specimen-support) +* [Working With Atc.Test (deep dive)](docs/working-with.md) +* [Advanced Usage](#advanced-usage) + * [Frozen Reuse Scenarios](#frozen-reuse-scenarios) + * [Auto Registration of Customizations](#auto-registration-of-customizations) + * [Helper Extensions](#helper-extensions) +* [Troubleshooting / FAQ](#troubleshooting--faq) +* [Why Atc.Test](#why-atctest) +* [Requirements](#requirements) +* [Migrating from FluentAssertions](#migrating-from-fluentassertions) +* [How to Contribute](#how-to-contribute) + +## Cheat Sheet + +Most users only ever need this table. + +| What you want | Use | Notes | +|---------------|-----|-------| +| Generate every parameter automatically | `[Theory] [AutoNSubstituteData]` | Interfaces/abstract types become NSubstitute substitutes. | +| Mix fixed values with generated ones | `[Theory] [InlineAutoNSubstituteData(2, 3)]` | Inline values fill the leading parameters. | +| Drive a theory from a member | `[Theory] [MemberAutoNSubstituteData(nameof(Source))]` | Supports **exact-type promotion** for `[Frozen]`. | +| Drive a theory from a class | `[Theory] [ClassAutoNSubstituteData(typeof(Source))]` | Positional frozen injection only, no promotion. | +| Reuse one instance across the graph | `[Frozen] IMyService service` | The same instance is injected into everything needing exactly `IMyService`. | +| Teach the fixture about your own type | `[AutoRegister]` on an `ICustomization` / `ISpecimenBuilder` | Discovered automatically, no registration call needed. | +| Build a fixture by hand | `FixtureFactory.Create()` | Same configuration the attributes use. | +| Fail a hanging async test fast | `await task.AddTimeout()` | Defaults to 5s, and is **bypassed when a debugger is attached**. | ## Features @@ -57,6 +101,7 @@ * Automatic interface/abstract substitution via NSubstitute. * Exact-type frozen promotion for member data (reuse supplied instance across later `[Frozen]` parameters). * Deterministic fixture configuration with opt‑in auto-registration of custom `ICustomization` / `ISpecimenBuilder` via `[AutoRegister]`. +* Built-in specimen support for types AutoFixture handles poorly or not at all — `CancellationToken`, `DateOnly`, `TimeOnly`, `Uri`, `TimeProvider`, immutable collections and recursive graphs. * Convenience extensions: equivalency options, substitute inspection helpers, task timeout helpers, object protected member access. * Clear separation of concerns: you own the xUnit runner/version. @@ -75,12 +120,20 @@ Add `Atc.Test` to your test project along with explicit references to xUnit and - + ``` +### Version Compatibility + +| `Atc.Test` | xUnit | Target framework | Assertions | +|------------|-------|------------------|------------| +| 3.x | xUnit v3 (`xunit.v3` 4.x) | `net10.0` | AwesomeAssertions | + +For releases prior to 3.0.0, see the [CHANGELOG](CHANGELOG.md). + ### Why xUnit Must Be Referenced Directly `Atc.Test` depends on `xunit.v3.extensibility.core` (the extensibility surface) but intentionally does **not** bring in the `xunit.v3` meta-package: @@ -154,6 +207,139 @@ All remaining parameters (after inline/member supplied ones) are created via an > **Note** > NSubstitute is used automatically when the requested type is an interface or abstract class. +## Common Recipes + +### Mock one dependency, auto-generate the rest + +Decorate the one you care about with `[Frozen]`. The same instance is injected into the system under test. + +```csharp +[Theory] +[AutoNSubstituteData] +public void Should_Persist_Order( + [Frozen] IOrderRepository repository, + Order order, + OrderService sut) +{ + sut.Save(order); + + repository.Received(1).Save(order); +} +``` + +### Assert a substitute was called with a specific argument + +`ReceivedCallWithArgument` returns the single argument of type `T` across all received calls, so you can assert on it directly instead of writing an `Arg.Is` predicate. It fails if there is not exactly one. + +```csharp +sut.Save(order); + +var saved = repository.ReceivedCallWithArgument(); + +saved.Status.Should().Be(OrderStatus.Pending); +``` + +Use `ReceivedCallsWithArguments` when several arguments of that type are expected; it returns them all. + +### Wait for an asynchronous call + +For code that dispatches work in the background, `WaitForCall` waits until the call arrives or the timeout elapses. + +```csharp +await handler.WaitForCall(x => x.Handle(Arg.Any())); +``` + +`WaitForCallForAnyArgs` is the argument-agnostic variant. Both default to a 5 second timeout, accept an explicit `TimeSpan`, and throw on timeout. + +### Stop a hanging test from blocking the suite + +`AddTimeout` fails a task that never completes, instead of letting the run hang until the framework kills it. + +```csharp +var result = await sut.ProcessAsync().AddTimeout(); + +await sut.StartAsync().AddTimeout(TimeSpan.FromSeconds(30)); +``` + +The default is 5 seconds. When a debugger is attached the timeout is **ignored**, so stepping through a test does not trip a `TimeoutException`. + +To await several tasks and collect their results, `AwaitTasks` reads more naturally than `Task.WhenAll`: + +```csharp +var results = await new[] { first, second, third }.AwaitTasks(); +``` + +### Freeze the clock + +`TimeProvider` resolves to a provider reporting a fixed instant, so time-dependent code is deterministic and the test can assert against the same value. + +```csharp +[Theory] +[AutoNSubstituteData] +public void Should_Stamp_Creation_Time( + [Frozen] TimeProvider timeProvider, + Order order, + OrderService sut) +{ + sut.Create(order); + + order.CreatedUtc.Should().Be(timeProvider.GetUtcNow()); +} +``` + +### Control generation for your own type + +Any `ICustomization` or `ISpecimenBuilder` marked with `[AutoRegister]` is picked up automatically — no registration call anywhere. + +```csharp +[AutoRegister] +public class PositiveAmountCustomization : ICustomization +{ + public void Customize(IFixture fixture) + => fixture.Customize(c => c.FromFactory(() => new Amount(100))); +} +``` + +### Compare objects containing timestamps + +Round-tripped timestamps rarely match to the tick. `CompareDateTimeUsingCloseTo` relaxes the comparison. + +```csharp +actual.Should().BeEquivalentTo( + expected, + o => o.CompareDateTimeUsingCloseTo()); +``` + +The default precision is 1000 ms. Pass an `int` for a different millisecond precision, or a `TimeSpan` when that reads better: + +```csharp +o => o.CompareDateTimeUsingCloseTo(precision: 500) +o => o.CompareDateTimeUsingCloseTo(TimeSpan.FromSeconds(2)) +``` + +### Compare strings ignoring formatting + +```csharp +actualJson.Should().HaveSimilarJsonAs(expectedJson); +actualXml.Should().HaveSimilarXmlAs(expectedXml); +actualText.Should().HaveSimilarContentAs(expectedText); +``` + +## Built-in Specimen Support + +`FixtureFactory.Create()` — and therefore every data attribute — handles the following out of the box. Everything else falls back to standard AutoFixture behaviour. + +| Type | Behaviour | +|------|-----------| +| `CancellationToken` | A token that has **not** been canceled. | +| `DateOnly` | Derived from a generated `DateTime`. | +| `TimeOnly` | Derived from a generated `DateTime`. | +| `Uri` | A readable absolute URI on the reserved `example.org` domain. | +| `TimeProvider` | A provider reporting a fixed, generated UTC instant (`LocalTimeZone` is UTC). Combine with `[Frozen]` to share the instant. | +| `ImmutableArray`, `ImmutableList`, `ImmutableHashSet`, `ImmutableSortedSet`, `ImmutableDictionary`, `ImmutableSortedDictionary` | Populated by generating a mutable counterpart and converting it. | +| Interfaces / abstract classes | Substituted with NSubstitute. | +| Recursive types | Recursion is omitted rather than throwing — the recursive member is left `null`. | + ## Advanced Usage ### Frozen Reuse Scenarios @@ -243,6 +429,8 @@ public class GuidCustomization : ICustomization } ``` +The decorated type must expose a parameterless constructor. Discovery scans the assemblies loaded into the current `AppDomain`, so customizations declared in your test project are found without any configuration. + ### Helper Extensions | Helper | Purpose | @@ -251,8 +439,62 @@ public class GuidCustomization : ICustomization | `SubstituteExtensions` | Inspect substitutes, wait for calls, retrieve arguments. | | `TaskExtensions` | Await with timeouts. | | `ObjectExtensions` | Access protected members via reflection helpers. | +| `StringExtensions` | Compare text, XML and JSON disregarding formatting. | | `FixtureFactory` | Central factory returning a consistently customized `IFixture`. | +## Troubleshooting / FAQ + +**`NotSupportedException` when a fixture is created** +A type marked `[AutoRegister]` implements neither `ICustomization` nor `ISpecimenBuilder`. Implement one of them, or remove the attribute. + +**My `[AutoRegister]` type is never applied** +It must expose a parameterless constructor, and its assembly must be loaded. Types in your test project are loaded automatically; types in a separate assembly that nothing references may not be. + +**`[Frozen]` did not reuse the instance from my member data** +Promotion matches the **exact** declared parameter type. A value supplied as `DualImpl` will not be promoted into a `[Frozen] IFoo` parameter. Declare the supplying parameter with the same type as the frozen one. + +**Class data does not promote frozen values, but member data does** +This is intentional. Class data is normally fully positional, where implicit promotion would hide mistakes; member data commonly supplies only a leading subset. Use `MemberAutoNSubstituteData` when you want promotion. + +**A property on my generated object is unexpectedly `null`** +The type is recursive. `Atc.Test` replaces AutoFixture's throwing recursion behaviour with `OmitOnRecursionBehavior`, so the recursive member is omitted instead of failing the test. + +**Build errors about missing xUnit types** +`Atc.Test` does not bring in the `xunit.v3` meta-package by design. Add an explicit `` to your test project. + +**My timestamps differ by a few ticks in `BeEquivalentTo`** +Use `o => o.CompareDateTimeUsingCloseTo()`, or freeze `TimeProvider` so both sides derive from the same instant. + +## Why Atc.Test + +> You can “just wire everything manually” with plain xUnit and hand‑rolled mocks—so why use this instead? + +| Problem Without | What You Gain With `Atc.Test` | Why It Matters Over Time | +|-----------------|-------------------------------|--------------------------| +| Repeating constructor/mocker boilerplate in every test | Parameter-only intent: you list just what the test cares about | Lower cognitive load; faster review – noise removed | +| Fragile refactors (add a ctor param ⇒ touch many files) | Fixture-driven auto‑supply of new dependencies | Constructor churn becomes O(1) instead of O(N tests) | +| Divergent ad‑hoc mock styles (naming, setup order) | Central factory + consistent frozen reuse semantics | Suite stays uniform; easier large-scale edits / audits | +| Accidental duplicate substitutes for logically single collaborator | `[Frozen]` exact-type reuse + early supplied promotion (member data) | Prevents subtle mismatch bugs & expectation gaps | +| Manual re-creation of “shared conventions” (recursion handling, generators) | One-time customization via `[AutoRegister]` | New test inherits standards automatically | +| AI-generated setup drifts over time | Declarative attributes act as a stable policy layer | Reduces maintenance & future prompt dependency | + +### When It Delivers the Most Value + +* Mid/large test suites (hundreds+ of theory cases). +* Domain services with evolving constructor graphs / dependencies. +* Teams that value refactor safety and consistent test style. +* Situations where only a few parameters per test truly matter. + +### When Bare xUnit (+ manual mocks) May Be Enough + +* Very small or short‑lived codebases. +* Highly bespoke object graphs where you override almost every generated value anyway. +* Educational contexts emphasizing explicit wiring for learning. + +### Summary + +`Atc.Test` trades a tiny amount of initial abstraction for compounding savings in refactors, readability, and consistency. AI can quickly generate boilerplate; this library’s value is eliminating the need for that boilerplate in the first place—and giving you a single, policy‑driven locus for customization and reuse. + ## Requirements | Aspect | Value | diff --git a/docs/working-with.md b/docs/working-with.md new file mode 100644 index 0000000..036c88d --- /dev/null +++ b/docs/working-with.md @@ -0,0 +1,395 @@ +# Working With Atc.Test + +A long-form guide to using `Atc.Test` in a real test suite. The [README](../README.md) covers +installation and the fastest path to a first passing test; this document goes deeper into the +data attributes, `[Frozen]` semantics, customization, and the helper extensions. + +## Table of Content + +* [The Problem This Solves](#the-problem-this-solves) +* [Data Attributes](#data-attributes) + * [AutoNSubstituteData](#autonsubstitutedata) + * [InlineAutoNSubstituteData](#inlineautonsubstitutedata) + * [MemberAutoNSubstituteData](#memberautonsubstitutedata) + * [ClassAutoNSubstituteData](#classautonsubstitutedata) + * [Choosing Between Them](#choosing-between-them) +* [The Frozen Attribute](#the-frozen-attribute) + * [Basic Frozen Reuse](#basic-frozen-reuse) + * [Positional Frozen Reuse](#positional-frozen-reuse) + * [Exact-Type Promotion](#exact-type-promotion) + * [What Is Not Promoted](#what-is-not-promoted) +* [Auto-Registration of Customizations](#auto-registration-of-customizations) +* [The FixtureFactory](#the-fixturefactory) +* [Helper Extensions](#helper-extensions) +* [Where to Go Next](#where-to-go-next) + +## The Problem This Solves + +Without a library like this, tests accumulate noise. Every test method wires up its own fixture, +freezes the right dependencies, creates substitutes, and constructs the system under test. With an +evolving constructor graph this becomes a maintenance burden: adding a single constructor parameter +can cascade into touching dozens or hundreds of test files. + +`Atc.Test` applies one principle — **you list only the parameters that matter to your test.** +Everything else is generated, substituted, and wired up for you. + +The practical consequences: + +* Constructor churn becomes **O(1)** instead of **O(N tests)**. Add a dependency; the fixture supplies it. +* Divergent per-developer mock styles are replaced by one factory and one set of reuse semantics. +* Duplicate substitutes for a logically single collaborator are prevented by `[Frozen]`. +* Shared conventions — recursion handling, custom generators — are registered once and inherited everywhere. + +This pays off most in mid-to-large suites, in domain services with complex constructor graphs, and on +teams that care about refactor safety. For a very small or short-lived codebase, plain xUnit with +hand-rolled mocks may be enough. + +## Data Attributes + +Four attributes cover the common scenarios. Picking the right one is most of the learning curve. + +### AutoNSubstituteData + +The workhorse. Every parameter is auto-generated; interfaces and abstract classes become NSubstitute +substitutes. + +```csharp +[Theory] +[AutoNSubstituteData] +public void GetValue_ShouldReturnDataFromService( + [Frozen] IMyService service, + MyController sut) +{ + service.GetValue().Returns(42); + + var result = sut.Get(); + + result.Should().Be(42); +} +``` + +`service` is a substitute for `IMyService`. Because it is `[Frozen]`, the *same* instance is injected +into `MyController`, so the `Returns` setup is visible to the system under test. You never write +`Substitute.For()` or `new Fixture()`. + +### InlineAutoNSubstituteData + +Supply specific values for the leading parameters and let AutoFixture generate the rest — the +auto-mocking equivalent of xUnit's `[InlineData]`. + +```csharp +[Theory] +[InlineAutoNSubstituteData(10, 20)] +[InlineAutoNSubstituteData(5, 5)] +[InlineAutoNSubstituteData(0, -1)] +public void Add_ShouldWorkWithSpecificValues(int a, int b, Calculator sut) + => sut.Add(a, b).Should().Be(a + b); +``` + +Inline values are assigned to parameters in order. Anything left over is auto-generated. Use this for +boundary conditions and edge cases. + +### MemberAutoNSubstituteData + +For test data that cannot be expressed as compile-time constants. Works like `[MemberData]`, but +augments the supplied rows with generated specimens. + +```csharp +public static IEnumerable TestCases() +{ + yield return new object?[] { 1, 2, 3 }; + yield return new object?[] { 10, 20, 30 }; + yield return new object?[] { -1, 1, 0 }; +} + +[Theory] +[MemberAutoNSubstituteData(nameof(TestCases))] +public void Add_ShouldReturnExpectedResult( + int a, + int b, + int expected, + Calculator sut) + => sut.Add(a, b).Should().Be(expected); +``` + +The member supplies `a`, `b` and `expected`; `sut` is generated. This attribute is also the only one +that supports [exact-type promotion](#exact-type-promotion). + +### ClassAutoNSubstituteData + +For test data complex enough to deserve its own type — construction logic, conditional rows, or state +shared between cases. + +```csharp +public class CalculatorTestCases : IEnumerable +{ + public IEnumerator GetEnumerator() + { + yield return new object[] { 1, 2, 3 }; + yield return new object[] { 10, 20, 30 }; + yield return new object[] { -1, 1, 0 }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +[Theory] +[ClassAutoNSubstituteData(typeof(CalculatorTestCases))] +public void Add_ShouldReturnExpectedResult(int a, int b, int expected, Calculator sut) + => sut.Add(a, b).Should().Be(expected); +``` + +### Choosing Between Them + +| Situation | Attribute | +|-----------|-----------| +| Nothing needs to be pinned | `AutoNSubstituteData` | +| A few compile-time constant cases | `InlineAutoNSubstituteData` | +| Cases need runtime construction, or you want frozen promotion | `MemberAutoNSubstituteData` | +| Cases warrant a reusable, self-contained type | `ClassAutoNSubstituteData` | + +## The Frozen Attribute + +Freezing means: *the same instance is reused for every other parameter in this test that needs that +type.* It is what makes "set up a mock, then verify the SUT used it" work. + +### Basic Frozen Reuse + +```csharp +[Theory] +[AutoNSubstituteData] +public void Handle_ShouldUseFrozenDependency( + [Frozen] IMyService service, + MyHandler sut) +{ + service.DoWork().Returns(true); + + var result = sut.Handle(); + + result.Should().BeTrue(); + service.Received(1).DoWork(); +} +``` + +Without `[Frozen]`, `service` and the `IMyService` injected into `MyHandler` would be **different +instances**, the setup would be invisible to the SUT, and the `Received` assertion would fail. This is +the single most common source of confusion for newcomers. + +### Positional Frozen Reuse + +With `InlineAutoNSubstituteData` and `ClassAutoNSubstituteData`, a value supplied at the same index as +a `[Frozen]` parameter is frozen and reused. + +```csharp +[Theory] +[InlineAutoNSubstituteData(42)] +public void Positional_Frozen_Reuses_Inline_Value( + [Frozen] int number, + SomeConsumer consumer) +{ + consumer.NumberDependency.Should().Be(number); +} +``` + +The inline `42` sits at index 0, mapping to `[Frozen] int number`, and is then reused when constructing +`SomeConsumer`. + +### Exact-Type Promotion + +`MemberAutoNSubstituteData` adds a capability the other attributes do not have. If the member row +supplies a value for an earlier parameter, and a *later* parameter is marked `[Frozen]` with the same +exact type, the earlier value is promoted rather than a new specimen being created. + +```csharp +public static IEnumerable ServiceRow() +{ + yield return new object?[] { Substitute.For() }; +} + +[Theory] +[MemberAutoNSubstituteData(nameof(ServiceRow))] +public void Promotion_Reuses_Earlier_Same_Type( + IMyService supplied, + [Frozen] IMyService frozenLater, + NeedsService consumer) +{ + frozenLater.Should().BeSameAs(supplied); + consumer.Service.Should().BeSameAs(supplied); +} +``` + +This exists because member rows commonly supply only a leading subset of parameters. Class data is +normally fully positional, where implicit promotion would hide mistakes — so it is deliberately not +applied there. + +### What Is Not Promoted + +Promotion requires an **exact declared type match**. A concrete instance implementing two interfaces is +not cross-promoted between them. + +```csharp +public interface IFoo { } +public interface IBar { } +public class DualImpl : IFoo, IBar { } + +public static IEnumerable DualRow() +{ + yield return new object?[] { new DualImpl() }; +} + +[Theory] +[MemberAutoNSubstituteData(nameof(DualRow))] +public void Different_Interface_Not_Promoted( + IFoo foo, + [Frozen] IBar bar, + UsesBar consumer) +{ + bar.Should().NotBeSameAs(foo); + consumer.Bar.Should().BeSameAs(bar); +} +``` + +Even though `DualImpl` implements both interfaces, the frozen `IBar` does not reuse it. This prevents +cross-interface bleed and the subtle bugs that follow. If you *want* reuse, declare the supplying +parameter with the same type as the frozen one. + +## Auto-Registration of Customizations + +Any `ICustomization` or `ISpecimenBuilder` decorated with `[AutoRegister]` is discovered and applied to +every fixture the library creates. + +```csharp +[AutoRegister] +public class GuidCustomization : ICustomization +{ + public void Customize(IFixture fixture) + => fixture.Register(() => Guid.NewGuid()); +} +``` + +No registration call anywhere. This is the mechanism for establishing project-wide testing conventions: + +* Default values for domain types AutoFixture cannot construct +* Recursion guards for awkward object graphs +* Realistic string generation instead of random GUID-like noise +* Custom builders for types with non-trivial construction + +Two constraints worth knowing: + +* The decorated type **must have a parameterless constructor**. +* Discovery scans assemblies loaded into the current `AppDomain`. Types in your test project are found + automatically; a customization in a separate assembly that nothing references may not be loaded yet. + +A type marked `[AutoRegister]` that implements neither interface throws `NotSupportedException` when a +fixture is created. + +## The FixtureFactory + +Every data attribute calls `FixtureFactory.Create()`, which applies three customizations: + +1. **`RecursionCustomization`** — replaces AutoFixture's throwing behaviour with `OmitOnRecursionBehavior`, + so a circular reference is omitted rather than failing the test. This is why a property on a generated + object is sometimes unexpectedly `null`. +2. **`AutoRegisterCustomization`** — discovers and applies everything marked `[AutoRegister]`. +3. **`AutoNSubstituteCustomization`** — configures NSubstitute for interfaces and abstract classes, with + `ConfigureMembers = false` and `GenerateDelegates = true`. + +You can call it directly when you need a fixture outside the attributes: + +```csharp +[Fact] +public void Manual_Fixture_Example() +{ + var fixture = FixtureFactory.Create(); + var sut = fixture.Create(); + + sut.Handle().Should().NotBeNull(); +} +``` + +## Helper Extensions + +| Helper | Purpose | +|--------|---------| +| `EquivalencyAssertionOptionsExtensions` | Date precision and `JsonElement` handling for `BeEquivalentTo`. | +| `SubstituteExtensions` | Inspect substitutes, retrieve arguments, wait for calls. | +| `TaskExtensions` | Await with timeouts; await many tasks. | +| `ObjectExtensions` | Reach protected members via reflection. | +| `StringExtensions` | Compare text, XML and JSON ignoring formatting. | +| `FixtureFactory` | The central, consistently customized `IFixture`. | + +### Equivalency options + +```csharp +actual.Should().BeEquivalentTo(expected, o => o.CompareDateTimeUsingCloseTo()); +``` + +`CompareDateTimeUsingCloseTo` defaults to 1000 ms precision and covers both `DateTime` and +`DateTimeOffset`. Override with an `int` (milliseconds) or a `TimeSpan`: + +```csharp +o => o.CompareDateTimeUsingCloseTo(precision: 500) +o => o.CompareDateTimeUsingCloseTo(TimeSpan.FromSeconds(2)) +``` + +`CompareJsonElementUsingJson()` compares `JsonElement` values by their underlying JSON representation +rather than attempting to compare the struct directly. + +### Substitute inspection + +```csharp +sut.Process("hello"); + +var argument = service.ReceivedCallWithArgument(); +argument.Should().Be("hello"); +``` + +`ReceivedCallWithArgument` asserts that **exactly one** argument of type `T` was received across all +calls and returns it. When several are expected, use `ReceivedCallsWithArguments`, which asserts the +collection is non-empty and returns all of them. + +Both accept the usual `because` / `becauseArgs` phrasing used throughout AwesomeAssertions. + +### Waiting for asynchronous calls + +When the call happens on a background thread or after an event: + +```csharp +sut.StartBackgroundProcessing(); + +await service.WaitForCall(x => x.DoWork()); +``` + +The default wait is 5 seconds; pass a `TimeSpan` to change it. Overloads exist for `Action`, +`Func` and `Func>`. Use `WaitForCallForAnyArgs` to ignore arguments. + +### Task helpers + +```csharp +var result = await someTask.AddTimeout(TimeSpan.FromSeconds(5)); +``` + +`AddTimeout` throws `TimeoutException` if the task does not complete in time, defaulting to 5 seconds. +**When a debugger is attached the timeout is ignored**, so stepping through a test does not spuriously +fail it. + +```csharp +var results = await new[] { task1, task2, task3 }.AwaitTasks(); +``` + +`AwaitTasks` wraps `Task.WhenAll` in a form that reads better in a test. + +### Protected members + +```csharp +var result = sut.InvokeProtectedMethod("CalculateInternal", input); +``` + +`HasProperties()` guards `BeEquivalentTo`, which throws for objects with no properties: + +```csharp +if (obj.HasProperties()) +{ + obj.Should().BeEquivalentTo(expected); +} +``` \ No newline at end of file diff --git a/global.json b/global.json index 86298f9..831e976 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,9 @@ { - "sdk": { - "rollForward": "latestMajor", - "allowPrerelease": false - } + "sdk": { + "rollForward": "latestMajor", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } } \ No newline at end of file diff --git a/src/Atc.Test/Atc.Test.csproj b/src/Atc.Test/Atc.Test.csproj index c087cdc..a2e5d0d 100644 --- a/src/Atc.Test/Atc.Test.csproj +++ b/src/Atc.Test/Atc.Test.csproj @@ -1,4 +1,4 @@ - + Atc.Test @@ -18,11 +18,16 @@ + - - - + + + \ No newline at end of file diff --git a/src/Atc.Test/Customizations/AutoRegisterAttribute.cs b/src/Atc.Test/Customizations/AutoRegisterAttribute.cs index fe2f311..1944eca 100644 --- a/src/Atc.Test/Customizations/AutoRegisterAttribute.cs +++ b/src/Atc.Test/Customizations/AutoRegisterAttribute.cs @@ -1,5 +1,30 @@ namespace Atc.Test.Customizations; +/// +/// Marks a class for automatic registration on the +/// created by . +/// +/// +/// The decorated class must implement either or +/// and expose a parameterless constructor. +/// Decorating a class that implements neither results in a +/// when the fixture is created. +/// +/// Discovery is performed by , which scans the +/// assemblies loaded into the current . Types in a test project +/// referencing Atc.Test are therefore picked up without any further configuration. +/// +/// +/// +/// +/// [AutoRegister] +/// public class GuidCustomization : ICustomization +/// { +/// public void Customize(IFixture fixture) +/// => fixture.Register(() => Guid.NewGuid()); +/// } +/// +/// [AttributeUsage(AttributeTargets.Class)] public sealed class AutoRegisterAttribute : Attribute { diff --git a/src/Atc.Test/Customizations/Generators/DateOnlyGenerator.cs b/src/Atc.Test/Customizations/Generators/DateOnlyGenerator.cs index e7fbd02..af2b9c5 100644 --- a/src/Atc.Test/Customizations/Generators/DateOnlyGenerator.cs +++ b/src/Atc.Test/Customizations/Generators/DateOnlyGenerator.cs @@ -2,7 +2,7 @@ namespace Atc.Test.Customizations.Generators; /// /// Responsible for generating instances -/// that has not been canceled. +/// derived from a generated . /// [AutoRegister] public class DateOnlyGenerator : ISpecimenBuilder diff --git a/src/Atc.Test/Customizations/Generators/TimeOnlyGenerator.cs b/src/Atc.Test/Customizations/Generators/TimeOnlyGenerator.cs new file mode 100644 index 0000000..c52408b --- /dev/null +++ b/src/Atc.Test/Customizations/Generators/TimeOnlyGenerator.cs @@ -0,0 +1,22 @@ +namespace Atc.Test.Customizations.Generators; + +/// +/// Responsible for generating instances +/// derived from a generated . +/// +[AutoRegister] +public class TimeOnlyGenerator : ISpecimenBuilder +{ + /// + public object Create( + object request, + ISpecimenContext context) + { + if (!request.IsRequestFor()) + { + return new NoSpecimen(); + } + + return TimeOnly.FromDateTime(context.Create()); + } +} diff --git a/src/Atc.Test/Customizations/Generators/TimeProviderGenerator.cs b/src/Atc.Test/Customizations/Generators/TimeProviderGenerator.cs new file mode 100644 index 0000000..0c719ad --- /dev/null +++ b/src/Atc.Test/Customizations/Generators/TimeProviderGenerator.cs @@ -0,0 +1,43 @@ +namespace Atc.Test.Customizations.Generators; + +/// +/// Responsible for generating deterministic instances +/// that report a fixed point in time. +/// +/// +/// Without this generator a parameter would be substituted +/// as an abstract class, which yields a provider returning default timestamps. +/// The generated provider reports a stable, fixture-generated UTC time, which makes +/// assertions on time-dependent code deterministic. Combine with [Frozen] to +/// share the same instant across the system under test and the assertions. +/// +[AutoRegister] +public class TimeProviderGenerator : ISpecimenBuilder +{ + /// + public object Create( + object request, + ISpecimenContext context) + { + if (!request.IsRequestFor()) + { + return new NoSpecimen(); + } + + return new FixedTimeProvider(context.Create()); + } + + private sealed class FixedTimeProvider : TimeProvider + { + private readonly DateTimeOffset utcNow; + + public FixedTimeProvider(DateTime dateTime) + => utcNow = new DateTimeOffset(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); + + public override DateTimeOffset GetUtcNow() + => utcNow; + + public override TimeZoneInfo LocalTimeZone + => TimeZoneInfo.Utc; + } +} diff --git a/src/Atc.Test/Customizations/Generators/UriGenerator.cs b/src/Atc.Test/Customizations/Generators/UriGenerator.cs new file mode 100644 index 0000000..0fa7512 --- /dev/null +++ b/src/Atc.Test/Customizations/Generators/UriGenerator.cs @@ -0,0 +1,41 @@ +namespace Atc.Test.Customizations.Generators; + +/// +/// Responsible for generating readable and deterministic absolute +/// instances using the reserved example.org domain. +/// +[AutoRegister] +public class UriGenerator : ISpecimenBuilder +{ + /// + public object Create( + object request, + ISpecimenContext context) + { + if (!request.IsRequestFor()) + { + return new NoSpecimen(); + } + + var segment = Sanitize(context.Create()); + + return new Uri($"https://example.org/{segment}"); + } + + private static string Sanitize(string value) + { + var sb = new StringBuilder(value.Length); + + foreach (var c in value) + { + if (char.IsAsciiLetterOrDigit(c) || c is '-') + { + sb.Append(char.ToLowerInvariant(c)); + } + } + + return sb.Length == 0 + ? Guid.NewGuid().ToString("N") + : sb.ToString(); + } +} diff --git a/src/Atc.Test/Customizations/ImmutableObjectCustomization.cs b/src/Atc.Test/Customizations/ImmutableObjectCustomization.cs index 6b11356..fae7d40 100644 --- a/src/Atc.Test/Customizations/ImmutableObjectCustomization.cs +++ b/src/Atc.Test/Customizations/ImmutableObjectCustomization.cs @@ -1,8 +1,26 @@ namespace Atc.Test.Customizations; +/// +/// Enables generation of the immutable collection types in +/// , which AutoFixture cannot create +/// out of the box because they have no public constructor. +/// +/// +/// Each supported immutable type is generated by first resolving a populated mutable +/// counterpart (for example for ) +/// and then converting it, so the resulting collections follow the same population +/// rules as any other generated collection. +/// +/// Supported: , , +/// , , +/// and +/// . +/// +/// [AutoRegister] public class ImmutableObjectCustomization : ICustomization { + /// public void Customize(IFixture fixture) { fixture.Customizations.Add( @@ -42,12 +60,20 @@ public void Customize(IFixture fixture) o => ImmutableSortedDictionary.ToImmutableSortedDictionary(o))); } + /// + /// Creates instances of a single immutable collection type by resolving a mutable + /// counterpart and converting it. + /// + /// The open generic immutable type to handle, for example typeof(ImmutableArray<>). + /// The open generic mutable type to resolve from the fixture. + /// Converts the resolved mutable instance into the immutable one. private sealed class ImmutableObjectBuilder( Type immutableType, Type underlyingType, Func converter) : ISpecimenBuilder { + /// public object Create( object request, ISpecimenContext context) diff --git a/src/Atc.Test/StringExtensions.cs b/src/Atc.Test/StringExtensions.cs index 816e6a8..6193751 100644 --- a/src/Atc.Test/StringExtensions.cs +++ b/src/Atc.Test/StringExtensions.cs @@ -125,7 +125,7 @@ public static void HaveSimilarJsonAs( } /// - /// Asserts that a string dos not have similar json content disregarding formatting. + /// Asserts that a string does not have similar json content disregarding formatting. /// /// The StringAssertions. /// diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a8f102b..137f3a1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -53,7 +53,7 @@ - + \ No newline at end of file diff --git a/test/.editorconfig b/test/.editorconfig index 3504194..1e7a420 100644 --- a/test/.editorconfig +++ b/test/.editorconfig @@ -20,17 +20,17 @@ # Meziantou # https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm -dotnet_diagnostic.MA0004.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0004.md -dotnet_diagnostic.MA0016.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0016.md +dotnet_diagnostic.MA0004.severity = none # Use Task.ConfigureAwait(false) +dotnet_diagnostic.MA0016.severity = none # Prefer return collection abstraction over implementation dotnet_diagnostic.MA0051.severity = none # Method Length # Microsoft - Code Analysis # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ -dotnet_diagnostic.CA1068.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1068.md -dotnet_diagnostic.CA1602.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1602.md -dotnet_diagnostic.CA1707.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1707.md -dotnet_diagnostic.CA2007.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA2007.md +dotnet_diagnostic.CA1068.severity = none # CancellationToken parameters must come last +dotnet_diagnostic.CA1602.severity = none +dotnet_diagnostic.CA1707.severity = none # Identifiers should not contain underscores +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task # Microsoft - Compiler Errors @@ -43,8 +43,8 @@ dotnet_diagnostic.CA2007.severity = none # https://github.com/atc-net # StyleCop # https://github.com/DotNetAnalyzers/StyleCopAnalyzers -dotnet_diagnostic.SA1122.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1122.md -dotnet_diagnostic.SA1133.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1133.md +dotnet_diagnostic.SA1122.severity = none # Use string.Empty for empty strings +dotnet_diagnostic.SA1133.severity = none # Do not combine attributes # SonarAnalyzer.CSharp diff --git a/test/Atc.Test.Tests/Atc.Test.Tests.csproj b/test/Atc.Test.Tests/Atc.Test.Tests.csproj index cd4eb2e..f97b7d0 100644 --- a/test/Atc.Test.Tests/Atc.Test.Tests.csproj +++ b/test/Atc.Test.Tests/Atc.Test.Tests.csproj @@ -1,4 +1,4 @@ - + Exe @@ -12,7 +12,7 @@ - + diff --git a/test/Atc.Test.Tests/CustomizationsTests.cs b/test/Atc.Test.Tests/CustomizationsTests.cs index d5bfa1b..6fbb5c3 100644 --- a/test/Atc.Test.Tests/CustomizationsTests.cs +++ b/test/Atc.Test.Tests/CustomizationsTests.cs @@ -41,6 +41,76 @@ public void DateOnlyGenerator_Should_Create_A_Populated_DateOnly() date.Should().NotBe(default); } + [Fact] + public void TimeOnlyGenerator_Should_Create_A_Populated_TimeOnly() + { + // Arrange + var fixture = FixtureFactory.Create(); + + // Act + var time = fixture.Create(); + + // Assert + time.Should().NotBe(default); + } + + [Fact] + public void UriGenerator_Should_Create_An_Absolute_Uri() + { + // Arrange + var fixture = FixtureFactory.Create(); + + // Act + var uri = fixture.Create(); + + // Assert + uri.IsAbsoluteUri.Should().BeTrue(); + uri.Scheme.Should().Be(Uri.UriSchemeHttps); + uri.Host.Should().Be("example.org"); + } + + [Fact] + public void UriGenerator_Should_Create_Unique_Uris() + { + // Arrange + var fixture = FixtureFactory.Create(); + + // Act + var first = fixture.Create(); + var second = fixture.Create(); + + // Assert + first.Should().NotBe(second); + } + + [Fact] + public void TimeProviderGenerator_Should_Create_A_Provider_With_A_Fixed_Non_Default_Time() + { + // Arrange + var fixture = FixtureFactory.Create(); + + // Act + var timeProvider = fixture.Create(); + + // Assert + timeProvider.GetUtcNow().Should().NotBe(default); + timeProvider.GetUtcNow().Should().Be(timeProvider.GetUtcNow()); + } + + [Fact] + public void TimeProviderGenerator_Should_Support_Frozen_Reuse() + { + // Arrange + var fixture = FixtureFactory.Create(); + var frozen = fixture.Freeze(); + + // Act + var resolved = fixture.Create(); + + // Assert + resolved.Should().BeSameAs(frozen); + } + [Fact] public void ImmutableObjectCustomization_Should_Create_A_Populated_ImmutableArray() { diff --git a/test/Atc.Test.Tests/TaskExtensionsTests.cs b/test/Atc.Test.Tests/TaskExtensionsTests.cs index 4dfdb3e..e41c5eb 100644 --- a/test/Atc.Test.Tests/TaskExtensionsTests.cs +++ b/test/Atc.Test.Tests/TaskExtensionsTests.cs @@ -76,7 +76,7 @@ public Task AddTimeout_Should_Throw_When_Timed_Out() public Task AddTimeout_Should_Throw_For_Null_Task() { // Arrange - Task task = null!; + Task task = null; // Act var act = () => task.AddTimeout(); diff --git a/test/Atc.Test.Tests/TestSupport/Models/RecursiveNode.cs b/test/Atc.Test.Tests/TestSupport/Models/RecursiveNode.cs index 56a3ea8..101924d 100644 --- a/test/Atc.Test.Tests/TestSupport/Models/RecursiveNode.cs +++ b/test/Atc.Test.Tests/TestSupport/Models/RecursiveNode.cs @@ -3,5 +3,5 @@ namespace Atc.Test.Tests.TestSupport.Models; public class RecursiveNode { - public RecursiveNode Child { get; set; } = null!; + public RecursiveNode Child { get; set; } } \ No newline at end of file diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 774954f..f2ff047 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -12,7 +12,7 @@ - +