diff --git a/Directory.Packages.props b/Directory.Packages.props index 7754e07..60f2432 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,6 +23,7 @@ + diff --git a/GeneratorTests/Moq.AutoMock.Generator.DisableTests/GeneratorDisableTests.cs b/GeneratorTests/Moq.AutoMock.Generator.DisableTests/GeneratorDisableTests.cs index aef3c6e..c263d8a 100644 --- a/GeneratorTests/Moq.AutoMock.Generator.DisableTests/GeneratorDisableTests.cs +++ b/GeneratorTests/Moq.AutoMock.Generator.DisableTests/GeneratorDisableTests.cs @@ -59,6 +59,17 @@ public void ApplicationInsightsGenerator_WhenDisabled_ExtensionMethodDoesNotExis "WithApplicationInsights extension method should not exist when EnableMoqAutoMockerApplicationInsightsGenerator=false"); } + [Fact] + public void FakeTimeProviderGenerator_WhenDisabled_ExtensionMethodDoesNotExist() + { + // When EnableMoqAutoMockerFakeTimeProviderGenerator=false, the fake time provider + // extension methods should not be generated + + bool hasGeneratedTimeProviderExtension = HasExtensionMethod("WithFakeTimeProvider"); + Assert.False(hasGeneratedTimeProviderExtension, + "WithFakeTimeProvider extension method should not exist when EnableMoqAutoMockerFakeTimeProviderGenerator=false"); + } + /// /// Checks if an extension method with the given name exists for the AutoMocker type /// in the current assembly (generated code is added to the consuming project). diff --git a/GeneratorTests/Moq.AutoMock.Generator.DisableTests/Moq.AutoMock.Generator.DisableTests.csproj b/GeneratorTests/Moq.AutoMock.Generator.DisableTests/Moq.AutoMock.Generator.DisableTests.csproj index bcfc18d..c8e36ac 100644 --- a/GeneratorTests/Moq.AutoMock.Generator.DisableTests/Moq.AutoMock.Generator.DisableTests.csproj +++ b/GeneratorTests/Moq.AutoMock.Generator.DisableTests/Moq.AutoMock.Generator.DisableTests.csproj @@ -12,6 +12,7 @@ false false false + false @@ -20,6 +21,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -56,6 +58,7 @@ + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/ControllerWithTimeProviderTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/ControllerWithTimeProviderTests.cs new file mode 100644 index 0000000..fd77d81 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/ControllerWithTimeProviderTests.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Time.Testing; + +namespace Moq.AutoMock.Generator.Example.MSTest; + +[TestClass] +public class ControllerWithTimeProviderTests +{ + [TestMethod] + public void CreateInstance_WithFakeTimeProvider_CreatesController() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + Assert.IsNotNull(controller); + } + + [TestMethod] + public void CreateInstance_WithFakeTimeProvider_UsesControlledTime() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + var fakeTimeProvider = mocker.Get(); + var startTime = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero); + fakeTimeProvider.SetUtcNow(startTime); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + Assert.AreEqual(startTime, controller.GetCurrentTime()); + + fakeTimeProvider.Advance(TimeSpan.FromHours(1)); + + Assert.AreEqual(startTime.AddHours(1), controller.GetCurrentTime()); + } +} diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj index a391ec9..48417d9 100644 --- a/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj @@ -11,6 +11,7 @@ + all diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/ControllerWithTimeProviderTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/ControllerWithTimeProviderTests.cs new file mode 100644 index 0000000..f1ac6fe --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/ControllerWithTimeProviderTests.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Time.Testing; +using NUnit.Framework; + +namespace Moq.AutoMock.Generator.Example.NUnit; + +public class ControllerWithTimeProviderTests +{ + [Test] + public void CreateInstance_WithFakeTimeProvider_CreatesController() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + Assert.That(controller, Is.Not.Null); + } + + [Test] + public void CreateInstance_WithFakeTimeProvider_UsesControlledTime() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + var fakeTimeProvider = mocker.Get(); + var startTime = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero); + fakeTimeProvider.SetUtcNow(startTime); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + Assert.That(controller.GetCurrentTime(), Is.EqualTo(startTime)); + + fakeTimeProvider.Advance(TimeSpan.FromHours(1)); + + Assert.That(controller.GetCurrentTime(), Is.EqualTo(startTime.AddHours(1))); + } +} diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj index ac86626..31e88b1 100644 --- a/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj @@ -11,6 +11,7 @@ + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/ControllerWithTimeProviderTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/ControllerWithTimeProviderTests.cs new file mode 100644 index 0000000..356a2d0 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/ControllerWithTimeProviderTests.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Time.Testing; + +namespace Moq.AutoMock.Generator.Example.TUnit; + +public partial class ControllerWithTimeProviderTests +{ + [Test] + public async Task CreateInstance_WithFakeTimeProvider_CreatesController() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + await Assert.That(controller).IsNotNull(); + } + + [Test] + public async Task CreateInstance_WithFakeTimeProvider_UsesControlledTime() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + var fakeTimeProvider = mocker.Get(); + var startTime = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero); + fakeTimeProvider.SetUtcNow(startTime); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + await Assert.That(controller.GetCurrentTime()).IsEqualTo(startTime); + + fakeTimeProvider.Advance(TimeSpan.FromHours(1)); + + await Assert.That(controller.GetCurrentTime()).IsEqualTo(startTime.AddHours(1)); + } +} diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/Moq.AutoMock.Generator.Example.TUnit.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/Moq.AutoMock.Generator.Example.TUnit.csproj index b27792d..e4a50a0 100644 --- a/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/Moq.AutoMock.Generator.Example.TUnit.csproj +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.TUnit/Moq.AutoMock.Generator.Example.TUnit.csproj @@ -11,6 +11,7 @@ + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/ControllerWithTimeProviderTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/ControllerWithTimeProviderTests.cs new file mode 100644 index 0000000..37d47dc --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/ControllerWithTimeProviderTests.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Moq.AutoMock.Generator.Example.xUnit; + +public class ControllerWithTimeProviderTests +{ + [Fact] + public void CreateInstance_WithFakeTimeProvider_CreatesController() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + Assert.NotNull(controller); + } + + [Fact] + public void CreateInstance_WithFakeTimeProvider_UsesControlledTime() + { + AutoMocker mocker = new(); + + mocker.WithFakeTimeProvider(); + + var fakeTimeProvider = mocker.Get(); + var startTime = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero); + fakeTimeProvider.SetUtcNow(startTime); + + ControllerWithTimeProvider controller = mocker.CreateInstance(); + + Assert.Equal(startTime, controller.GetCurrentTime()); + + fakeTimeProvider.Advance(TimeSpan.FromHours(1)); + + Assert.Equal(startTime.AddHours(1), controller.GetCurrentTime()); + } +} diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj index 6c040c4..4828a9c 100644 --- a/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj @@ -11,6 +11,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/GeneratorTests/Moq.AutoMocker.Generator.Example/ControllerWithTimeProvider.cs b/GeneratorTests/Moq.AutoMocker.Generator.Example/ControllerWithTimeProvider.cs new file mode 100644 index 0000000..2b7db73 --- /dev/null +++ b/GeneratorTests/Moq.AutoMocker.Generator.Example/ControllerWithTimeProvider.cs @@ -0,0 +1,13 @@ +namespace Moq.AutoMock.Generator.Example; + +public class ControllerWithTimeProvider +{ + private readonly TimeProvider _timeProvider; + + public ControllerWithTimeProvider(TimeProvider timeProvider) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } + + public DateTimeOffset GetCurrentTime() => _timeProvider.GetUtcNow(); +} diff --git a/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj b/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj index 0cb908c..7635cba 100644 --- a/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj +++ b/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj @@ -10,6 +10,7 @@ + diff --git a/Moq.AutoMock/build/Moq.AutoMock.props b/Moq.AutoMock/build/Moq.AutoMock.props index 4198f60..5299ef8 100644 --- a/Moq.AutoMock/build/Moq.AutoMock.props +++ b/Moq.AutoMock/build/Moq.AutoMock.props @@ -9,5 +9,6 @@ + diff --git a/Moq.AutoMocker.Generators.Tests/CSharpSourceGeneratorVerifier.cs b/Moq.AutoMocker.Generators.Tests/CSharpSourceGeneratorVerifier.cs index 355648c..a67a28f 100644 --- a/Moq.AutoMocker.Generators.Tests/CSharpSourceGeneratorVerifier.cs +++ b/Moq.AutoMocker.Generators.Tests/CSharpSourceGeneratorVerifier.cs @@ -17,6 +17,7 @@ public class Test : CSharpSourceGeneratorTest GetSourceGenerators() protected override Project ApplyCompilationOptions(Project project) { //project.AnalyzerOptions.WithAdditionalFiles(); - if (ReferenceAutoMocker || ReferenceOptionsAbstractions || ReferenceFakeLogging || ReferenceApplicationInsights || ReferenceDependencyInjection || ReferenceOpenTelemetryInMemoryExporter || ReferenceDiagnosticSource) + if (ReferenceAutoMocker || ReferenceOptionsAbstractions || ReferenceFakeLogging || ReferenceFakeTimeProvider || ReferenceApplicationInsights || ReferenceDependencyInjection || ReferenceOpenTelemetryInMemoryExporter || ReferenceDiagnosticSource) { string fullPath = Path.GetFullPath($"{AutoMock.AssemblyName}.dll"); project = project.AddMetadataReference(MetadataReference.CreateFromFile(fullPath)); @@ -80,6 +81,20 @@ protected override Project ApplyCompilationOptions(Project project) } } + if (ReferenceFakeTimeProvider) + { + // Add reference to Microsoft.Extensions.TimeProvider.Testing + try + { + var fakeTimeProviderAssembly = typeof(Microsoft.Extensions.Time.Testing.FakeTimeProvider).Assembly; + project = project.AddMetadataReference(MetadataReference.CreateFromFile(fakeTimeProviderAssembly.Location)); + } + catch + { + // If we can't find the assembly, the test will fail, which is appropriate + } + } + if (ReferenceApplicationInsights) { // Add reference to Microsoft.ApplicationInsights diff --git a/Moq.AutoMocker.Generators.Tests/FakeTimeProviderGeneratorTests.cs b/Moq.AutoMocker.Generators.Tests/FakeTimeProviderGeneratorTests.cs new file mode 100644 index 0000000..2f8ced5 --- /dev/null +++ b/Moq.AutoMocker.Generators.Tests/FakeTimeProviderGeneratorTests.cs @@ -0,0 +1,116 @@ +using Microsoft.CodeAnalysis.Text; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using VerifyCS = Moq.AutoMocker.Generators.Tests.CSharpSourceGeneratorVerifier; +using Microsoft.CodeAnalysis.Testing; + +namespace Moq.AutoMocker.Generators.Tests; + +[TestClass] +public class FakeTimeProviderGeneratorTests +{ + private const string ExpectedFakeTimeProviderGeneratedFile = """ + #nullable enable + // Generated by Moq.AutoMocker.Generators.FakeTimeProviderExtensionSourceGenerator. + // Licensed under the MIT License. See LICENSE in the project root for license information. + namespace Moq.AutoMock + { + using System; + using Microsoft.Extensions.Time.Testing; + + /// + /// This class provides extension methods for interacting with in an instance. + /// + static partial class AutoMockerFakeTimeProviderExtensions + { + /// + /// This method sets up with a from Microsoft.Extensions.Time.Testing, + /// allowing time-dependent components to be tested in a deterministic way. + /// + /// The instance. + /// The same instance passed as parameter, allowing chained calls. + public static AutoMocker WithFakeTimeProvider(this AutoMocker mocker) + { + if (mocker == null) + { + throw new ArgumentNullException(nameof(mocker)); + } + + var timeProvider = new FakeTimeProvider(); + mocker.Use(timeProvider); + mocker.Use(timeProvider); + + return mocker; + } + } + } + """; + + [TestMethod] + public async Task WhenTimeProviderAssemblyIsNotReferenced_NoGenerationOccurs() + { + await new VerifyCS.Test + { + TestCode = "", + ReferenceFakeTimeProvider = false + }.RunAsync(TestContext.CancellationToken); + } + + [TestMethod] + public async Task WhenTimeProviderAssemblyIsReferenced_ExtensionMethodIsGenerated() + { + await new VerifyCS.Test + { + CompilerDiagnostics = CompilerDiagnostics.None, + ReferenceFakeTimeProvider = true, + TestCode = "", + TestState = + { + GeneratedSources = + { + GetSourceFile(ExpectedFakeTimeProviderGeneratedFile, "AutoMockerFakeTimeProviderExtensions.g.cs") + } + } + }.RunAsync(TestContext.CancellationToken); + } + + [TestMethod] + public async Task WhenGeneratorIsDisabled_NoGenerationOccurs() + { + var test = new VerifyCS.Test + { + ReferenceFakeTimeProvider = true, + TestCode = "" + }; + test.SetGlobalOption("build_property.EnableMoqAutoMockerFakeTimeProviderGenerator", "false"); + await test.RunAsync(TestContext.CancellationToken); + } + + [TestMethod] + public async Task WhenGeneratorIsExplicitlyEnabled_ExtensionMethodIsGenerated() + { + var test = new VerifyCS.Test + { + CompilerDiagnostics = CompilerDiagnostics.None, + ReferenceFakeTimeProvider = true, + TestCode = "", + TestState = + { + GeneratedSources = + { + GetSourceFile(ExpectedFakeTimeProviderGeneratedFile, "AutoMockerFakeTimeProviderExtensions.g.cs") + } + } + }; + test.SetGlobalOption("build_property.EnableMoqAutoMockerFakeTimeProviderGenerator", "true"); + await test.RunAsync(TestContext.CancellationToken); + } + + private static (string FileName, SourceText SourceText) GetSourceFile(string content, string fileName) + { + return (Path.Combine("Moq.AutoMocker.Generators", "Moq.AutoMocker.Generators.FakeTimeProviderExtensionSourceGenerator", fileName), SourceText.From(content, Encoding.UTF8, SourceHashAlgorithm.Sha256)); + } + + public TestContext TestContext { get; set; } +} diff --git a/Moq.AutoMocker.Generators.Tests/Moq.AutoMocker.Generators.Tests.csproj b/Moq.AutoMocker.Generators.Tests/Moq.AutoMocker.Generators.Tests.csproj index fdc5ec2..5a04860 100644 --- a/Moq.AutoMocker.Generators.Tests/Moq.AutoMocker.Generators.Tests.csproj +++ b/Moq.AutoMocker.Generators.Tests/Moq.AutoMocker.Generators.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/Moq.AutoMocker.Generators/FakeTimeProviderExtensionSourceGenerator.cs b/Moq.AutoMocker.Generators/FakeTimeProviderExtensionSourceGenerator.cs new file mode 100644 index 0000000..b7e884f --- /dev/null +++ b/Moq.AutoMocker.Generators/FakeTimeProviderExtensionSourceGenerator.cs @@ -0,0 +1,92 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Moq.AutoMocker.Generators; + +[Generator(LanguageNames.CSharp)] +public sealed class FakeTimeProviderExtensionSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Check if the generator is enabled via MSBuild property + IncrementalValueProvider isEnabled = context.AnalyzerConfigOptionsProvider + .Select(static (provider, _) => IsGeneratorEnabled(provider)); + + // Check if Microsoft.Extensions.TimeProvider.Testing is referenced + IncrementalValueProvider referencesFakeTimeProvider = context.CompilationProvider + .Select(static (compilation, _) => ReferencesFakeTimeProvider(compilation.ReferencedAssemblyNames)); + + // Combine both conditions + IncrementalValueProvider shouldGenerate = isEnabled + .Combine(referencesFakeTimeProvider) + .Select(static (tuple, _) => tuple.Left && tuple.Right); + + // Only generate source if enabled and Microsoft.Extensions.TimeProvider.Testing is referenced + context.RegisterSourceOutput(shouldGenerate, static (context, shouldGenerate) => + { + if (shouldGenerate) + { + context.AddSource("AutoMockerFakeTimeProviderExtensions.g.cs", FakeTimeProviderExtensionContent); + } + }); + } + + private static bool IsGeneratorEnabled(AnalyzerConfigOptionsProvider provider) + { + if (provider.GlobalOptions.TryGetValue("build_property.EnableMoqAutoMockerFakeTimeProviderGenerator", out var value)) + { + return !string.Equals(value, "false", StringComparison.OrdinalIgnoreCase); + } + return true; // Enabled by default + } + + private static bool ReferencesFakeTimeProvider(IEnumerable assemblies) + { + foreach (AssemblyIdentity assembly in assemblies) + { + if (assembly.Name.Equals("Microsoft.Extensions.TimeProvider.Testing", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + private const string FakeTimeProviderExtensionContent = + """ + #nullable enable + // Generated by Moq.AutoMocker.Generators.FakeTimeProviderExtensionSourceGenerator. + // Licensed under the MIT License. See LICENSE in the project root for license information. + namespace Moq.AutoMock + { + using System; + using Microsoft.Extensions.Time.Testing; + + /// + /// This class provides extension methods for interacting with in an instance. + /// + static partial class AutoMockerFakeTimeProviderExtensions + { + /// + /// This method sets up with a from Microsoft.Extensions.Time.Testing, + /// allowing time-dependent components to be tested in a deterministic way. + /// + /// The instance. + /// The same instance passed as parameter, allowing chained calls. + public static AutoMocker WithFakeTimeProvider(this AutoMocker mocker) + { + if (mocker == null) + { + throw new ArgumentNullException(nameof(mocker)); + } + + var timeProvider = new FakeTimeProvider(); + mocker.Use(timeProvider); + mocker.Use(timeProvider); + + return mocker; + } + } + } + """; +} diff --git a/docs/SourceGenerators.md b/docs/SourceGenerators.md index 0eda19f..1f423e8 100644 --- a/docs/SourceGenerators.md +++ b/docs/SourceGenerators.md @@ -128,6 +128,28 @@ var service = mocker.CreateInstance(); [Learn more →](SourceGenerators/MeterFactoryExtensionGenerator.md) +### 7. [Fake Time Provider Extension Generator](SourceGenerators/FakeTimeProviderExtensionGenerator.md) + +Generates `WithFakeTimeProvider()` extension method when `Microsoft.Extensions.TimeProvider.Testing` is referenced. + +**Key Features:** +- Provides a `FakeTimeProvider` for deterministic time-based testing +- Registers as both `TimeProvider` (for injection) and `FakeTimeProvider` (for test control) +- Allows advancing time with `Advance()` and `SetUtcNow()` + +**Quick Example:** +```csharp +mocker.WithFakeTimeProvider(); +var fakeTime = mocker.Get(); +fakeTime.SetUtcNow(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)); + +var service = mocker.CreateInstance(); +fakeTime.Advance(TimeSpan.FromHours(1)); +// Verify behavior at specific time +``` + +[Learn more →](SourceGenerators/FakeTimeProviderExtensionGenerator.md) + ## 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: @@ -170,6 +192,7 @@ Each source generator can be individually disabled using MSBuild properties in y | Fake Logging Extension | `EnableMoqAutoMockerFakeLoggingGenerator` | | Application Insights Extension | `EnableMoqAutoMockerApplicationInsightsGenerator` | | Meter Factory Extension | `EnableMoqAutoMockerMeterFactoryGenerator` | +| Fake Time Provider Extension | `EnableMoqAutoMockerFakeTimeProviderGenerator` | **Example: Disabling a generator** ```xml diff --git a/docs/SourceGenerators/FakeTimeProviderExtensionGenerator.md b/docs/SourceGenerators/FakeTimeProviderExtensionGenerator.md new file mode 100644 index 0000000..cb57a5d --- /dev/null +++ b/docs/SourceGenerators/FakeTimeProviderExtensionGenerator.md @@ -0,0 +1,229 @@ +# Fake Time Provider Extension Generator + +When your test project references `Microsoft.Extensions.TimeProvider.Testing`, this generator creates a `WithFakeTimeProvider()` extension method for `AutoMocker` that sets up a `FakeTimeProvider` for deterministic time-based testing. + +## Features + +- Automatically generates when `Microsoft.Extensions.TimeProvider.Testing` is referenced +- Registers `FakeTimeProvider` as both `TimeProvider` (for constructor injection) and `FakeTimeProvider` (for test control) +- Enables deterministic time manipulation with `Advance()`, `SetUtcNow()`, and `AutoAdvanceAmount` +- Uses Microsoft's official `FakeTimeProvider` from `Microsoft.Extensions.Time.Testing` + +## Installation + +Install the `Microsoft.Extensions.TimeProvider.Testing` package: + +```bash +dotnet add package Microsoft.Extensions.TimeProvider.Testing +``` + +## Usage + +```csharp +using Microsoft.Extensions.Time.Testing; + +public class SchedulerService +{ + private readonly TimeProvider _timeProvider; + + public SchedulerService(TimeProvider timeProvider) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + } + + public bool IsBusinessHours() + { + var now = _timeProvider.GetLocalNow(); + return now.Hour >= 9 && now.Hour < 17; + } +} + +[TestClass] +public class SchedulerServiceTests +{ + [TestMethod] + public void IsBusinessHours_DuringBusinessDay_ReturnsTrue() + { + // Arrange + AutoMocker mocker = new(); + mocker.WithFakeTimeProvider(); + + var fakeTime = mocker.Get(); + fakeTime.SetUtcNow(new DateTimeOffset(2025, 1, 13, 14, 0, 0, TimeSpan.Zero)); // 2 PM UTC + + // Act + SchedulerService service = mocker.CreateInstance(); + + // Assert + Assert.IsTrue(service.IsBusinessHours()); + } + + [TestMethod] + public void IsBusinessHours_AfterHours_ReturnsFalse() + { + // Arrange + AutoMocker mocker = new(); + mocker.WithFakeTimeProvider(); + + var fakeTime = mocker.Get(); + fakeTime.SetUtcNow(new DateTimeOffset(2025, 1, 13, 20, 0, 0, TimeSpan.Zero)); // 8 PM UTC + + // Act + SchedulerService service = mocker.CreateInstance(); + + // Assert + Assert.IsFalse(service.IsBusinessHours()); + } +} +``` + +## Generated Extension Method + +The generator creates: + +```csharp +public static AutoMocker WithFakeTimeProvider(this AutoMocker mocker) +{ + var timeProvider = new FakeTimeProvider(); + mocker.Use(timeProvider); + mocker.Use(timeProvider); + return mocker; +} +``` + +## How It Works + +The `WithFakeTimeProvider()` method: + +1. Creates a `FakeTimeProvider` instance +2. Registers it as `TimeProvider` so classes with `TimeProvider` constructor parameters receive it automatically +3. Registers it as `FakeTimeProvider` so tests can retrieve and control it via `mocker.Get()` + +Both registrations point to the same instance, ensuring that the time your test controls is the same time the service under test observes. + +## Advanced Usage + +### Advancing Time + +```csharp +[TestMethod] +public void Cache_AfterExpiry_RemovesEntry() +{ + // Arrange + AutoMocker mocker = new(); + mocker.WithFakeTimeProvider(); + + var fakeTime = mocker.Get(); + fakeTime.SetUtcNow(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + ExpiryCache cache = mocker.CreateInstance(); + cache.Add("key", "value"); + + // Act — advance past expiry + fakeTime.Advance(TimeSpan.FromMinutes(10)); + + // Assert + Assert.IsFalse(cache.TryGetValue("key", out _)); +} +``` + +### Auto-Advancing Time + +`FakeTimeProvider` supports automatically advancing time on each `GetUtcNow()` call: + +```csharp +[TestMethod] +public void Service_WithAutoAdvance_ObservesPassingTime() +{ + AutoMocker mocker = new(); + mocker.WithFakeTimeProvider(); + + var fakeTime = mocker.Get(); + fakeTime.AutoAdvanceAmount = TimeSpan.FromSeconds(1); + + TimestampService service = mocker.CreateInstance(); + + var first = service.GetTimestamp(); + var second = service.GetTimestamp(); + + Assert.IsTrue(second > first); +} +``` + +### Chaining with Other Extensions + +`WithFakeTimeProvider()` returns the `AutoMocker` instance for fluent chaining: + +```csharp +AutoMocker mocker = new AutoMocker() + .WithFakeTimeProvider() + .WithFakeLogging(); +``` + +## Disabling the Generator + +You can disable this generator using an MSBuild property in your test project's `.csproj` file: + +```xml + + false + +``` + +### Example: Disabling in Project File + +```xml + + + net8.0 + + + false + + + + + + + +``` + +## Troubleshooting + +### Extension Method Not Available + +1. Verify `Microsoft.Extensions.TimeProvider.Testing` is referenced in your test project +2. Check that the generator is not disabled in your `.csproj` +3. Rebuild the project to trigger generator execution +4. Ensure you are using the `Moq.AutoMock` namespace + +### `TimeProvider` Not Resolved + +The `WithFakeTimeProvider()` call must happen before `CreateInstance()`: + +```csharp +// ✅ Correct — configure before creating the instance +mocker.WithFakeTimeProvider(); +var service = mocker.CreateInstance(); + +// ❌ Incorrect — too late, the instance already has a mocked TimeProvider +var service = mocker.CreateInstance(); +mocker.WithFakeTimeProvider(); +``` + +### Accessing `FakeTimeProvider` for Time Control + +Use `mocker.Get()` to retrieve the instance registered by `WithFakeTimeProvider()`: + +```csharp +mocker.WithFakeTimeProvider(); +var fakeTime = mocker.Get(); +fakeTime.Advance(TimeSpan.FromDays(1)); +``` + +## See Also + +- [Source Generators Overview](../SourceGenerators.md) +- [Fake Logging Extension Generator](FakeLoggingExtensionGenerator.md) +- [Microsoft.Extensions.TimeProvider.Testing on NuGet](https://www.nuget.org/packages/Microsoft.Extensions.TimeProvider.Testing) +- [FakeTimeProvider API documentation](https://learn.microsoft.com/dotnet/api/microsoft.extensions.time.testing.faketimeprovider)