public class TestFixtureGenerator
const string ObjectSuffix = "Test";
public static NamespaceDeclarationSyntax ServiceTestFixture(ClassDeclarationSyntax model, string ns, SemanticModel semanticModel) => DefaultTestFixture(model, ns, false, semanticModel);
public static NamespaceDeclarationSyntax ControllerTestFixture(ClassDeclarationSyntax model, string ns, SemanticModel semanticModel) => DefaultTestFixture(model, ns, true, semanticModel);
static NamespaceDeclarationSyntax DefaultTestFixture(ClassDeclarationSyntax model, string ns, bool ctorCtx, SemanticModel semanticModel)
var fixture = SyntaxFactory.ClassDeclaration($"{model.Identifier.Text}{ObjectSuffix}")
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
SyntaxFactory.MethodDeclaration(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), $"{model.Identifier.Text}_Construct")
SyntaxFactory.Token(SyntaxKind.PublicKeyword)
SyntaxFactory.ParseStatement("var testScope = new DefaultTestScope();"),
SyntaxFactory.ParseStatement("Assert.NotNull(testScope.InstanceUnderTest);")
var inputs = model.DescendantNodes().OfType<ConstructorDeclarationSyntax>().First().DescendantNodes().OfType<AssignmentExpressionSyntax>()
.Where(x => !(x.Left as IdentifierNameSyntax).Identifier.Text.Contains("mapper"))
.ToDictionary(x => x.Left as IdentifierNameSyntax, y => y.Right as IdentifierNameSyntax);
List<MemberDeclarationSyntax> tests = new List<MemberDeclarationSyntax>();
foreach (var method in model.DescendantNodes().OfType<MethodDeclarationSyntax>().Where(x => x.Modifiers.Any(SyntaxKind.PublicKeyword)))
var testName = $"{model.Identifier.Text}_{method.Identifier.Text}_Is_Invoked_Successfully";
var testLines = new List<string>();
testLines.Add($"var testScope = new DefaultTestScope();");
var nodes = method.DescendantNodesAndSelf().OfType<InvocationExpressionSyntax>();
foreach(var node in nodes)
if(node.Expression is MemberAccessExpressionSyntax m && m.Expression is IdentifierNameSyntax i && inputs.Any(x => x.Key.Identifier.Text == i.Identifier.Text))
var ctorInvocation = inputs.Single(x => x.Key.Identifier.Text == i.Identifier.Text);
var argList = new List<string>();
foreach(var nodeArg in node.ArgumentList.Arguments)
var name = nodeArg.Expression as IdentifierNameSyntax;
var paramName = name?.Identifier.Text;
if (paramName == null && nodeArg.Expression is MemberAccessExpressionSyntax a)
if (a.Expression is IdentifierNameSyntax n)
paramName = $"{n.Identifier.Text}_{a.Name.Identifier.Text}";
if (a.Expression is ElementAccessExpressionSyntax ea && ea.Expression is IdentifierNameSyntax n2)
paramName = $"{n2.Identifier.Text}_{a.Name.Identifier.Text}";
var argTypeInfo = semanticModel.GetTypeInfo(name);
var argType = argTypeInfo.Type;
argTypeName = argType?.ToMinimalDisplayString(semanticModel, 0) ?? "???";
isRefType = argType?.TypeKind != TypeKind.Struct;
var argName = $"{i.Identifier.Text}_{m.Name.Identifier.Text}_{ paramName ?? ("arg" + argCount) }_{argCounter++}";
if (nodeArg.Expression is LambdaExpressionSyntax l)
var argTypeInfo = semanticModel.GetTypeInfo(nodeArg);
var argType = argTypeInfo.Type;
argTypeName = argType?.ToMinimalDisplayString(semanticModel, 0) ?? "???";
argName = $"It.IsAny<Expression<Func<{argTypeName},bool>>>()";
testLines.Add($"var {argName} = " + (isRefType ? $"Builder<{argTypeName}>.CreateNew().Build();" : $"default({argTypeName});"));
var sym = semanticModel.GetSymbolInfo(m).Symbol as IMethodSymbol;
var parms = sym.Parameters.Select(_ => $"var argName = Builder<{_.Type.ToDisplayString()}>.CreateNew().Build();");
testLines.AddRange(parms);
var returnType = semanticModel.GetTypeInfo(node);
testLines.Add($"var test = new {returnType.Type.ToDisplayString()}();");
if (node.Parent is AssignmentExpressionSyntax ae)
var returnTypeName = semanticModel.GetTypeInfo(ae.Left).Type?.ToMinimalDisplayString(semanticModel, 0);
testLines.Add($"var {i.Identifier.Text}_{m.Name.Identifier.Text}_Return_{ae.Left.ToString().Replace(".", "_")} = " + (isClass ? $"Builder<{returnTypeName}>.CreateNew().Build();" : $"default({returnTypeName});"));
returnSetup = $".Returns({i.Identifier.Text}_{m.Name.Identifier.Text}_Return_{ae.Left.ToString().Replace(".", "_")})";
var eq = node.AncestorsAndSelf().OfType<EqualsValueClauseSyntax>().FirstOrDefault();
var returnTypeName = semanticModel.GetTypeInfo(eq.Value).Type?.ToMinimalDisplayString(semanticModel, 0);
testLines.Add($"var {i.Identifier.Text}_{m.Name.Identifier.Text}_Return = " + (isClass ? $"Builder<{returnTypeName}>.CreateNew().Build();" : $"default({returnTypeName});"));
returnSetup = $".Returns({i.Identifier.Text}_{m.Name.Identifier.Text}_Return)";
testLines.Add($"testScope.{ctorInvocation.Value.Identifier.Text}.Setup(p => p.{m.Name.Identifier.Text}({string.Join(", ", argList)})){returnSetup};");
foreach (var parm in method.ParameterList.Parameters)
testLines.Add($"var {parm.Identifier.Text} = Builder<{parm.Type}>.CreateNew().Build();");
testLines.Add($"var expected = Builder<{method.ReturnType}>.CreateNew().Build();");
var args = method.ParameterList.Parameters.Select(x => x.Identifier.Text);
testLines.Add($"var actual = testScope.InstanceUnderTest.{method.Identifier.Text}({string.Join(" ,", args)});");
testLines.Add($"Assert.Equal(actual, expected);");
tests.Add(SyntaxFactory.MethodDeclaration(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), testName)
.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(
SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Fact")))))
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddBodyStatements(testLines.Select(x => SyntaxFactory.ParseStatement(x)).ToArray()));
fixture = fixture.AddMembers(tests.ToArray());
fixture = fixture.AddMembers(CreateTestScope(model, ctorCtx));
var ancestors = model.AncestorsAndSelf().ToArray();
var usings = new List<string> { "System", "Moq", "Xunit", "AutoMapper", "FizzWare.NBuilder", "System.Linq", "System.Linq.Expressions" };
usings.AddRange(ancestors.OfType<NamespaceDeclarationSyntax>().Select(x => x.Name.ToString()));
usings.AddRange(ancestors.OfType<CompilationUnitSyntax>().SelectMany(x => x.Usings.Select(_ => _.Name.ToString())));
return SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(ns))
.AddUsings(usings.Distinct().OrderBy(x => x.Length).Select(x => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(x))).ToArray())
private static ClassDeclarationSyntax CreateTestScope(ClassDeclarationSyntax model, bool controllerContext)
var ctorArgs = model.DescendantNodes().OfType<ConstructorDeclarationSyntax>().First().ParameterList;
var props = ctorArgs.Parameters.Select(x => Prop($"Mock<{x.Type}>", x.Identifier.Text)).ToArray();
var initializers = ctorArgs.Parameters.Select(x => SyntaxFactory.ParseStatement($"{x.Identifier.Text} = new Mock<{x.Type}>();")).ToList();
var instanceArgs = ctorArgs.Parameters.Select(x => $"{x.Identifier.Text}.Object").ToArray();
ControllerContext = new ControllerContext()
HttpContext = new DefaultHttpContext(),
var instanceInitialized = SyntaxFactory.ParseStatement($@"InstanceUnderTest = new {model.Identifier.Text}({string.Join(" ,", instanceArgs)})
{ (controllerContext ? ctrCtxText : "") }
initializers.Add(instanceInitialized);
var scope = SyntaxFactory.ClassDeclaration("DefaultTestScope")
.AddMembers(Prop(model.Identifier.Text, "InstanceUnderTest"))
.AddMembers(SyntaxFactory.ConstructorDeclaration("DefaultTestScope").AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddBodyStatements(initializers.ToArray()))
private static PropertyDeclarationSyntax Prop(string type, string name)
return SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(type), name)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)));
private static MethodDeclarationSyntax Method(string returnType, string name, string body)
return SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName(returnType), name)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.WithBody(SyntaxFactory.Block(SyntaxFactory.ParseStatement(body)));
private static DocumentationCommentTriviaSyntax XmlComment()
return SyntaxFactory.DocumentationCommentTrivia(
SyntaxKind.SingleLineDocumentationCommentTrivia,
SyntaxFactory.List<XmlNodeSyntax>(
SyntaxFactory.XmlTextLiteral(
SyntaxFactory.TriviaList(
SyntaxFactory.DocumentationCommentExterior("///")),
SyntaxFactory.TriviaList()))),
SyntaxFactory.XmlElement(
SyntaxFactory.XmlElementStartTag(
SyntaxFactory.Identifier("summary"))),
SyntaxFactory.XmlElementEndTag(
SyntaxFactory.Identifier("summary"))))
SyntaxFactory.SingletonList<XmlNodeSyntax>(
SyntaxFactory.XmlTextLiteral(
SyntaxFactory.TriviaList(),
SyntaxFactory.TriviaList()))))),
SyntaxFactory.XmlTextNewLine(
SyntaxFactory.TriviaList(),
SyntaxFactory.TriviaList())))}));