使用 Roslyn 重构 TestCategory 属性
Refactoring TestCategory properties using Roslyn
最近我不得不重构大量测试,以便根据报告的结果将它们全部标记为特定的 TestCategory 属性 .如果测试在报告中列出,则应使用 "good category" 标记,否则应在 "bad category" 中。这些类别将用于过滤哪些测试 运行 作为我们门控构建的一部分。
以下是一些如何完成的示例。
流程的第一步是加载解决方案
var wkps = MSBuildWorkspace.Create();
var sln = wkps.OpenSolutionAsync(slnPath).Result;
现在我们有了解决方案引用,我们可以遍历每个项目并获取其语法树。然后我们可以在每个 SyntaxTree 上调用 GetRoot 并将其转换为 CompilationUnitSyntax。从这一点开始,我们搜索所有符合我们作为方法的标准的 DecsendantNodes,并定义了 TestMethod 属性。
这是整体的样子
foreach (var proj in sln.Projects)
{
var comp = proj.GetCompilationAsync().Result;
foreach (var method in root.DescendantNodes().OfType<MethodDeclarationSyntax>().Where(m => HasAttribute(m, TEST_METHOD)))
{
//do something with this test method
}
}
上面有一个名为 HasAttribute 的辅助方法,它只查找方法中名称为 "TestMethod" 的任何属性。这是它的样子
bool HasAttribute(MethodDeclarationSyntax method, string attributeName)
{
return method.AttributeLists
.Any(al => al.Attributes
.Any(a => a.Name is IdentifierNameSyntax && (((IdentifierNameSyntax)a.Name).Identifier.Text == attributeName)));
}
现在我们已经有了一种循环访问所有 TestMethod 方法的方法,我们需要将 TestCategory 属性分配给它们。这是上面循环的 "do something" 部分。
这里的过程有两个步骤。首先是编辑我们的 SyntaxTree,以便我们添加 and/or 删除我们想要的类别。其次是将该 SyntaxTree 写回源文件。
我们需要做的第一件事是根据我们的输入列表检查方法的名称。假设我们有一个方法名称字典,它可能看起来像这样
var methodName = method.Identifier.ValueText;
var testIsOnList = testDictionary.ContainsKey(methodFullName);
但是,此测试假设在整个解决方案中,测试名称是全局唯一的。不幸的是,在我的情况下,这不是真的。为了解决这个问题,我们将输入列表设为 "Fully Qualified Test Name",因为它会出现在 MSTest 测试运行程序中。这将是:
- 命名空间
- class 层级
- 方法名称
例如。 My.Long.NameSpace.ParentClass.ChildClass.Method
这是一个小帮助程序方法,它将根据 MethodDeclarationSyntax
创建 FQTN
string BuildFullTestName(MethodDeclarationSyntax method)
{
StringBuilder sb = new StringBuilder();
sb.Append(method.Identifier.ValueText);
SyntaxNode node = method;
while(node.Parent is ClassDeclarationSyntax)
{
node = node.Parent;
sb.Insert(0, ".");
sb.Insert(0, ((ClassDeclarationSyntax)node).Identifier.ValueText);
}
if(node.Parent is NamespaceDeclarationSyntax)
{
node = node.Parent;
sb.Insert(0, ".");
sb.Insert(0, ((NamespaceDeclarationSyntax)node).Name.ToString());
}
else
{
throw new Exception("method \{method.Identifier.ValueText} has wierd parents.");
}
return sb.ToString();
}
所以我们已经完成了比较,我们想用我们的好或坏 TestCategory 属性 来标记测试。这是另一个辅助方法,它将接受 MethodDeclarationSyntax、属性 名称(在我们的例子中是 TestCateogry)和 属性 的参数(在我们的例子中是类别的名称)。它将 return 包含我们更改的新 MethodDeclarationSyntax。
MethodDeclarationSyntax AddMethodProperty(MethodDeclarationSyntax method, string propertyName, string argumentName)
{
return method.AddAttributeLists(
SyntaxFactory.AttributeList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Attribute(
SyntaxFactory.IdentifierName(propertyName),
SyntaxFactory.AttributeArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Token(
default(SyntaxTriviaList),
SyntaxKind.StringLiteralToken,
argumentName,
argumentName,
default(SyntaxTriviaList))
))))))));
}
因为所有 SyntaxNodes 都是不可变的,我们不能就地更新方法。因此,现在我们已经有了新的 MethodDeclarationSyntax,我们需要创建一个新的 SyntaxTree,在其中我们用新方法替换了旧方法。
SyntaxTree newTree = SyntaxFactory.SyntaxTree(
Formatter.Format(syntaxRoot.ReplaceNode(method, newMethod), wkps))
.WithFilePath(method.SyntaxTree.FilePath);
注意:需要 .WithFilePath 以便新的 SyntaxTree 保留其映射到哪个源文件的上下文。
现在我们可以将新的 SyntaxTree 写入磁盘。标准的东西在这里。
using (StreamWriter file = File.CreateText(method.SyntaxTree.FilePath))
{
file.Write(newTree.ToString());
file.Flush();
}
在遍历您的方法时要记住一个主要问题。每次创建新的 SyntaxTree 时,其根 CompilationUnitSyntax 必须传递给循环的未来迭代。此外,我们上面对 syntaxRoot.ReplaceNode 的调用只有在被替换的方法实际上来自该 SyntaxTree 时才会起作用。换句话说,我们的大嵌套 foreach 的下一次迭代中的 MethodDeclarationSyntax 引用将不会在您新创建的 SyntaxTree 中找到。为了处理这个问题,我创建了另一个辅助方法,它将在给定旧语法树的新语法树中找到 MethodDeclarationSyntax。
MethodDeclarationSyntax GetMethodFromSyntaxRoot(CompilationUnitSyntax root, string nameSpaceName, string className, MethodDeclarationSyntax method)
{
var result = root.Members.OfType<NamespaceDeclarationSyntax>().Single(ns => ns.Name.ToString() == nameSpaceName)
.DescendantNodes(d => true).OfType<ClassDeclarationSyntax>().Single(c => c.Identifier.ValueText == className)
.Members.OfType<MethodDeclarationSyntax>().SingleOrDefault(m => m.Identifier.ValueText == method.Identifier.ValueText && m.ParameterList.ToString() == method.ParameterList.ToString());
}
最近我不得不重构大量测试,以便根据报告的结果将它们全部标记为特定的 TestCategory 属性 .如果测试在报告中列出,则应使用 "good category" 标记,否则应在 "bad category" 中。这些类别将用于过滤哪些测试 运行 作为我们门控构建的一部分。
以下是一些如何完成的示例。
流程的第一步是加载解决方案
var wkps = MSBuildWorkspace.Create();
var sln = wkps.OpenSolutionAsync(slnPath).Result;
现在我们有了解决方案引用,我们可以遍历每个项目并获取其语法树。然后我们可以在每个 SyntaxTree 上调用 GetRoot 并将其转换为 CompilationUnitSyntax。从这一点开始,我们搜索所有符合我们作为方法的标准的 DecsendantNodes,并定义了 TestMethod 属性。
这是整体的样子
foreach (var proj in sln.Projects)
{
var comp = proj.GetCompilationAsync().Result;
foreach (var method in root.DescendantNodes().OfType<MethodDeclarationSyntax>().Where(m => HasAttribute(m, TEST_METHOD)))
{
//do something with this test method
}
}
上面有一个名为 HasAttribute 的辅助方法,它只查找方法中名称为 "TestMethod" 的任何属性。这是它的样子
bool HasAttribute(MethodDeclarationSyntax method, string attributeName)
{
return method.AttributeLists
.Any(al => al.Attributes
.Any(a => a.Name is IdentifierNameSyntax && (((IdentifierNameSyntax)a.Name).Identifier.Text == attributeName)));
}
现在我们已经有了一种循环访问所有 TestMethod 方法的方法,我们需要将 TestCategory 属性分配给它们。这是上面循环的 "do something" 部分。
这里的过程有两个步骤。首先是编辑我们的 SyntaxTree,以便我们添加 and/or 删除我们想要的类别。其次是将该 SyntaxTree 写回源文件。
我们需要做的第一件事是根据我们的输入列表检查方法的名称。假设我们有一个方法名称字典,它可能看起来像这样
var methodName = method.Identifier.ValueText;
var testIsOnList = testDictionary.ContainsKey(methodFullName);
但是,此测试假设在整个解决方案中,测试名称是全局唯一的。不幸的是,在我的情况下,这不是真的。为了解决这个问题,我们将输入列表设为 "Fully Qualified Test Name",因为它会出现在 MSTest 测试运行程序中。这将是:
- 命名空间
- class 层级
- 方法名称
- class 层级
例如。 My.Long.NameSpace.ParentClass.ChildClass.Method
这是一个小帮助程序方法,它将根据 MethodDeclarationSyntax
创建 FQTNstring BuildFullTestName(MethodDeclarationSyntax method)
{
StringBuilder sb = new StringBuilder();
sb.Append(method.Identifier.ValueText);
SyntaxNode node = method;
while(node.Parent is ClassDeclarationSyntax)
{
node = node.Parent;
sb.Insert(0, ".");
sb.Insert(0, ((ClassDeclarationSyntax)node).Identifier.ValueText);
}
if(node.Parent is NamespaceDeclarationSyntax)
{
node = node.Parent;
sb.Insert(0, ".");
sb.Insert(0, ((NamespaceDeclarationSyntax)node).Name.ToString());
}
else
{
throw new Exception("method \{method.Identifier.ValueText} has wierd parents.");
}
return sb.ToString();
}
所以我们已经完成了比较,我们想用我们的好或坏 TestCategory 属性 来标记测试。这是另一个辅助方法,它将接受 MethodDeclarationSyntax、属性 名称(在我们的例子中是 TestCateogry)和 属性 的参数(在我们的例子中是类别的名称)。它将 return 包含我们更改的新 MethodDeclarationSyntax。
MethodDeclarationSyntax AddMethodProperty(MethodDeclarationSyntax method, string propertyName, string argumentName)
{
return method.AddAttributeLists(
SyntaxFactory.AttributeList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Attribute(
SyntaxFactory.IdentifierName(propertyName),
SyntaxFactory.AttributeArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Token(
default(SyntaxTriviaList),
SyntaxKind.StringLiteralToken,
argumentName,
argumentName,
default(SyntaxTriviaList))
))))))));
}
因为所有 SyntaxNodes 都是不可变的,我们不能就地更新方法。因此,现在我们已经有了新的 MethodDeclarationSyntax,我们需要创建一个新的 SyntaxTree,在其中我们用新方法替换了旧方法。
SyntaxTree newTree = SyntaxFactory.SyntaxTree(
Formatter.Format(syntaxRoot.ReplaceNode(method, newMethod), wkps))
.WithFilePath(method.SyntaxTree.FilePath);
注意:需要 .WithFilePath 以便新的 SyntaxTree 保留其映射到哪个源文件的上下文。
现在我们可以将新的 SyntaxTree 写入磁盘。标准的东西在这里。
using (StreamWriter file = File.CreateText(method.SyntaxTree.FilePath))
{
file.Write(newTree.ToString());
file.Flush();
}
在遍历您的方法时要记住一个主要问题。每次创建新的 SyntaxTree 时,其根 CompilationUnitSyntax 必须传递给循环的未来迭代。此外,我们上面对 syntaxRoot.ReplaceNode 的调用只有在被替换的方法实际上来自该 SyntaxTree 时才会起作用。换句话说,我们的大嵌套 foreach 的下一次迭代中的 MethodDeclarationSyntax 引用将不会在您新创建的 SyntaxTree 中找到。为了处理这个问题,我创建了另一个辅助方法,它将在给定旧语法树的新语法树中找到 MethodDeclarationSyntax。
MethodDeclarationSyntax GetMethodFromSyntaxRoot(CompilationUnitSyntax root, string nameSpaceName, string className, MethodDeclarationSyntax method)
{
var result = root.Members.OfType<NamespaceDeclarationSyntax>().Single(ns => ns.Name.ToString() == nameSpaceName)
.DescendantNodes(d => true).OfType<ClassDeclarationSyntax>().Single(c => c.Identifier.ValueText == className)
.Members.OfType<MethodDeclarationSyntax>().SingleOrDefault(m => m.Identifier.ValueText == method.Identifier.ValueText && m.ParameterList.ToString() == method.ParameterList.ToString());
}