Microsoft are working on an official package for testing analyzers: Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.
Hopefully this library will not be needed in the future.
Asserts for testing Roslyn analyzers.
Use 1.x for Microsoft.CodeAnalysis 1.x
- RoslynAssert.Valid
- RoslynAssert.Diagnostics
- CodeFix
- FixAll
- NoFix
- Refactoring
- AST
- Settings
- Analyze
- Fix
- CodeFactory
- Benchmark
- SyntaxNodeExt
- AstView
- Usage with different test project types
Use RoslynAssert.Valid<NopAnalyzer>(code)
to test that an analyzer does not report errors for valid code.
The code is checked so that it does not have any compiler errors either.
A typical test fixture looks like:
public class Valid
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
[Test]
public void M()
{
var code = @"
namespace N
{
class C
{
}
}";
RoslynAssert.Valid(Analyzer, code);
}
...
}
If the analyzer produces many diagnostics you can pass in a descriptor so that only diagnostics matching it are checked.
public class Valid
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
private static readonly DiagnosticDescriptor Descriptor = YourAnalyzer.SomeDescriptor;
[Test]
public void M()
{
var code = @"
namespace N
{
class C
{
}
}";
RoslynAssert.Valid(Analyzer, Descriptor, code);
}
...
}
When testing all analyzers something like this can be used:
public class Valid
{
private static readonly IReadOnlyList<DiagnosticAnalyzer> AllAnalyzers =
typeof(TypeInAnalyzerAssembly)
.Assembly.GetTypes()
.Where(typeof(DiagnosticAnalyzer).IsAssignableFrom)
.Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t))
.ToArray();
private static readonly Solution ValidProjectSln = CodeFactory.CreateSolution(
ProjectFile.Find("Valid.csproj"),
AllAnalyzers);
[TestCaseSource(nameof(AllAnalyzers))]
public void Valid(DiagnosticAnalyzer analyzer)
{
RoslynAssert.Valid(analyzer, ValidProjectSln);
}
}
Use RoslynAssert.Diagnostics<FieldNameMustNotBeginWithUnderscore>(code)
to test that the analyzer reports an error or warning at the position indicated by the character ↓
. To type this character, hold down Alt and use the numpad to type the number 25.
A typical test fixture looks like:
public class Diagnostics
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(YourAnalyzer.Descriptor);
[Test]
public void M()
{
var code = @"
namespace N
{
class ↓C
{
}
}";
RoslynAssert.Diagnostics(Analyzer, code);
}
[Test]
public void M()
{
var code = @"
namespace N
{
class ↓C
{
}
}";
RoslynAssert.Diagnostics(Analyzer, ExpectedDiagnostic.WithMessage("Don't name it C"), code);
}
...
}
If the analyzer produces many diagnostics you can pass in a descriptor so that only diagnostics matching it are checked.
public class Diagnostics
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(YourAnalyzer.Descriptor);
[Test]
public void M()
{
var code = @"
namespace N
{
class ↓Foo
{
}
}";
RoslynAssert.Diagnostics(Analyzer, ExpectedDiagnostic, code);
}
...
}
If the analyzer supports many diagnostics the overload with ExpectedDiagnostic
must be used. This suppresses all diagnstics other than the expected.
Use RoslynAssert.Suppressed(suppressor, code)
to test
that the suppressor suppresses an error or warning at the position
indicated by the character ↓
. To type this character, hold down Alt
and use the numpad to type the number 25.
A typical test fixture looks like:
public class Suppression
{
private static readonly DiagnosticSuppressor Suppressor = new YourSuppressor();
[Test]
public void M()
{
var code = @"
namespace N
{
class C
{
public string ↓F;
}
}";
RoslynAssert.NotSuppressed(Suppressor, code);
}
[Test]
public void M()
{
var code = @"
namespace N
{
class C
{
public string ↓Magic;
}
}";
RoslynAssert.Suppressed(Suppressor, code);
}
...
}
If the suppressor can suppress many diagnostics you can pass in a descriptor so that only diagnostics matching it are checked.
public class Diagnostics
{
private static readonly DiagnosticSuppressor Suppressor = new YourSuppressor();
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(YourSuppressor.Descriptor);
[Test]
public void M()
{
var code = @"
namespace N
{
class ↓Foo
{
}
}";
RoslynAssert.Suppressed(Analyzer, ExpectedDiagnostic, code);
}
...
}
Test that the analyzer reports an error or warning at the position indicated by the character ↓
and that the code fix fixes it and produces the expected code.
To type this character, hold down Alt and use the numpad to type the number 25.
public class CodeFix
{
private static readonly DiagnosticAnalyzer Analyzer = new FieldNameMustNotBeginWithUnderscore();
private static readonly CodeFixProvider Fix = new SA1309CodeFixProvider();
[Test]
public void M()
{
var before = @"
namespace N
{
class C
{
private readonly int ↓_value;
}
}";
var after = @"
namespace N
{
class C
{
private readonly int value;
}
}";
RoslynAssert.CodeFix(Analyzer, Fix, before, after);
}
}
A typical test fixture looks like:
public class CodeFix
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
private static readonly CodeFixProvider Fix = new YorCodeFixProvider();
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(YourAnalyzer.Descriptor);
[Test]
public void M1()
{
var before = @"
namespace N
{
class C
{
private readonly int ↓_value;
}
}";
var after = @"
namespace N
{
class C
{
private readonly int value;
}
}";
RoslynAssert.CodeFix(Analyzer, Fix, before, after);
}
}
With explicit title for the fix to apply. Useful when there are many candidate fixes.
public class CodeFix
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
private static readonly CodeFixProvider Fix = new YorCodeFixProvider();
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(YourAnalyzer.Descriptor);
[Test]
public void M1()
{
var before = @"
namespace N
{
class C
{
private readonly int ↓_value;
}
}";
var after = @"
namespace N
{
class C
{
private readonly int value;
}
}";
RoslynAssert.CodeFix(Analyzer, Fix, before, after, fixTitle: "Don't use underscore prefix");
}
...
}
If the analyzer supports many diagnostics the overload with ExpectedDiagnostic
must be used. This suppresses all diagnostics other than the expected.
When the code fix is for a warning produced by an analyzer that you do not own, for example a built in analyzer in Visual Studio.
public class CodeFix
{
private static readonly CodeFixProvider Fix = new RemoveUnusedFixProvider();
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create("CS0067");
[Test]
public void TestThatCodeFixProducesExpectedCode()
{
var before = @"
namespace N
{
using System;
public class C
{
public event EventHandler ↓Bar;
}
}";
var after = @"
namespace N
{
using System;
public class C
{
}
}";
RoslynAssert.CodeFix(Fix, ExpectedDiagnostic, before, after);
}
}
When there are many issues that will be fixed: RoslynAssert.FixAll does:
- Fix all diagnostics one by one
- Fix all diagnostics in all supported scopes.
public class CodeFix
{
private static readonly DiagnosticAnalyzer Analyzer = new FieldNameMustNotBeginWithUnderscore();
private static readonly CodeFixProvider Fix = new SA1309CodeFixProvider();
[Test]
public void M()
{
var before = @"
namespace N
{
class C
{
private readonly int ↓_value1;
private readonly int ↓_value2;
}
}";
var after = @"
namespace N
{
class C
{
private readonly int value1;
private readonly int value2;
}
}";
RoslynAssert.FixAll(Analyzer, Fix, before, after);
}
}
Test that the analyzer reports an error or warning at the position indicated by the character ↓
and that the code fix does not change the code.
To type this character, hold down Alt and use the numpad to type the number 25.
This can happen if for example it is decided to not support rare edge cases with the code fix.
public class CodeFix
{
private static readonly DiagnosticAnalyzer Analyzer = new FieldNameMustNotBeginWithUnderscore();
private static readonly CodeFixProvider Fix = new SA1309CodeFixProvider();
[Test]
public void M()
{
var code = @"
namespace N
{
class C
{
private readonly int ↓_value;
}
}";
RoslynAssert.NoFix(Analyzer, Fix, code);
}
}
public class CodeFix
{
private static readonly CodeRefactoringProvider Refactoring = new ClassNameToUpperCaseRefactoringProvider();
[Test]
public void M()
{
var before = @"
class ↓c
{
}";
var after = @"
class C
{
}";
RoslynAssert.Refactoring(Refactoring, before, after);
// Or if you want to assert on title also
RoslynAssert.Refactoring(Refactoring, before, after, title: "Change to uppercase.");
}
}
For checking every node and token in the tree.
[Test]
public void CheckAst()
{
var actual = SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("a"), SyntaxFactory.IdentifierName("b"));
var expected = CSharpSyntaxTree.ParseText("var c = a + b").FindAssignmentExpression("a + b");
RoslynAssert.Ast(expected, actual);
}
Get a string with a call to SyntaxFactory for generating the code passed in.
var code = @"namespace A.B
{
public class C
{
}
}";
var call = SyntaxFactoryWriter.Serialize(code);
Settings.Default is meant to be set once and contains information used by asserts and code factory methods. If specific settings are required for a test there are overloads acceping a settings intance.
internal static class ModuleInitializer
{
[ModuleInitializer]
internal static void Initialize()
{
Settings.Default = Settings.Default.WithMetadataReferences(
// This adds all transitive metadata references from containing project.
Asserts.MetadataReferences.Transitive(typeof(ModuleInitializer)));
}
}
Analyze a cs, csproj or sln file on disk.
[Test]
public async Task GetDiagnosticsFromProjectOnDisk()
{
var dllFile = new Uri(Assembly.GetExecutingAssembly().CodeBase, UriKind.Absolute).LocalPath;
Assert.AreEqual(true, CodeFactory.TryFindProjectFile(new FileInfo(dllFile), out FileInfo projectFile));
var diagnostics = await Analyze.GetDiagnosticsAsync(new FieldNameMustNotBeginWithUnderscore(), projectFile)
.ConfigureAwait(false);
...
}
When dropping down to manual mode Analyze
& Fix
can be used like this:
[Test]
public void SingleClassOneErrorCorrectFix()
{
var code = @"
namespace N
{
class Foo
{
private readonly int _value;
}
}";
var after = @"
namespace N
{
class Foo
{
private readonly int value;
}
}";
var analyzer = new FieldNameMustNotBeginWithUnderscore();
var settings = Settings.Default.WithCompilationOptions(CodeFactory.DefaultCompilationOptions(analyzer))
.WithMetadataReferences(MetadataReference.CreateFromFile(typeof(int).Assembly.Location))
var sln = CodeFactory.CreateSolution(code, settings: settings);
var diagnostics = Analyze.GetDiagnostics(sln, analyzer);
var fixedSln = Fix.Apply(sln, new DoNotUseUnderscoreFix(), diagnostics);
CodeAssert.AreEqual(after, fixedSln.Projects.Single().Documents.Single());
}
[Test]
public void CreateSolutionFromSources()
{
var code = @"
namespace N
{
class Foo
{
private readonly int _value;
}
}";
var sln = CodeFactory.CreateSolution(code, new[] { new FieldNameMustNotBeginWithUnderscore() });
Assert.AreEqual("N", sln.Projects.Single().Name);
Assert.AreEqual("Foo.cs", sln.Projects.Single().Documents.Single().Name);
}
[Test]
public void CreateSolutionFromSources()
{
var code1 = @"
namespace Project1
{
class Foo1
{
private readonly int _value;
}
}";
var code2 = @"
namespace Project2
{
class Foo2
{
private readonly int _value;
}
}";
var sln = CodeFactory.CreateSolution(new[] { code1, code2 }, new[] { new FieldNameMustNotBeginWithUnderscore() });
CollectionAssert.AreEqual(new[] { "Project1", "Project2" }, sln.Projects.Select(x => x.Name));
Assert.AreEqual(new[] { "Foo1.cs", "Foo2.cs" }, sln.Projects.Select(x => x.Documents.Single().Name));
}
[Test]
public void CreateSolutionFromProjectFile()
{
Assert.AreEqual(
true,
CodeFactory.TryFindProjectFile(
new FileInfo(new Uri(Assembly.GetExecutingAssembly().CodeBase, UriKind.Absolute).LocalPath),
out FileInfo projectFile));
var solution = CodeFactory.CreateSolution(projectFile);
}
[Test]
public void CreateSolutionFromSolutionFile()
{
Assert.AreEqual(
true,
CodeFactory.TryFindFileInParentDirectory(
new FileInfo(new Uri(Assembly.GetExecutingAssembly().CodeBase, UriKind.Absolute).LocalPath).Directory, "Gu.Roslyn.Asserts.sln",
out FileInfo solutionFile));
var solution = CodeFactory.CreateSolution(solutionFile);
}
Sample benchmark using BenchmarkDotNet.
public class FieldNameMustNotBeginWithUnderscoreBenchmark
{
private static readonly Solution Solution = CodeFactory.CreateSolution(
CodeFactory.FindSolutionFile("Gu.Roslyn.Asserts.sln"));
private static readonly Benchmark Benchmark = Benchmark.Create(Solution, new FieldNameMustNotBeginWithUnderscore());
[BenchmarkDotNet.Attributes.Benchmark]
public void RunOnGuRoslynAssertsSln()
{
Benchmark.Run();
}
}
[Test]
public void FindAssignmentExpressionDemo()
{
var syntaxTree = CSharpSyntaxTree.ParseText(
@"
namespace N
{
internal class Foo
{
internal Foo()
{
var temp = 1;
temp = 2;
}
}
}");
var compilation = CSharpCompilation.Create("test", new[] { syntaxTree });
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var assignment = syntaxTree.FindAssignmentExpression("temp = 2");
Assert.AreEqual("temp = 2", assignment.ToString());
Assert.AreEqual("int", semanticModel.GetTypeInfo(assignment.Right).Type.ToDisplayString());
}
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>