diff --git a/FastMoq-Release.sln b/FastMoq-Release.sln
index 40fec51d..448ea37c 100644
--- a/FastMoq-Release.sln
+++ b/FastMoq-Release.sln
@@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq", "FastMoq\FastMoq.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Abstractions", "FastMoq.Abstractions\FastMoq.Abstractions.csproj", "{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Generators", "FastMoq.Generators\FastMoq.Generators.csproj", "{3A865DC1-4C90-42F7-B0E9-25A31D7C432E}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Generators", "FastMoq.Generators\FastMoq.Generators.csproj", "{5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastMoq.Azure", "FastMoq.Azure\FastMoq.Azure.csproj", "{CADD0874-D3E6-40B3-A8D5-EB04047597EC}"
EndProject
@@ -39,10 +39,10 @@ Global
{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}.Debug|Any CPU.Build.0 = Release|Any CPU
{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}.Release|Any CPU.ActiveCfg = Release|Any CPU
{970828D1-0EF2-4D0D-BF1B-BB85DEB38514}.Release|Any CPU.Build.0 = Release|Any CPU
- {3A865DC1-4C90-42F7-B0E9-25A31D7C432E}.Debug|Any CPU.ActiveCfg = Release|Any CPU
- {3A865DC1-4C90-42F7-B0E9-25A31D7C432E}.Debug|Any CPU.Build.0 = Release|Any CPU
- {3A865DC1-4C90-42F7-B0E9-25A31D7C432E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3A865DC1-4C90-42F7-B0E9-25A31D7C432E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Debug|Any CPU.Build.0 = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5FE82C4B-17E0-4C41-9A79-4F13FCB6D2D1}.Release|Any CPU.Build.0 = Release|Any CPU
{CADD0874-D3E6-40B3-A8D5-EB04047597EC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{CADD0874-D3E6-40B3-A8D5-EB04047597EC}.Debug|Any CPU.Build.0 = Release|Any CPU
{CADD0874-D3E6-40B3-A8D5-EB04047597EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs b/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs
index 0cfe7488..260a483d 100644
--- a/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs
+++ b/FastMoq.Abstractions/Generators/FastMoqGeneratedTestTargetAttribute.cs
@@ -14,10 +14,20 @@ public sealed class FastMoqGeneratedTestTargetAttribute : Attribute
/// Initializes a new instance of the class.
///
/// The component under test that the generated harness path should target.
- /// An optional explicit constructor signature to use for the generated harness bootstrap.
- public FastMoqGeneratedTestTargetAttribute(Type componentType, params Type[] constructorParameterTypes)
+ public FastMoqGeneratedTestTargetAttribute(Type componentType)
{
ComponentType = componentType ?? throw new ArgumentNullException(nameof(componentType));
+ ConstructorParameterTypes = Array.Empty();
+ }
+
+ ///
+ /// Initializes a new instance of the class with an explicit constructor signature.
+ ///
+ /// The component under test that the generated harness path should target.
+ /// The explicit constructor signature to use for the generated harness bootstrap. Pass an explicit empty array to target the parameterless constructor.
+ public FastMoqGeneratedTestTargetAttribute(Type componentType, params Type[] constructorParameterTypes)
+ : this(componentType)
+ {
ConstructorParameterTypes = constructorParameterTypes ?? throw new ArgumentNullException(nameof(constructorParameterTypes));
}
diff --git a/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs b/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs
index 60c41b83..7e5b49f1 100644
--- a/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs
+++ b/FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs
@@ -24,6 +24,11 @@ internal static class AnalyzerTestHelpers
"FastMoq.Tests.Web",
};
+ private static bool IsXunitAssemblyName(string assemblyName)
+ {
+ return assemblyName.StartsWith("xunit", StringComparison.OrdinalIgnoreCase);
+ }
+
public static async Task> GetDiagnosticsAsync(string source, params DiagnosticAnalyzer[] analyzers)
{
return await GetDiagnosticsAsync(source, includeAzureFunctionsHelpers: false, includeMoqProviderPackage: true, includeNSubstituteProviderPackage: true, includeWebHelpers: true, analyzers).ConfigureAwait(false);
@@ -148,7 +153,8 @@ public static Document CreateDocumentForTest(
bool includeWebHelpers = true,
bool includeDatabaseHelpers = false,
bool includeAzureHelpers = false,
- bool includeAggregatePackage = false)
+ bool includeAggregatePackage = false,
+ bool includeXunit = true)
{
return CreateDocument(
source,
@@ -158,7 +164,8 @@ public static Document CreateDocumentForTest(
includeWebHelpers,
includeDatabaseHelpers,
includeAzureHelpers,
- includeAggregatePackage);
+ includeAggregatePackage,
+ includeXunit);
}
private static Document CreateDocument(
@@ -169,7 +176,8 @@ private static Document CreateDocument(
bool includeWebHelpers = true,
bool includeDatabaseHelpers = false,
bool includeAzureHelpers = false,
- bool includeAggregatePackage = false)
+ bool includeAggregatePackage = false,
+ bool includeXunit = true)
{
var project = CreateProject(
[("Test.cs", source)],
@@ -179,7 +187,8 @@ private static Document CreateDocument(
includeWebHelpers,
includeDatabaseHelpers,
includeAzureHelpers,
- includeAggregatePackage);
+ includeAggregatePackage,
+ includeXunit);
return project.Documents.Single();
}
@@ -191,7 +200,8 @@ private static Project CreateProject(
bool includeWebHelpers = true,
bool includeDatabaseHelpers = false,
bool includeAzureHelpers = false,
- bool includeAggregatePackage = false)
+ bool includeAggregatePackage = false,
+ bool includeXunit = true)
{
var workspace = new AdhocWorkspace();
var projectId = ProjectId.CreateNewId();
@@ -208,7 +218,8 @@ private static Project CreateProject(
includeWebHelpers,
includeDatabaseHelpers,
includeAzureHelpers,
- includeAggregatePackage))
+ includeAggregatePackage,
+ includeXunit))
{
solution = solution.AddMetadataReference(projectId, metadataReference);
}
@@ -229,7 +240,8 @@ private static IEnumerable GetMetadataReferences(
bool includeWebHelpers,
bool includeDatabaseHelpers,
bool includeAzureHelpers,
- bool includeAggregatePackage)
+ bool includeAggregatePackage,
+ bool includeXunit)
{
if (includeAggregatePackage)
{
@@ -250,6 +262,11 @@ private static IEnumerable GetMetadataReferences(
continue;
}
+ if (!includeXunit && IsXunitAssemblyName(assemblyName))
+ {
+ continue;
+ }
+
if (!includeAggregatePackage &&
string.Equals(assemblyName, "FastMoq", StringComparison.OrdinalIgnoreCase))
{
@@ -335,6 +352,10 @@ private static IEnumerable GetMetadataReferences(
references.Add(typeof(Moq.Mock).Assembly.Location);
references.Add(typeof(NSubstitute.Substitute).Assembly.Location);
+ if (includeXunit)
+ {
+ references.Add(typeof(global::Xunit.FactAttribute).Assembly.Location);
+ }
references.Add(typeof(Microsoft.Extensions.Logging.ILogger).Assembly.Location);
references.Add(typeof(Microsoft.AspNetCore.Http.DefaultHttpContext).Assembly.Location);
references.Add(typeof(Microsoft.AspNetCore.Mvc.ControllerContext).Assembly.Location);
diff --git a/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs b/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs
index 48c21065..e7f08b26 100644
--- a/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs
+++ b/FastMoq.Analyzers.Tests/GeneratedHarnessSourceGeneratorTests.cs
@@ -2,6 +2,7 @@
using FastMoq.Generators;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
@@ -144,7 +145,50 @@ public partial class OrderSubmitterTests : MockerTestBase
var generatedSource = result.GeneratedSources.Should().ContainSingle().Subject.SourceText.ToString();
generatedSource.Should().Contain("typeof(global::Demo.Tests.IOrderGateway)");
- generatedSource.Should().NotContain("new global::System.Type[]\r\n {\r\n };");
+ generatedSource.Should().NotContain("new global::System.Type?[]\r\n {\r\n };");
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldUseExplicitParameterlessConstructor_ForMultiConstructorTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+using System;
+
+namespace Demo.Tests;
+
+public interface IOrderGateway { }
+
+public sealed class OrderSubmitter
+{
+ public OrderSubmitter()
+ {
+ ConstructorKind = ""parameterless"";
+ }
+
+ public OrderSubmitter(IOrderGateway gateway)
+ {
+ ConstructorKind = ""dependency"";
+ }
+
+ public string ConstructorKind { get; }
+}
+
+[FastMoqGeneratedTestTarget(typeof(OrderSubmitter), new global::System.Type[] { })]
+public partial class OrderSubmitterTests : MockerTestBase
+{
+ public string DescribeConstructorKind() => Component.ConstructorKind;
+
+ public Type?[]? DescribeConstructorTypes() => ComponentConstructorParameterTypes;
+}
+";
+
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(source);
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.OrderSubmitterTests");
+
+ Invoke(generatedHarness, "DescribeConstructorKind").Should().Be("parameterless");
+ Invoke(generatedHarness, "DescribeConstructorTypes").Should().BeEmpty();
}
[Fact]
@@ -156,23 +200,692 @@ public async Task GeneratedHarnessSourceGenerator_ShouldNotEmit_WhenMultiplePubl
namespace Demo.Tests;
-public interface IOrderGateway { }
-public interface IAuditWriter { }
-
-public sealed class OrderSubmitter
+public interface IOrderGateway { }
+public interface IAuditWriter { }
+
+public sealed class OrderSubmitter
+{
+ public OrderSubmitter(IOrderGateway gateway)
+ {
+ }
+
+ public OrderSubmitter(IAuditWriter auditWriter)
+ {
+ }
+}
+
+[FastMoqGeneratedTestTarget(typeof(OrderSubmitter))]
+public partial class OrderSubmitterTests : MockerTestBase
+{
+}
+";
+
+ var result = await RunGeneratorAsync(source);
+
+ result.DriverDiagnostics.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.OutputCompilation.GetDiagnostics().Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Should()
+ .BeEmpty();
+ result.GeneratedSources.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldWorkInRepresentativeCompiledConsumerScenario()
+ {
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(RepresentativeConsumingScenarioSource);
+
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.GeneratedConstructorHarness");
+
+ Invoke(generatedHarness, "DescribeConstructorKind").Should().Be("selected");
+ Invoke(generatedHarness, "DescribeConstructorTypes")
+ .Should()
+ .Equal([typeof(System.IO.Abstractions.IFileSystem), typeof(string)]);
+
+ var generatedPlan = Invoke(generatedHarness, "DescribeComponentConstruction");
+ generatedPlan.Parameters.Should().HaveCount(2);
+ generatedPlan.Parameters[0].Name.Should().Be("fileSystem");
+ generatedPlan.Parameters[0].ParameterType.Should().Be(typeof(System.IO.Abstractions.IFileSystem));
+ generatedPlan.Parameters[1].Name.Should().Be("value");
+ generatedPlan.Parameters[1].ParameterType.Should().Be(typeof(string));
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldMatchManualRuntimeHarness_ForRepresentativeConsumerScenario()
+ {
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(RepresentativeConsumingScenarioSource);
+
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.GeneratedConstructorHarness");
+ var manualHarness = CreateInstance(loadedAssembly, "Demo.Tests.ManualConstructorHarness");
+
+ var generatedPlan = Invoke(generatedHarness, "DescribeComponentConstruction");
+ var manualPlan = Invoke(manualHarness, "DescribeComponentConstruction");
+
+ Invoke(generatedHarness, "DescribeConstructorKind").Should().Be(Invoke(manualHarness, "DescribeConstructorKind"));
+ Invoke(generatedHarness, "DescribeValueWasNull").Should().Be(Invoke(manualHarness, "DescribeValueWasNull"));
+ Invoke(generatedHarness, "DescribeConstructorTypes").Should().Equal(Invoke(manualHarness, "DescribeConstructorTypes"));
+
+ generatedPlan.RequestedType.Should().Be(manualPlan.RequestedType);
+ generatedPlan.ResolvedType.Should().Be(manualPlan.ResolvedType);
+ generatedPlan.UsedNonPublicConstructor.Should().Be(manualPlan.UsedNonPublicConstructor);
+ generatedPlan.UsedPreferredConstructorAttribute.Should().Be(manualPlan.UsedPreferredConstructorAttribute);
+ generatedPlan.UsedAmbiguityFallback.Should().Be(manualPlan.UsedAmbiguityFallback);
+ generatedPlan.Parameters.Select(static parameter => parameter.Name)
+ .Should()
+ .Equal(manualPlan.Parameters.Select(static parameter => parameter.Name));
+ generatedPlan.Parameters.Select(static parameter => parameter.ParameterType)
+ .Should()
+ .Equal(manualPlan.Parameters.Select(static parameter => parameter.ParameterType));
+ generatedPlan.Parameters.Select(static parameter => parameter.Source)
+ .Should()
+ .Equal(manualPlan.Parameters.Select(static parameter => parameter.Source));
+
+ var metadataType = generatedHarness.GetType().GetNestedType("FastMoqGeneratedHarnessMetadata", BindingFlags.NonPublic);
+ metadataType.Should().NotBeNull();
+ var dependencyNames = (string[]) metadataType!.GetProperty("DependencyNames", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!;
+ var dependencyTypes = (Type[]) metadataType.GetProperty("DependencyTypes", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!;
+
+ dependencyNames.Should().Equal(generatedPlan.Parameters.Select(static parameter => parameter.Name));
+ dependencyTypes.Should().Equal(generatedPlan.Parameters.Select(static parameter => parameter.ParameterType));
+ }
+
+ [Fact]
+ public async Task GeneratedHarnessSourceGenerator_ShouldEmitExecutableScenarioScaffold_ForGeneratedHarnessTarget()
+ {
+ const string source = @"
+using FastMoq;
+using FastMoq.Generators;
+
+namespace Demo.Tests;
+
+public sealed class ScenarioCounter
+{
+ public int Count { get; private set; }
+
+ public bool WasVerified { get; private set; }
+
+ public void Increment()
+ {
+ Count++;
+ }
+
+ public void MarkVerified()
+ {
+ WasVerified = true;
+ }
+}
+
+[FastMoqGeneratedTestTarget(typeof(ScenarioCounter))]
+public partial class GeneratedScenarioHarness : MockerTestBase
+{
+ public int DescribeCount() => Component.Count;
+
+ public bool DescribeWasVerified() => Component.WasVerified;
+
+ partial void ActGeneratedScenario(ScenarioBuilder scenario)
+ {
+ scenario.When(component => component.Increment());
+ }
+
+ partial void AssertGeneratedScenario(ScenarioBuilder scenario)
+ {
+ scenario.Then(component =>
+ {
+ if (component.Count != 1)
+ {
+ throw new global::System.InvalidOperationException(""Expected exactly one increment."");
+ }
+ });
+ }
+
+ partial void VerifyGeneratedScenario(ScenarioBuilder scenario)
+ {
+ scenario.Then(component => component.MarkVerified());
+ }
+}
+";
+
+ var loadedAssembly = await LoadGeneratedAssemblyAsync(source);
+ var generatedHarness = CreateInstance(loadedAssembly, "Demo.Tests.GeneratedScenarioHarness");
+
+ Invoke