How can I unit test my own custom analyzers and Code Fix providers?
I'm sitting in front of my computer with my hands on the keyboard but I don't know what to type.
A typical unit test contains 3 phases: First, it initializes a small piece of an application it wants to test (also known as the system under test, or SUT), then it applies some stimulus to the system under test (usually by calling a method on it), and finally, it observes the resulting behavior.
A good place to start is by creating a new solution using the "Diagnostics and Code Fix" template. This will create a unit test project that comes with a few classes which allow you to very easily test your diagnostics.
However this also shows its weakness: the classes are hardcoded in your codebase and are not a dependency which you can easily update when needed. In a still constantly changing codebase like Roslyn, this means you will fall behind quickly: the test classes are aimed at Beta-1 while Roslyn is already at RC2 at the time of writing.
There are two solutions I propose:
Read through the rest of this post where I give a broad layout of what is being done in those classes and what their key aspects are. Afterwards you can create your own implementation according to your needs.
Remove all those classes and instead use the RoslynTester NuGet package which I created based on these helpers. This will allow you to immediately get started with the RC2 version of Roslyn and keep it more easily updated. For more information, take a look at my blog or the Github page.
The idea behind the helpers is simple: given one or more strings that represent class files and one or more objects that represent expected diagnostic results, create an in-memory project with the given classes and execute the analyzers.
In the case of a CodeFix provider, you can also specify how the code should look after it's transformed.
This is an example test that shows a warning when you have an asynchronous method whose name doesn't end with "Async" and provides a CodeFix to change the name.
[TestMethod]
public void AsyncMethodWithoutAsyncSuffixAnalyzer_WithAsyncKeywordAndNoSuffix_InvokesWarning()
{
var original = @"
using System;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
class MyClass
{
async Task Method()
{
}
}
}";
var result = @"
using System;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
class MyClass
{
async Task MethodAsync()
{
}
}
}";
var expectedDiagnostic = new DiagnosticResult
{
Id = AsyncMethodWithoutAsyncSuffixAnalyzer.DiagnosticId,
Message = string.Format(AsyncMethodWithoutAsyncSuffixAnalyzer.Message, "Method"),
Severity = EmptyArgumentExceptionAnalyzer.Severity,
Locations =
new[]
{
new DiagnosticResultLocation("Test0.cs", 10, 13)
}
};
VerifyCSharpDiagnostic(original, expectedDiagnostic);
VerifyCSharpFix(original, result);
}
As you can see the setup is very straightforward: you determine how the faulty code looks, you specify how it should look and you indicate the properties of the warning that should be displayed.
The first step is to create the in-memory project. This consists of a few steps:
new AdhocWorkspace()
).CurrentSolution.AddProject()
).AddMetadataReferences()
)solution.AddDocument()
)Here we will use the documents we just created. These two lines are most important:
var compilation = project.GetCompilationAsync().Result;
var diagnostics = compilation.WithAnalyzers(ImmutableArray.Create(analyzer))
.GetAnalyzerDiagnosticsAsync()
.Result;
At this point you have everything you need: you have the actual results and you have the expected results. All that is left is verifying that the two collections match.
This roughly follows the same pattern as the diagnostics but adds a little bit to it.
CodeFixContext
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostics[0],
(a, d) => actions.Add(a), CancellationToken.None);
codeFixProvider.RegisterCodeFixesAsync(context).Wait();
var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
If everything is still a little blurry, definitely take a look at the exact source code. If you want clearer examples of how to create your own diagnostics, take a look at my VSDiagnostics repository or my blogpost on writing your own.
There are the Microsoft.CodeAnalysis.Testing (GitHub) packages from Microsoft, where currently pre-release versions are available on MyGet. These are also included in the Visual Studio 16.6 template Analyzer with Code Fix (.NET Standard).
In order to consume these packages, you have to add their package source to the NuGet Config file:
Go to /configuration/packageSources
and add:
<add key="roslyn-analyzers" value="https://dotnet.myget.org/F/roslyn-analyzers/api/v3/index.json" />
If you have no NuGet Config file yet, you can create a new one with dotnet new nugetconfig
.
When adding/updating these packages with a NuGet Package Manager, you may have to include prereleases.
These packages support testing DiagnosticAnalyzer, CodeFixProvider, and CodeRefactoringProvider,
written in either C# or Visual Basic,
via MSTest V2, NUnit, or xUnit.net,
by adding the particular PackageReference
:
Microsoft.CodeAnalysis.[CSharp|VisualBasic].[Analyzer|CodeFix|CodeRefactoring].Testing.[MSTest|NUnit|XUnit]
Testing CompletionProvider is currently not supported.
Warning: the Visual Studio 16.6 template contains errors and will not compile. Refer to the documentation for the correct namespaces and method names.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With