ZCrew.Extensions.CodeAnalysis.CSharp.Testing 1.0.0-preview.8

This is a prerelease version of ZCrew.Extensions.CodeAnalysis.CSharp.Testing.
dotnet add package ZCrew.Extensions.CodeAnalysis.CSharp.Testing --version 1.0.0-preview.8
                    
NuGet\Install-Package ZCrew.Extensions.CodeAnalysis.CSharp.Testing -Version 1.0.0-preview.8
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ZCrew.Extensions.CodeAnalysis.CSharp.Testing" Version="1.0.0-preview.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ZCrew.Extensions.CodeAnalysis.CSharp.Testing" Version="1.0.0-preview.8" />
                    
Directory.Packages.props
<PackageReference Include="ZCrew.Extensions.CodeAnalysis.CSharp.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ZCrew.Extensions.CodeAnalysis.CSharp.Testing --version 1.0.0-preview.8
                    
#r "nuget: ZCrew.Extensions.CodeAnalysis.CSharp.Testing, 1.0.0-preview.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ZCrew.Extensions.CodeAnalysis.CSharp.Testing@1.0.0-preview.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ZCrew.Extensions.CodeAnalysis.CSharp.Testing&version=1.0.0-preview.8&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ZCrew.Extensions.CodeAnalysis.CSharp.Testing&version=1.0.0-preview.8&prerelease
                    
Install as a Cake Tool

ZCrew.Extensions.CodeAnalysis.CSharp.Testing

Testing library for verifying Roslyn source generators and diagnostic analyzers. It drives any number of them through Roslyn's AnalyzerTest harness from a small JSON descriptor, so each test case is just a set of input and expected-output files plus a .json that ties them together.

Preview. The API is still settling and breaks between versions.

The assertion runs in both directions: every file the generators emit must be listed as an expected generated file, and every file listed must be emitted. Listing none is therefore a real assertion — that the generators produce nothing for those inputs.

Installation

Available on NuGet for .NET Standard 2.0:

<PackageReference Include="ZCrew.Extensions.CodeAnalysis.CSharp.Testing" />

Writing a test case

A test case is three kinds of file in a TestCases/ folder next to your test class:

  • Input sources (e.g. MyCase.Attribute.cs) compiled as input to the generator.
  • Expected generated files (e.g. MyCase.SourceText.g.cs) the output to verify against.
  • A JSON descriptor (MyCase.json) maps the inputs, expected outputs, and any expected diagnostics:
{
    "SourceFiles": [
        { "SourceFileName": "MyCase.Attribute.cs" }
    ],
    "GeneratedFiles": [
        {
            "SourceFileName": "MyCase.SourceText.g.cs",
            "GeneratedFileName": "$(MyGenerator)/MyNamespace.MyTypeSourceText.g.cs"
        }
    ]
}

SourceFileName is the file on disk. GeneratedFileName is the full path Roslyn emits the source under: {generator assembly name}/{generator full type name}/{hint name}, where the hint name is what your generator passes to context.AddSource(hintName, ...).

Every registered generator contributes a variable named after its type, so $(MyGenerator) expands to its directory.

Expecting diagnostics

Declare expected diagnostics where they occur:

  • On a source file or generated file entry, an ExpectedDiagnostics array asserts diagnostics located in that file. Give each a location by Snippet (the start of its single occurrence in that file) or by an explicit 1-based Line/Column.
  • At the top level, an ExpectedDiagnostics array asserts diagnostics with no location (Location.None) — for example CS8785 when a generator throws, or a compilation-level analyzer diagnostic.

Each entry has an Id, an optional Severity (defaults to Error), and an optional Message to match exactly.

{
    "SourceFiles": [
        {
            "SourceFileName": "MyCase.Attribute.cs",
            "ExpectedDiagnostics": [
                { "Id": "CS0246", "Snippet": "CreateService<T>(" },
                { "Id": "CS0246", "Line": 10, "Column": 17 }
            ]
        }
    ],
    "GeneratedFiles": [
        {
            "SourceFileName": "MyCase.SourceText.g.cs",
            "GeneratedFileName": "$(MyGenerator)/MyNamespace.MyTypeSourceText.g.cs",
            "ExpectedDiagnostics": [
                { "Id": "CS0219", "Severity": "Warning", "Snippet": "unused" }
            ]
        }
    ],
    "ExpectedDiagnostics": [
        { "Id": "CS8785", "Severity": "Warning" }
    ]
}

Only the diagnostic's start is asserted, so the reported span may extend past the located snippet. A snippet that is missing or appears more than once in its file fails the test with a helpful message — use a more specific snippet or an explicit Line/Column.

Wiring the test

Configure a shared baseline once, then load and run each case. Resolve the TestCases folder from the source tree with TestPath.ForCaller() so fixtures are checked into git:

private static readonly RoslynTestBuilder<DefaultVerifier> Baseline =
    IncrementalGeneratorTestBuilder
        .CreateDefaultBuilder<MyGenerator>()
        .WithReferenceAssemblies(ReferenceAssemblies.Net.Net100)
        .WithAdditionalReferences("MyLibrary.dll");

private static readonly TestPath testCases = TestPath.ForCaller() / "TestCases";

[Theory]
[InlineData("MyCase.json")]
public async Task Generates_expected_sources(string descriptor)
{
    var testCase = await JsonTestCase.FromJsonFileAsync(testCases / descriptor);
    var test = await Baseline.BuildAsync(testCase);
    await test.RunAsync();
}

CreateDefaultBuilder verifies all compiler diagnostics, suppresses CS1591, and expects every generator's post-initialization sources (WithGeneratorPostInitializationSources). Start from Create instead to opt out.

WithAdditionalReferences adds the assemblies the input sources need — typically the library declaring the attributes your generator looks for, without which they fail to compile with CS0246. A bare file name resolves against the test's output directory, so the assembly has to land there (a ProjectReference to it does that).

The builder is immutable (every With* call forks a new builder) so a fully configured baseline can be shared as a fixture and specialized per test without affecting other tests.

Multiple Generators and Analyzers

The RoslynTestBuilder supports multiple generators and diagnostic analyzers:

RoslynTestBuilder
    .CreateDefaultBuilder()
    .WithIncrementalGenerator<MyIncrementalGenerator>()
    .WithSourceGenerator<MySourceGenerator>()
    .WithDiagnosticAnalyzer<MyAnalyzer>();

This would run every generator and then the analyzer on all input and output files. This can even be done with only source generators or with only diagnostic analyzers.

Project setup

Keep fixture files out of the compilation and out of the build output. Because the test resolves them from the source tree via TestPath.ForCaller(), they do not need to be copied to bin:

<ItemGroup>
  
  <Compile Remove="**/TestCases/**/*.cs" />
  
  <None Include="**/TestCases/**/*.json" Exclude="bin/**/*.json" />
  <None Include="**/TestCases/**/*.cs" Exclude="bin/**/*.cs" />
</ItemGroup>

Expected generated files are compared exactly, with no line-ending normalization, so a .g.cs fixture has to match the generator's output byte for byte. Keep git from rewriting them:

*.g.cs -text

and check that your formatter skips them (CSharpier ignores *.g.cs by default; others may not). If the generator builds its output with StringBuilder.AppendLine or Environment.NewLine, its line endings follow the OS it runs on and no single fixture can match both — emit a fixed newline instead if the tests run on more than one platform.

Updating expected files in place

Hand-writing and maintaining .g.cs files is tedious and I hate it, you should too. Add WithExpectedSourceUpdates() to the baseline and, whenever the generator's output differs from (or is missing) an expected file, the test overwrites it on disk with the produced output and still fails:

.WithExpectedSourceUpdates(enabled: Environment.GetEnvironmentVariable("CI") is null)

Workflow:

  1. Change your generator, or add a new test case whose .g.cs files do not exist yet.
  2. Run the tests. Mismatched and missing expected files are rewritten in place and the run is red.
  3. Review the changes in source control
  4. Keep them (re-run → green) or revert them.

The tests compare the output to previous file contents, so this won't cause false-positives. This does mean that when you get a failure and you've fixed it: the next test will fail (the previous file contents were broken and the new ones are fixed), and so running the tests again will then pass.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-preview.8 87 8/1/2026
1.0.0-preview.6 75 7/5/2026
1.0.0-preview.5 70 7/5/2026
1.0.0-preview.4 65 6/16/2026
1.0.0-preview.3 64 6/12/2026