Testing Source Generators with Microsoft.CodeAnalysis.Testing

Source generators produce code that becomes part of your compilation. If the generated code has a syntax error, a missing namespace, or a logic bug, you will only discover it when the consuming project fails to build — or worse, at runtime. A proper test suite catches these problems before they escape.

The Microsoft.CodeAnalysis.Testing family of packages provides a structured way to test analysers, code fix providers, and source generators.

Setting Up the Test Project

Create a test project and add the necessary packages:

terminal
dotnet new xunit -n MyGenerator.Tests
cd MyGenerator.Tests
dotnet add package Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit
dotnet add package Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit

These packages provide base test classes and verifiers that handle the boilerplate of creating compilations, running generators, and asserting results.

Testing an Analyser

The testing framework uses a verifier pattern. You provide source code with markup indicating where diagnostics are expected:

Example.cs
using Microsoft.CodeAnalysis.Testing;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
    MethodLengthAnalyser,
    DefaultVerifier>;

public class MethodLengthAnalyserTests
{
    [Fact]
    public async Task Long_Method_Reports_Diagnostic()
    {
        // Generate a method body with 35 lines
        var lines = string.Join("\n",
            Enumerable.Range(0, 35)
                .Select(i => $"        var x{i} = {i};"));

        var test = $$"""
            class C
            {
                void {|#0:LongMethod|}()
                {
            {{lines}}
                }
            }
            """;

        var expected = Verify.Diagnostic("ML001")
            .WithLocation(0)
            .WithArguments("LongMethod", 37, 30);

        await Verify.VerifyAnalyzerAsync(test, expected);
    }

    [Fact]
    public async Task Short_Method_No_Diagnostic()
    {
        var test = """
            class C
            {
                void ShortMethod()
                {
                    var x = 1;
                }
            }
            """;

        // No expected diagnostics — verifier confirms none are reported
        await Verify.VerifyAnalyzerAsync(test);
    }
}

The {|#0:LongMethod|} markup marks location #0. The WithLocation(0) assertion references it. This approach is more robust than hardcoding line and column numbers.

Testing a Source Generator

For source generators, you can use CSharpGeneratorDriver directly. This gives you more control:

Example.cs
[Fact]
public void Generator_Emits_ToString_Override()
{
    var source = """
        using Generated;

        namespace TestApp;

        [AutoToString]
        public partial class Customer
        {
            public string Name { get; set; }
            public int Id { get; set; }
        }
        """;

    // Create compilation
    var syntaxTree = CSharpSyntaxTree.ParseText(source);
    var references = new[]
    {
        MetadataReference.CreateFromFile(
            typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(
            typeof(Attribute).Assembly.Location),
    };

    var compilation = CSharpCompilation.Create("Tests")
        .AddReferences(references)
        .AddSyntaxTrees(syntaxTree)
        .WithOptions(new CSharpCompilationOptions(
            OutputKind.DynamicallyLinkedLibrary));

    // Run generator
    var generator = new AutoToStringGenerator();
    GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
    driver = driver.RunGeneratorsAndUpdateCompilation(
        compilation,
        out var outputCompilation,
        out var diagnostics);

    // Assert no generator diagnostics
    Assert.Empty(diagnostics);

    // Assert no compilation errors in the output
    var compilationDiagnostics = outputCompilation.GetDiagnostics()
        .Where(d => d.Severity == DiagnosticSeverity.Error);
    Assert.Empty(compilationDiagnostics);

    // Assert generated source content
    var runResult = driver.GetRunResult();
    var generatedTree = runResult.GeneratedTrees
        .Single(t => t.FilePath.Contains("Customer.ToString"));

    var generatedText = generatedTree.GetText().ToString();
    Assert.Contains("public override string ToString()", generatedText);
    Assert.Contains("Name", generatedText);
    Assert.Contains("Id", generatedText);
}

Snapshot Testing

For generators that produce substantial output, comparing the full generated text against a saved snapshot is more maintainable than individual assertions:

Example.cs
[Fact]
public Task Generator_Output_Matches_Snapshot()
{
    // ... setup compilation and run generator as above ...

    var runResult = driver.GetRunResult();
    var generatedText = runResult.GeneratedTrees
        .Single(t => t.FilePath.Contains("Customer"))
        .GetText()
        .ToString();

    // Using the Verify library (https://github.com/VerifyTests/Verify)
    return Verify(generatedText);
}

The Verify library saves the expected output as a .verified.txt file alongside your test. On the first run, it creates the file. On subsequent runs, it compares against it. When the output legitimately changes, you review and accept the new snapshot.

Testing Incremental Behaviour

One of the key benefits of incremental generators is caching. You can verify this works:

Example.cs
[Fact]
public void Generator_Is_Cached_When_Unrelated_File_Changes()
{
    // ... initial setup ...
    driver = driver.RunGeneratorsAndUpdateCompilation(
        compilation, out _, out _);

    var firstResult = driver.GetRunResult();

    // Add an unrelated syntax tree
    var unrelatedTree = CSharpSyntaxTree.ParseText(
        "class Unrelated { }");
    var newCompilation = compilation.AddSyntaxTrees(unrelatedTree);

    // Run again
    driver = driver.RunGeneratorsAndUpdateCompilation(
        newCompilation, out _, out _);

    var secondResult = driver.GetRunResult();

    // The generator result should indicate it was cached
    var generatorResult = secondResult.Results.Single();
    Assert.All(generatorResult.TrackedSteps.SelectMany(s => s.Value),
        step => Assert.All(step.Outputs,
            output => Assert.Equal(
                IncrementalStepRunReason.Cached,
                output.Reason)));
}

The TrackedSteps API lets you inspect which pipeline stages ran and which were cached, giving you confidence that your generator is truly incremental.

Key Recommendations