Skip to content

RoslynTestBase

Unified base class for testing Roslyn analyzers, generators, and code fixes.

See also: Testing README | Reference Configuration | Roslyn Toolkit

Quick Start

All Roslyn tests inherit from RoslynTestBase:

public class MyTests : RoslynTestBase
{
    [Test]
    public async Task QuerySymbols()
    {
        var type = SymbolsFor("public class Foo { }").RequireNamedType("Foo");
        await Assert.That(type.Value.Name).IsEqualTo("Foo");
    }

    [Test]
    public async Task TestAnalyzer()
    {
        await AnalyzeWith<MyAnalyzer>(source)
            .ShouldReportDiagnostic("MY001");
    }

    [Test]
    public async Task TestGenerator()
    {
        await GenerateWith<MyGenerator>(source)
            .ShouldGenerate()
            .WithFileContaining("public partial class");
    }

    [Test]
    public async Task TestCodeFix()
    {
        await AnalyzeAndFixWith<MyAnalyzer, MyCodeFix>(source)
            .ForDiagnostic("MY001")
            .ShouldProduce(expectedSource);
    }
}

Entry Points

SymbolsFor(source)

Create a SymbolTestContext for querying symbols from compiled source:

var ctx = SymbolsFor(@"
    public class Customer 
    {
        public string Name { get; set; }
    }");

// Get/Require patterns - Require throws if not found
var type = ctx.RequireNamedType("Customer");
var prop = ctx.RequireProperty("Name");

// Optional patterns - returns OptionalSymbol
var maybeType = ctx.GetNamedType("Customer");
if (maybeType.IsNotValid(out var valid))
    return;

CompilationFor(source)

Get the raw Compilation for source code:

var compilation = CompilationFor(source);

AnalyzeWith\<TAnalyzer>(source)

Run an analyzer and make assertions about diagnostics:

await AnalyzeWith<MyAnalyzer>(source)
    .ShouldReportDiagnostic("MY001")
    .WithSeverity(DiagnosticSeverity.Error)
    .WithMessage("*must be partial*");

GenerateWith\<TGenerator>(source)

Run a source generator and make assertions about output:

await GenerateWith<MyGenerator>(source)
    .ShouldGenerate()
    .WithFileCount(2)
    .WithFileNamed("Customer.g.cs")
    .WithFileContaining("public partial class")
    .WithNoDiagnostics();

FixWith\<TCodeFix>(source)

Test a code fix for compiler diagnostics:

await FixWith<MyCodeFix>(source)
    .ForDiagnostic("CS0246")
    .ShouldProduce(expectedSource);

AnalyzeAndFixWith\<TAnalyzer, TCodeFix>(source)

Test a code fix for analyzer diagnostics:

await AnalyzeAndFixWith<MyAnalyzer, MyCodeFix>(source)
    .ForDiagnostic("MY001")
    .ShouldProduce(expectedSource);

RenderTemplateFrom\<TGenerator>(source)

Test Scriban template rendering with symbol context:

await RenderTemplateFrom<MyGenerator>(source)
    .Render("MyTemplate.scriban-cs", ctx => new { Name = ctx.RequireNamedType("Foo").Value.Name })
    .ShouldRender()
    .VerifySnapshot();

SymbolTestContext

Query symbols from a compilation with fluent APIs.

Direct Symbol Access

var ctx = SymbolsFor(source);

// Types
OptionalSymbol<INamedTypeSymbol> type = ctx.GetNamedType("Customer");
ValidSymbol<INamedTypeSymbol> type = ctx.RequireNamedType("Customer");

// Members (searches all types)
ValidSymbol<IMethodSymbol> method = ctx.RequireMethod("ProcessAsync");
ValidSymbol<IPropertySymbol> prop = ctx.RequireProperty("Name");
ValidSymbol<IFieldSymbol> field = ctx.RequireField("_logger");
ValidSymbol<IParameterSymbol> param = ctx.RequireParameter("id");

// Namespaces
ValidSymbol<INamespaceSymbol> ns = ctx.RequireNamespace("MyApp.Services");

Fluent Type Queries

// Query members on a specific type
var publicMethods = ctx.Type("Customer").Methods().ThatArePublic().GetAll();
var requiredProps = ctx.Type("Customer").Properties().ThatAreRequired().GetAll();
var constructors = ctx.Type("Customer").Constructors().GetAll();

// Query types in source (excludes referenced assemblies)
var allClasses = ctx.Types().ThatAreClasses().ThatArePublic().GetAll();

// Query all types including references
var withAttribute = ctx.AllTypesInCompilation()
    .ThatAreClasses()
    .WithAttribute("MyAttribute")
    .Query()
    .GetAll();

Projections

// Map a type to a custom projection
var model = ctx.Map("Customer", type => new CustomerModel(type));

// Query with compilation context
var info = ctx.Query(
    s => s.RequireNamedType("Runtime"),
    (symbol, compilation) => symbol.QuerySystemInfo(compilation));

Analyzer Assertions

// Assert specific diagnostic
await AnalyzeWith<MyAnalyzer>(source)
    .ShouldReportDiagnostic("MY001");

// With additional constraints
await AnalyzeWith<MyAnalyzer>(source)
    .ShouldReportDiagnostic("MY001")
    .WithSeverity(DiagnosticSeverity.Error)
    .WithMessage("*partial*");  // Wildcard matching

// Assert no specific diagnostic
await AnalyzeWith<MyAnalyzer>(source)
    .ShouldNotReportDiagnostic("MY001");

// Assert diagnostics exist (without specific ID)
await AnalyzeWith<MyAnalyzer>(source)
    .ShouldHaveDiagnostics()
    .WithErrorCode("MY001");

// Assert no diagnostics at all
await AnalyzeWith<MyAnalyzer>(source)
    .ShouldHaveNoDiagnostics();

Generator Assertions

// Basic generation check
await GenerateWith<MyGenerator>(source)
    .ShouldGenerate();

// Assert no generation
await GenerateWith<MyGenerator>(source)
    .ShouldNotGenerate();

// Detailed assertions
await GenerateWith<MyGenerator>(source)
    .ShouldGenerate()
    .WithFileCount(2)
    .WithFileNamed("Customer.g.cs")
    .WithFileContaining("public partial class")
    .WithoutFileContaining("// TODO");

// Diagnostic assertions
await GenerateWith<MyGenerator>(source)
    .ShouldGenerate()
    .WithNoDiagnostics();

await GenerateWith<MyGenerator>(source)
    .ShouldGenerate()
    .WithNoErrors();

await GenerateWith<MyGenerator>(source)
    .ShouldGenerate()
    .WithNoDiagnostics(filter => filter
        .WithSeverity(DiagnosticSeverity.Error)
        .WithId("DS0001"));

// Snapshot testing
await GenerateWith<MyGenerator>(source)
    .ShouldGenerate()
    .VerifySnapshot();

Code Fix Assertions

// Test code fix for analyzer diagnostic
await AnalyzeAndFixWith<MyAnalyzer, MyCodeFix>(source)
    .ForDiagnostic("MY001")
    .ShouldProduce(expectedSource);

// Test code fix for compiler diagnostic
await FixWith<MyCodeFix>(source)
    .ForDiagnostic("CS0246")
    .ShouldProduce(expectedSource);

// Add analyzer dynamically
await FixWith<MyCodeFix>(source)
    .WithAnalyzer<MyAnalyzer>()
    .ForDiagnostic("MY001")
    .ShouldProduce(expectedSource);

Template Assertions

// Test template rendering with symbol queries
await RenderTemplateFrom<MyGenerator>(source)
    .Render("MyTemplate.scriban-cs", ctx => new 
    { 
        Name = ctx.RequireNamedType("Foo").Value.Name,
        Properties = ctx.Type("Foo").Properties().GetAll()
    })
    .ShouldRender()
    .WithContent("public class Foo");

// Direct context object
await RenderTemplateFrom<MyGenerator>(source)
    .Render("MyTemplate.scriban-cs", new { Name = "Test" })
    .ShouldRender()
    .VerifySnapshot();

// Assert template failure
await RenderTemplateFrom<MyGenerator>(source)
    .Render("Invalid.scriban-cs", new { })
    .ShouldFail();

Reference Configuration

If your tests need custom assembly references (e.g., for attributes), configure them once via ModuleInitializer:

[ModuleInitializer]
public static void Init() =>
    ReferenceConfiguration.AddReferencesFromTypes(typeof(MyAttribute));

See Reference Configuration for details.


License

RPL-1.5 (Reciprocal Public License) — Real reciprocity, no loopholes.

You can use this code, modify it, and share it freely. But when you deploy it — internally or externally, as a service or within your company — you share your improvements back under the same license.

Why? We believe if you benefit from this code, the community should benefit from your improvements. That's the deal we think is fair.

Personal research and experimentation? No obligations. Go learn, explore, and build.

See LICENSE for the full legal text.