This library provides TDD friendly DiagnosticAnalyzer test helpers:
-
DiagnosticAnalyzerRunner
A runner for
Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer. The purpose of the runner is providing another runner instead ofMicrosoft.CodeAnalysis.Analyzer.Testing.AnalyzerVerifier.VerifyAnalyzerAsync.Because of the
AnalyzerVerifierhas several problems:- Using AnalyzerVerifier, it is hard to instantiate analyzer with custom arguments (the custom args may be needed if your analyzer is composed by several smaller analyzer-like components)
- AnalyzerVerifier may throw some exceptions because it test Diagnostics. But it should be optional because analyzer-like smaller components may not need it. If it is not optional the tests for the components become to need to wrap try-catch statements for each call of
VerifyAnalyzerAsync
-
Test Doubles for DiagnosticAnalyzer
- NullAnalyzer: it do nothing
- StubAnalyzer: it analyze codes with a
Dena.CodeAnalysis.Testing.AnalyzerActions - SpyAnalyzer: it analyze codes and do not report any Diagnostics, but instead it records all actions that registered via
Microsoft.CodeAnalysis.Dignostics.AnalysisContext
- .NET Standard 2.1 or later
var analyzer = new YourAnalyzer();
// The analyzer get intialized and get to call registered actions.
await DiagnosticAnalyzerRunner.Run(
analyzer,
@"public static class Foo
{
public static void Bar()
{
System.Console.WriteLine(""Hello, World!"");
}
}");var analyzer = new YourAnalyzer();
var diagnostics = await DiagnosticAnalyzerRunner.Run(
analyzer,
@"public static class Foo
{
public static void Bar()
{
System.Console.WriteLine(""Hello, World!"");
}
}");
Assert.AreEqual(0, diagnostics.Length);var location = diagnostic.Location;
LocationAssert.HaveTheSpan(
"/0/Test0.", // Optional. Skip path assertion if the path not specified,
new LinePosition(1, 0),
new LinePosition(8, 5),
location
);var diagnostics = await DiagnosticAnalyzerRunner.Run(
anyAnalyzer,
@"
internal static class Foo
{
internal static void Bar()
{
System.Console.WriteLine(""Hello, World!"");
}
}
ERROR");
Assert.AreEqual(0, diagnostics.Length, DiagnosticsFormatter.Format(diagnostics));
// This message is like:
//
// // /0/Test0.cs(9,1): error CS0116: A namespace cannot directly contain members such as fields or methods
// DiagnosticResult.CompilerError(""CS0116"").WithSpan(""/0/Test0.cs"", 9, 1, 9, 6),var spyAnalyzer = new SpyAnalyzer();
var diagnostics = await DiagnosticAnalyzerRunner.Run(
spyAnalyzer,
@"public static class Foo
{
public static void Bar()
{
System.Console.WriteLine(""Hello, World!"");
}
}");
Assert.IsTrue(spyAnalyzer.IsInitialized);var spyAnalyzer = new SpyAnalyzer();
var diagnostics = await DiagnosticAnalyzerRunner.Run(
spyAnalyzer,
@"public static class Foo
{
public static void Bar()
{
System.Console.WriteLine(""Hello, World!"");
}
}");
// CompilationActionHistory hold the Compilation object that given
// to the action registered by AnalysisContext.RegisterCompilationAction.
Assert.AreEqual(1, spyAnalyzer.CompilationActionHistory.Count);
// Other available histories are:
//
// - spyAnalyzer.CodeBlockActionHistory
// - spyAnalyzer.CodeBlockStartActionHistory
// - spyAnalyzer.CompilationActionHistory
// - spyAnalyzer.CompilationStartActionHistory
// - spyAnalyzer.OperationActionHistory
// - spyAnalyzer.OperationBlockActionHistory
// - spyAnalyzer.OperationBlockStartAction
// - spyAnalyzer.OperationBlockStartActionHistory
// - spyAnalyzer.SemanticModelActionHistory
// - spyAnalyzer.SymbolActionHistory
// - spyAnalyzer.SymbolStartActionHistory
// - spyAnalyzer.SyntaxNodeActionHistory
// - spyAnalyzer.SyntaxTreeActionHistoryvar stubAnalyzer = new StubAnalyzer(
new AnalyzerActions
{
CodeBlockStartAction = context => DoSomething()
}
);
await DiagnosticAnalyzerRunner.Run(
stubAnalyzer,
@"public static class Foo
{
public static void Bar()
{
System.Console.WriteLine(""Hello, World!"");
}
}");
// Other available actions are:
//
// - stubAnalyzer.CodeBlockAction
// - stubAnalyzer.CodeBlockStartAction
// - stubAnalyzer.CompilationAction
// - stubAnalyzer.CompilationStartAction
// - stubAnalyzer.OperationAction
// - stubAnalyzer.OperationBlockAction
// - stubAnalyzer.OperationBlockStartAction
// - stubAnalyzer.OperationBlockStartAction
// - stubAnalyzer.SemanticModelAction
// - stubAnalyzer.SymbolAction
// - stubAnalyzer.SymbolStartAction
// - stubAnalyzer.SyntaxNodeAction
// - stubAnalyzer.SyntaxTreeActionDiagnosticAssert.AreEqual assert that collections of Diagnostics for equality.
Throw an assert exception if given collections satisfy the following condition:
Elements that are only contained on one side. The equivalence is based on following properties
- File path (e.g., path/to/file.cs)
- Location of the
Diagnostic(starting line number, starting character position)-(finishing line number, finishing character position) - Identifier of the
DiagnosticDescriptor(DDID) (e.g., CS0494) DiagnosticMessage(e.g., The field 'C.hoge' is assigned but its value is never used)
Otherwise, do nothing.
[Test]
public async Task M()
{
var analyzer = new YourAnalyzer();
const string testData = @"
class C
{
string {|hoge|CS0414|The field 'C.hoge' is assigned but its value is never used|} = ""Forgot semicolon string""
}";
var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnosticFromFile(testData);
var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source);
DiagnosticsAssert.AreEqual(expected, actual);
}Output example
Missing 0 diagnostics, extra 1 diagnostics of all 2 diagnostics:
extra /0/Test0.cs: (3,43)-(3,43), CS1002, ; expected
DiagnosticAssert.IsEmpty assert that the Diagnositc is no exist.
Throw an assert exception if given collections exist any Diagnostic.
The output format and equivalence is the same as DiagnosticAssert.AreEqual.
Otherwise, do nothing.
[Test]
public async Task M()
{
var analyzer = new YourAnalyzer();
var source = @"
class C
{
}";
var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source);
DiagnosticsAssert.IsEmpty(actual);
}Create source and expected diagnostic from formatting embedded.
[Test]
public async Task M()
{
var analyzer = new YourAnalyzer();
const string testData = @"
class C
{
string {|hoge|CS0414|The field 'C.hoge' is assigned but its value is never used|} = ""Forgot semicolon string""
}";
var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnosticFromFile(testData);
}The testData variable has formatting embedded in the source.
You can parse this format using CreateSourceAndExpectedDiagnosticFromFile and get the source and expected Diagnostics.
- source
class C
{
string hoge = "Forgot semicolon string"
}-
expected Diagnostics
- Location
- (3,11)-(3,15)
- DDID
- CS0414
- DiagnosticMessage
- The field 'C.hoge' is assigned but its value is never used
- Location
Specify the part to be reported in the following format.
The format is enclosed in { } and separated by |.
{|source|DDID|DiagnosticMessage|}