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
AnalyzerVerifier
has 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.SyntaxTreeActionHistory
var 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.SyntaxTreeAction
DiagnosticAssert.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|}