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
- Attributes
- Analyze
- Fix
- CodeFactory
- Benchmark
- SyntaxNodeExt
- AstView
- Usage with different test project types
Use RoslynAssert.Valid<NoErrorAnalyzer>(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 ValidCode
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
[Test]
public void SomeTest()
{
var code = @"
namespace TestCode
{
class Foo
{
}
}";
RoslynAssert.Valid(YourAnalyzer, code);
}
...
}
If the analyzer produces many diagnostics you can pass in a descriptor so that only diagnostics matching it are checked.
public class ValidCode
{
private static readonly DiagnosticAnalyzer Analyzer = new YourAnalyzer();
private static readonly DiagnosticDescriptor Descriptor = YourAnalyzer.SomeDescriptor;
[Test]
public void SomeTest()
{
var code = @"
namespace TestCode
{
class Foo
{
}
}";
RoslynAssert.Valid(YourAnalyzer, Descriptor, code);
}
...
}
When testing all analyzers something like this can be used:
public class ValidCodeWithAllAnalyzers
{
private static readonly IReadOnlyList<DiagnosticAnalyzer> AllAnalyzers = typeof(KnownSymbol)
.Assembly.GetTypes()
.Where(typeof(DiagnosticAnalyzer).IsAssignableFrom)
.Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t))
.ToArray();
private static readonly Solution ValidCodeProjectSln = CodeFactory.CreateSolution(
ProjectFile.Find("ValidCode.csproj"),
AllAnalyzers,
RoslynAssert.MetadataReferences);
[TestCaseSource(nameof(AllAnalyzers))]
public void ValidCodeProject(DiagnosticAnalyzer analyzer)
{
RoslynAssert.Valid(analyzer, ValidCodeProjectSln);
}
}
Use RoslynAssert.Diagnostics<FieldNameMustNotBeginWithUnderscore>(code)
to test that the analyzer reports error or warning at position indicated with ↓
With an aplhanumeric keyboard alt + 25
writes ↓
.
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 SomeTest()
{
var code = @"
namespace TestCode
{
class ↓Foo
{
}
}";
RoslynAssert.Diagnostics(YourAnalyzer, code);
}
[Test]
public void CheckMessageAlso()
{
var code = @"
namespace TestCode
{
class ↓Foo
{
}
}";
RoslynAssert.Diagnostics(YourAnalyzer, ExpectedDiagnostic.WithMessage("Don't name it foo"), 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 SomeTest()
{
var code = @"
namespace TestCode
{
class ↓Foo
{
}
}";
RoslynAssert.Diagnostics(YourAnalyzer, ExpectedDiagnostic, code);
}
...
}
If the analyzer supports many diagnostics the overload with ExpectedDiagnostic
must be used. This suppresses all diagnsics other than the expected.
Test that the analyzer reports an error or warning at position indicated with ↓ and that the codefix fixes it and produces the expected code.
With an aplhanumeric keyboard alt + 25
writes ↓
.
[Test]
public void TestThatAnalyzerWarnsOnCorrectPositionAndThatCodeFixProducesExpectedCode()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int ↓_value;
}
}";
var fixedCode = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int value;
}
}";
RoslynAssert.CodeFix<FieldNameMustNotBeginWithUnderscore, SA1309CodeFixProvider>(code, fixedCode);
}
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 SomeTest()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int ↓_value;
}
}";
var fixedCode = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int value;
}
}";
RoslynAssert.CodeFix(Analyzer, Fix, code, fixedCode);
}
[Test]
public void ExplicitFixTitle()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int ↓_value;
}
}";
var fixedCode = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int value;
}
}";
RoslynAssert.CodeFix(Analyzer, Fix, code, fixedCode, fixTitle: "Don't use underscore prefix");
}
...
}
With explicit title for the fix to apply. Useful when there are many candidate fixes.
If the analyzer supports many diagnostics the overload with ExpectedDiagnostic
must be used. This suppresses all diagnsics 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.
[Test]
public void TestThatCodeFixProducesExpectedCode()
{
var code = @"
namespace RoslynSandbox
{
using System;
public class Foo
{
public event EventHandler ↓Bar;
}
}";
var fixedCode = @"
namespace RoslynSandbox
{
using System;
public class Foo
{
}
}";
RoslynAssert.CodeFix<RemoveUnusedFixProvider>("CS0067", code, fixedCode);
}
When there are many isses that will be fixed:
[Test]
public void TestThatAnalyzerWarnsOnCorrectPositionAndThatCodeFixProducesExpectedCode()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int ↓_value1;
private readonly int ↓_value2;
}
}";
var fixedCode = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int value1;
private readonly int value2;
}
}";
RoslynAssert.FixAll<FieldNameMustNotBeginWithUnderscore, SA1309CodeFixProvider>(code, fixedCode);
}
Test that the analyzer reports an error or warning at position indicated with ↓ and that the codefix does not change the code.
With an aplhanumeric keyboard alt + 25
writes ↓
.
This can happen if for example it is decided to not support rare edge cases with the code fix.
[Test]
public void TestThatAnalyzerWarnsOnCorrectPositionAndThatCodeFixDoesNothing()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int ↓_value;
}
}";
RoslynAssert.NoFix<FieldNameMustNotBeginWithUnderscore, SA1309CodeFixProvider>(code);
}
[Test]
public void WithPositionIndicated()
{
var testCode = @"
class ↓Foo
{
}";
var fixedCode = @"
class FOO
{
}";
RoslynAssert.Refactoring(new ClassNameToUpperCaseRefactoringProvider(), testCode, fixedCode);
// Or if you want to assert on title also
RoslynAssert.Refactoring(new ClassNameToUpperCaseRefactoringProvider(), testCode, fixedCode, 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);
When creating the workspace to analyze metadata references need to be added. There are a couple of ways to provide them using this library. Some overloads of the asserts allow passing explicit references but it will be verbose to do that everywhere.
In most scenarios something like this in the test project is what you want:
using Gu.Roslyn.Asserts;
[assembly: TransitiveMetadataReferences(
typeof(Microsoft.EntityFrameworkCore.DbContext),
typeof(Microsoft.AspNetCore.Mvc.Controller))]
For specifying a metadata reference to be used in the tests, with or without aliases.
[assembly: MetadataReference(typeof(object), new[] { "global", "corlib" })]
For specifying a batch of metadata references to be used in the tests.
[assembly: MetadataReferences(
typeof(System.Linq.Enumerable),
typeof(System.Net.WebClient),
typeof(System.Reactive.Disposables.SerialDisposable),
typeof(System.Reactive.Disposables.ICancelable),
typeof(System.Reactive.Linq.Observable),
typeof(Gu.Reactive.Condition),
typeof(Gu.Wpf.Reactive.ConditionControl),
typeof(System.Xml.Serialization.XmlSerializer),
typeof(System.Windows.Media.Matrix),
typeof(Microsoft.CodeAnalysis.CSharp.CSharpCompilation),
typeof(Microsoft.CodeAnalysis.Compilation),
typeof(NUnit.Framework.Assert))]
Calling RoslynAssert.ResetMetadataReferences()
resets RoslynAssert.MetadataReferences
to the list provided via the attribute or clears it if no attribute is provided.
For getting all metadata references specified with attributes use:
var compilation = CSharpCompilation.Create(
"TestProject",
new[] { syntaxTree },
MetadataReferences.FromAttributes());
using System.Reflection;
using System.Runtime.InteropServices;
using Gu.Roslyn.Asserts;
...
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: MetadataReference(typeof(object), new[] { "global", "corlib" })]
[assembly: MetadataReference(typeof(System.Diagnostics.Debug), new[] { "global", "system" })]
[assembly: MetadataReferences(
typeof(System.Linq.Enumerable),
typeof(System.Net.WebClient),
typeof(System.Data.Common.DbConnection),
typeof(System.Reactive.Disposables.SerialDisposable),
typeof(System.Reactive.Disposables.ICancelable),
typeof(System.Reactive.Linq.Observable),
typeof(System.Xml.Serialization.XmlSerializer),
typeof(System.Windows.Media.Brush),
typeof(System.Windows.Controls.Control),
typeof(System.Windows.Media.Matrix),
typeof(System.Xaml.XamlLanguage),
typeof(Microsoft.CodeAnalysis.CSharp.CSharpCompilation),
typeof(Microsoft.CodeAnalysis.Compilation),
typeof(NUnit.Framework.Assert))]
RoslynAssert.MetadataReferences.Add(MetadataReference.CreateFromFile(typeof(int).Assembly.Location));
A helper like this can be used.
private static IReadOnlyList<MetadataReference> CreateMetadataReferences(params Type[] types)
{
return types.Select(type => type.GetTypeInfo().Assembly)
.Distinct()
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
.ToArray();
}
For globally ignoring compiler warnings and errors introduced by code fixes when calling calling RoslynAssert.CodeFix and RoslynAssert.FixAll.
[assembly: IgnoredErrors("CS1569", ...)]
For globally ignoring compiler warnings and errors introduced by code fixes when calling calling RoslynAssert.CodeFix and RoslynAssert.FixAll.
[assembly: AllowedDiagnostics(AllowedDiagnostics.Warnings)]
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, MetadataReferences)
.ConfigureAwait(false);
...
}
When dropping down to manual mode Analyze
& Fix
can be used like this:
[Test]
public void SingleClassOneErrorCorrectFix()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int _value;
}
}";
var fixedCode = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int value;
}
}";
var analyzer = new FieldNameMustNotBeginWithUnderscore();
var cSharpCompilationOptions = CodeFactory.DefaultCompilationOptions(analyzer);
var metadataReferences = new[] { MetadataReference.CreateFromFile(typeof(int).Assembly.Location) };
var sln = CodeFactory.CreateSolution(code, cSharpCompilationOptions, metadataReferences);
var diagnostics = Analyze.GetDiagnostics(sln, analyzer);
var fixedSln = Fix.Apply(sln, new DontUseUnderscoreCodeFixProvider(), diagnostics);
CodeAssert.AreEqual(fixedCode, fixedSln.Projects.Single().Documents.Single());
}
[Test]
public void CreateSolutionFromSources()
{
var code = @"
namespace RoslynSandbox
{
class Foo
{
private readonly int _value;
}
}";
var sln = CodeFactory.CreateSolution(code, new[] { new FieldNameMustNotBeginWithUnderscore() });
Assert.AreEqual("RoslynSandbox", 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,
new[] { new FieldNameMustNotBeginWithUnderscore(), },
CreateMetadataReferences(typeof(object)));
}
[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,
new[] { new FieldNameMustNotBeginWithUnderscore(), },
CreateMetadataReferences(typeof(object)));
}
Sample benchmark using BenchmarkDotNet.
public class FieldNameMustNotBeginWithUnderscoreBenchmark
{
private static readonly Solution Solution = CodeFactory.CreateSolution(
CodeFactory.FindSolutionFile("Gu.Roslyn.Asserts.sln"),
MetadataReferences.Transitive(typeof(Benchmark).Assembly).ToArray());
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 RoslynSandbox
{
internal class Foo
{
internal Foo()
{
var temp = 1;
temp = 2;
}
}
}");
var compilation = CSharpCompilation.Create("test", new[] { syntaxTree }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), });
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>
TODO figure out what is needed here.