Skip to main content
ANcpLua.Roslyn.Utilities.Testing provides fluent base classes for testing Roslyn components:
Base ClassPurpose
AnalyzerTest<TAnalyzer>Test diagnostic analyzers
CodeFixTest<TAnalyzer, TCodeFix>Test code fix providers
RefactoringTest<TRefactoring>Test code refactoring providers
Test<TGenerator>Test incremental source generators
These base classes work with xUnit v3’s ambient TestContext. Use TestContext.Current.TestOutputHelper for diagnostic output without constructor injection.

Installation

<PackageReference Include="ANcpLua.Roslyn.Utilities.Testing" Version="1.15.0" />

AnalyzerTest

Base class for testing diagnostic analyzers with pre-configured .NET 10 reference assemblies.

Basic Usage

using ANcpLua.Roslyn.Utilities.Testing;

public class MyAnalyzerTests : AnalyzerTest<MyAnalyzer>
{
    [Fact]
    public Task ShouldReportWarning() => VerifyAsync("""
        class C
        {
            void M() { {|MY001:BadCode()|}; }
        }
        """);

    [Fact]
    public Task ShouldNotReport() => VerifyAsync("""
        class C
        {
            void M() { GoodCode(); }
        }
        """);
}
Use diagnostic markup {|DIAGNOSTIC_ID:code|} to mark expected diagnostic locations.

Testing with Additional Files

For analyzers that inspect non-C# files (MSBuild props, JSON configs, etc.):
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;

public class PropsAnalyzerTests : AnalyzerTest<MyPropsAnalyzer>
{
    [Fact]
    public Task ShouldWarnWhenMissingImport()
    {
        const string directoryBuildProps = """
            <Project>
                <PropertyGroup>
                    <SomeProperty>Value</SomeProperty>
                </PropertyGroup>
            </Project>
            """;

        var expected = new DiagnosticResult("MY001", DiagnosticSeverity.Warning)
            .WithLocation("Directory.Build.props", 1, 1);

        return VerifyAsync(
            source: "public class C { }",
            additionalFiles: [("Directory.Build.props", directoryBuildProps)],
            expectedDiagnostics: [expected]);
    }

    [Fact]
    public Task ShouldNotWarnWhenImportPresent()
    {
        const string directoryBuildProps = """
            <Project>
                <Import Project="Version.props" />
            </Project>
            """;

        // No expected diagnostics = verify clean compilation
        return VerifyAsync(
            source: "public class C { }",
            additionalFiles: [("Directory.Build.props", directoryBuildProps)]);
    }
}

Target Framework Selection

// Default: .NET 10 references
await VerifyAsync(source);

// For netstandard2.0 analyzers (e.g., source generators)
await VerifyAsync(source, useNet10References: false);

CodeFixTest

Base class for testing code fix providers that transform diagnostic-marked code.
using ANcpLua.Roslyn.Utilities.Testing;

public class MyCodeFixTests : CodeFixTest<MyAnalyzer, MyCodeFix>
{
    [Fact]
    public Task ShouldFixWarning() => VerifyAsync(
        source: """
            class C
            {
                void M() { {|MY001:BadCode()|}; }
            }
            """,
        fixedSource: """
            class C
            {
                void M() { GoodCode(); }
            }
            """);
}
The test framework automatically runs the analyzer, triggers the code fix, and verifies the result matches fixedSource.

RefactoringTest

New in v1.14.0
Base class for testing code refactoring providers (lightbulb suggestions without diagnostics).

Basic Usage

using ANcpLua.Roslyn.Utilities.Testing;

public class MakeStaticRefactoringTests : RefactoringTest<MakeStaticLambdaRefactoring>
{
    [Fact]
    public Task ShouldMakeLambdaStatic() => VerifyAsync(
        source: """
            using System;
            class C
            {
                Func<int, int> f = [|x => x * 2|];
            }
            """,
        fixedSource: """
            using System;
            class C
            {
                Func<int, int> f = static x => x * 2;
            }
            """);
}
Use span markup [|code|] to mark the refactoring trigger location.

Verifying No Refactoring Offered

[Fact]
public Task ShouldNotOfferWhenAlreadyStatic() => VerifyNoRefactoringAsync("""
    using System;
    class C
    {
        Func<int, int> f = [|static x => x * 2|];
    }
    """);

[Fact]
public Task ShouldNotOfferWhenCapturing() => VerifyNoRefactoringAsync("""
    using System;
    class C
    {
        void M()
        {
            int captured = 5;
            Func<int, int> f = [|x => x + captured|];
        }
    }
    """);

GeneratorTest

Fluent API for testing Roslyn incremental source generators with built-in caching validation.

Basic Usage

using ANcpLua.Roslyn.Utilities.Testing;

public class MyGeneratorTests
{
    [Fact]
    public async Task ShouldGenerateExpectedOutput()
    {
        const string source = """
            [GenerateDto]
            public partial class User
            {
                public string Name { get; set; }
            }
            """;

        using var result = await Test<MyGenerator>.Run(source);
        result
            .Produces("User.g.cs", """
                public partial class User
                {
                    public UserDto ToDto() => new(Name);
                }
                """)
            .IsCached()
            .IsClean();
    }
}
The Test<TGenerator> static class automatically runs your generator twice to validate incremental caching behavior.

Custom Configuration

For complex scenarios, use the configuration overload:
[Fact]
public async Task ShouldHandleAdditionalText()
{
using var result = await Test < MyGenerator >.Run(engine => engine
    .WithSource("""
public partial class Config {
}
""")
    .WithAdditionalText("config.json", """{"setting": "value"}""")
.WithLanguageVersion(LanguageVersion.CSharp12)
    .WithStepTracking(true))

result.Produces("Config.g.cs").IsCached();
}

GeneratorTestEngine Methods

MethodDescription
WithSource(string)Add C# source code to the compilation
WithAdditionalText(path, text)Add an additional file (JSON, XML, etc.)
WithReference(MetadataReference)Add a metadata reference
WithLanguageVersion(version)Set C# language version (default: latest)
WithReferenceAssemblies(assemblies)Set reference assemblies package
WithAnalyzerConfigOptions(provider)Set MSBuild property access
WithStepTracking(bool)Enable/disable step tracking for caching analysis

GeneratorResult Assertions

The GeneratorResult class provides a fluent API for verifying generator output.

Output Verification

result
.Produces("Output.g.cs")                        // File exists
.Produces("Output.g.cs", expectedContent)       // Exact content match
.Produces("Output.g.cs", "partial class", exactMatch: false); // Contains

Compilation Verification

result
.IsClean()      // No diagnostics of any severity
.Compiles();    // No errors (warnings allowed)

Diagnostic Assertions

result
.HasDiagnostic("GEN001")                        // Diagnostic exists
.HasDiagnostic("GEN001", DiagnosticSeverity.Warning)  // With severity
.HasNoDiagnostic("GEN002");                     // Diagnostic absent

Custom File Assertions

result.File("Output.g.cs", content =>
{
Assert.Contains("public partial class", content);
Assert.DoesNotContain("private", content);
});
GeneratorResult implements IDisposable and calls Verify() on dispose. Use using var result = ... to automatically throw on assertion failures.

Caching Validation

Incremental generators must cache their outputs properly to avoid recomputation during IDE typing scenarios. The testing framework validates caching behavior automatically.

Basic Caching Check

result.IsCached();  // All pipeline steps must be cached

Checking Specific Steps

result.IsCached("TransformStep", "CombineStep");  // Only check named steps

Understanding the Caching Report

var report = result.CachingReport;

Console.WriteLine($"Generator: {report.GeneratorName}");
Console.WriteLine($"Produced output: {report.ProducedOutput}");

foreach (var step in report.ObservableSteps)
{
Console.WriteLine($"Step: {step.StepName}")
Console.WriteLine($"  Cached: {step.Cached}")
Console.WriteLine($"  Unchanged: {step.Unchanged}")
Console.WriteLine($"  Modified: {step.Modified}")
Console.WriteLine($"  New: {step.New}")
Console.WriteLine($"  Removed: {step.Removed}")
Console.WriteLine($"  Success: {step.IsCachedSuccessfully}")
}

Forbidden Types

Certain Roslyn types must never be cached because they prevent proper incremental caching:
Forbidden TypeReason
ISymbolNot value-equal across compilations
CompilationContains mutable state
SemanticModelTied to specific compilation
SyntaxNodeReference identity, not structural equality
SyntaxTreeReference identity, not structural equality
IOperationCompilation-specific
// Validates no forbidden types in any step
result.HasNoForbiddenTypes();

// IsCached() automatically checks for forbidden types
result.IsCached();  // Fails if ISymbol, Compilation, etc. are cached
Caching forbidden types causes memory leaks and IDE performance degradation. Extract primitive data (strings, booleans, etc.) from Roslyn types before caching.

Step Tracking

Enable step tracking to analyze which pipeline steps were executed and their caching states.

How Step Tracking Works

  1. The test engine runs your generator twice with identical input
  2. The first run populates the cache
  3. The second run reveals caching behavior

Step States

StateDescription
CachedOutput reused from previous run (optimal)
UnchangedRecomputed but produced same value (equality worked)
ModifiedValue changed between runs (cache miss)
NewAdditional item produced
RemovedItem no longer produced
A successfully cached step produces only Cached or Unchanged outputs.

Named Step Tracking

Use WithTrackingName in your generator to enable fine-grained analysis:
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
    .CreateSyntaxProvider(...)
    .WithTrackingName("ParseStep");  // Named for testing

var combined = provider
    .Combine(context.CompilationProvider)
    .WithTrackingName("CombineStep");

context.RegisterSourceOutput(combined, ...);
}
Then verify specific steps:
result.IsCached("ParseStep", "CombineStep");

Method Reference

AnalyzerTest<TAnalyzer>

MethodDescription
VerifyAsync(source)Verify analyzer produces expected diagnostics marked in source
VerifyAsync(source, additionalFiles, expectedDiagnostics?)Verify with additional files and explicit diagnostics

CodeFixTest<TAnalyzer, TCodeFix>

MethodDescription
VerifyAsync(source, fixedSource)Verify code fix transforms source to expected output

RefactoringTest<TRefactoring>

MethodDescription
VerifyAsync(source, fixedSource)Verify refactoring transforms source to expected output
VerifyNoRefactoringAsync(source)Verify no refactoring is offered at the marked span

Test<TGenerator>

MethodDescription
Run(source)Run generator with source and return result
Run(configure)Run generator with custom configuration

GeneratorResult

MethodDescription
Produces(hintName)Assert file exists
Produces(hintName, content)Assert file exists with exact content
Produces(hintName, content, false)Assert file contains content
IsClean()Assert no diagnostics
Compiles()Assert no errors (warnings allowed)
IsCached()Assert all steps are cached
IsCached(stepNames)Assert specific steps are cached
HasDiagnostic(id)Assert diagnostic exists
HasDiagnostic(id, severity)Assert diagnostic exists with severity
HasNoDiagnostic(id)Assert diagnostic does not exist
HasNoForbiddenTypes()Assert no ISymbol/Compilation/etc. cached
File(hintName, action)Execute custom assertion on file content

GeneratorCachingReport

PropertyDescription
GeneratorNameName of the generator being tested
ProducedOutputWhether generator produced any files
ObservableStepsList of user-defined pipeline steps
ForbiddenTypeViolationsList of cached forbidden types detected

GeneratorStepAnalysis

PropertyDescription
StepNameName of the pipeline step
CachedCount of cached outputs
UnchangedCount of unchanged outputs
ModifiedCount of modified outputs
NewCount of new outputs
RemovedCount of removed outputs
HasForbiddenTypesWhether step has forbidden type violations
IsCachedSuccessfullyTrue if no Modified/New/Removed outputs

Test Project Patterns

Minimal Generator Test Project

A clean generator test project requires only essential packages:
<Project Sdk="ANcpLua.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <OutputType>Exe</OutputType>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="xunit.v3.mtp-v2"/>
        <PackageReference Include="AwesomeAssertions"/>
        <PackageReference Include="Verify.XunitV3"/>
        <PackageReference Include="Verify.SourceGenerators"/>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
        <PackageReference Include="ANcpLua.Roslyn.Utilities" GeneratePathProperty="true" PrivateAssets="all"/>
        <PackageReference Include="ANcpLua.Roslyn.Utilities.Testing"/>
    </ItemGroup>
    <ItemGroup>
        <ProjectReference Include="../MyGenerator/MyGenerator.csproj" OutputItemType="Analyzer"/>
    </ItemGroup>
    <ItemGroup>
        <Analyzer Include="$(PkgANcpLua_Roslyn_Utilities)/lib/netstandard2.0/ANcpLua.Roslyn.Utilities.dll"/>
    </ItemGroup>
</Project>
The Analyzer item loads ANcpLua.Roslyn.Utilities as an analyzer dependency alongside your generator.

Shared Test Base Class

Create a base class to centralize test infrastructure:
using ANcpLua.Roslyn.Utilities.Testing;
using Microsoft.AspNetCore.Http;

namespace MyGenerator.Tests;

public abstract class GeneratorTestBase
{
protected static readonly Type[] RequiredTypes =
    [
        typeof (HttpContext),
        // Add types your generator needs
    ];

protected static async Task < GeneratorResult > RunAsync(string source)
{
    using
    var scope = TestConfiguration.WithAdditionalReferences(RequiredTypes);
    return await Test < MyGenerator >
.
    Run(source, TestContext.Current.CancellationToken);
}

protected static async Task VerifyAsync(string source)
{
    using
    var result = await RunAsync(source);


    await Verify(new
    {
        GeneratedSources = result.Files
            .Select(static f
=>
    new {f.HintName, Source = f.Content}
)
.
    OrderBy(static
    s => s.HintName
),
    Diagnostics = result.Diagnostics
        .Select(static
    d => new {d.Id, Severity = d.Severity.ToString(), Message = d.GetMessage()}
)
.
    OrderBy(static
    d => d.Id
)
}).
    UseDirectory("Snapshots");
}
}

Verify Module Initializer

Configure Verify for Roslyn types:
using System.Runtime.CompilerServices;

namespace MyGenerator.Tests;

public static class ModuleInitializer
{
[ModuleInitializer]
public static void Initialize()
{
    VerifySourceGenerators.Initialize();
    VerifierSettings.AddExtraSettings(static
    settings =>
        settings.Converters.Add(new EquatableArrayJsonConverter())
)
}
}

file sealed class EquatableArrayJsonConverter : WriteOnlyJsonConverter
{
public override void Write(VerifyJsonWriter writer, object value)
{
    var type = value.GetType();
    if (!type.IsGenericType || type.GetGenericTypeDefinition().Name != "EquatableArray`1") {
        writer.Serialize(value);
        return;
    }


    var itemsProperty = type.GetProperty("Items");
    var items = itemsProperty?.GetValue(value);
    if (items is
    null
)
    {
        writer.WriteStartArray();
        writer.WriteEndArray();
        return;
    }


    var toArrayMethod = items.GetType().GetMethod("ToArray");
    writer.Serialize(toArrayMethod?.Invoke(items, null) ?? Array.Empty < object > ());
}

public override bool CanConvert(Type type) =>
type.IsGenericType && type.GetGenericTypeDefinition().Name == "EquatableArray`1";
}

Test File Organization

tests/MyGenerator.Tests/
├── ModuleInitializer.cs      # Verify configuration
├── GeneratorTestBase.cs      # Shared infrastructure
├── GeneratorOutputTests.cs   # Output verification
├── AnalyzerDiagnosticTests.cs
└── Snapshots/                # Verify snapshot files
└── *.verified.txt
Keep tests focused: one test class per concern. Avoid duplicate test infrastructure across files.

Dynamic Compilation

The Compile class provides a fluent API for dynamic compilation in tests, useful for testing code that needs to compile and execute user-provided source code.

Basic Usage

using ANcpLua.Roslyn.Utilities.Testing;

// Compile and assert success
Compile.Source("""
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
    }
    """)
    .WithCommonReferences()
    .Build()
    .ShouldSucceed();

// Get assembly directly (throws on failure)
var assembly = Compile.Source(code).BuildOrThrow();

// Create instance from compiled code
var calc = Compile.Source(code)
    .Build()
    .ShouldSucceed()
    .CreateInstance<ICalculator>("Calculator");

Compile Builder Methods

MethodDescription
Source(code)Create compiler with source code
Empty()Create empty compiler for configuration
WithSource(code)Add additional source code
WithSources(codes)Add multiple source files
WithReference<T>()Add reference from type’s assembly
WithReference(assembly)Add reference from assembly
WithReference(path)Add reference from file path
WithReferences(types)Add references from multiple types
WithCommonReferences()Add Console, Linq, Collections references
WithAssemblyName(name)Set output assembly name
WithOutputKind(kind)Set output kind
AsExecutable()Configure as console application
WithLanguageVersion(v)Set C# language version
WithOptimization()Enable release mode
WithUnsafe()Allow unsafe code
Build()Build and return CompileResult
BuildOrThrow()Build and return assembly (throws on fail)

CompileResult Properties

PropertyDescription
SucceededWhether compilation succeeded
FailedWhether compilation failed
AssemblyLoaded assembly if succeeded
CompilationCSharpCompilation for inspection
DiagnosticsAll diagnostics
ErrorsError diagnostics only
WarningsWarning diagnostics only

CompileResult Query Methods

MethodDescription
HasError(errorId)Check if error with ID exists
HasWarning(warningId)Check if warning with ID exists
ContainsType(typeName)Check if type exists in assembly
GetType(name)Get type from assembly
GetRequiredType(name)Get type or throw
CreateInstance(typeName)Create instance of type
CreateInstance<T>(typeName)Create instance cast to T
CreateRequiredInstance<T>(t)Create instance or throw
FormatDiagnostics()Format diagnostics for display
GetSourceText()Get combined source text
GetSemanticModel()Get semantic model for 1st tree

CompileResult Assertions

result
    .ShouldSucceed()           // Assert compilation succeeded
    .ShouldHaveNoWarnings()    // Assert no warnings
    .ShouldContainType("MyNamespace.MyClass");

result.ShouldFail();           // Assert compilation failed
result.ShouldHaveError("CS0246");
result.ShouldHaveWarning("CS8618");
result.ShouldHaveErrors(3);    // At least 3 errors

Log Testing

The LogAssert class provides fluent assertions for FakeLogCollector from Microsoft.Extensions.Logging.Testing.

Basic Usage

using ANcpLua.Roslyn.Utilities.Testing;
using Microsoft.Extensions.Logging.Testing;

var collector = new FakeLogCollector();
var logger = new FakeLogger(collector);

// ... code that logs ...

collector
    .ShouldHaveCount(3)
    .ShouldContain("started")
    .ShouldHaveNoErrors();

Count Assertions

MethodDescription
ShouldHaveCount(n)At least N entries
ShouldHaveExactCount(n)Exactly N entries
ShouldBeEmpty()No entries

Content Assertions

MethodDescription
ShouldContain(text, comparison?)Contains text
ShouldNotContain(text)Does not contain text
ShouldMatch(pattern)Matches regex pattern

Level Assertions

MethodDescription
ShouldHaveLevel(level)Has log at level
ShouldNotHaveLevel(l)No log at level
ShouldHaveNoErrors()No Error level logs
ShouldHaveNoWarnings()No Warning level logs
ShouldBeClean()No errors or warnings

Combined Assertions

collector.ShouldHave(LogLevel.Error, "connection failed");

Predicate Assertions

collector.ShouldHaveAny(r => r.Exception is not null, "Expected exception log");
collector.ShouldHaveAll(r => r.Level >= LogLevel.Information);
collector.ShouldHaveNone(r => r.Message?.Contains("secret") == true);

Async Waiting

For testing async logging scenarios, use polling assertions:
// Wait for log containing text (5s default timeout)
await collector.ShouldEventuallyContain("operation completed");

// Wait for count
await collector.ShouldEventuallyHaveCount(5);

// Wait for log level
await collector.ShouldEventuallyHaveLevel(LogLevel.Error);

// Custom condition
await collector.ShouldEventuallySatisfy(
    logs => logs.Count(r => r.Level == LogLevel.Warning) >= 2,
    because: "Expected at least 2 warnings",
    timeout: TimeSpan.FromSeconds(10));
Async assertions poll every 25ms and timeout after 5 seconds by default. Pass a custom timeout parameter for longer operations.

Web Testing

ASP.NET Core integration test base classes with fake logging support.

IntegrationTestBase

Fast integration test base using in-memory TestServer for API tests that don’t require real network I/O.
using ANcpLua.Roslyn.Utilities.Testing.WebTesting;

public class MyApiTests : IntegrationTestBase<Program>
{
    public MyApiTests(WebApplicationFactory<Program> factory) : base(factory) { }

    [Fact]
    public async Task GetEndpoint_ReturnsOk()
    {
        var response = await Client.GetAsync("/api/items");
        response.EnsureSuccessStatusCode();

        // Assert on fake logs
        Logs.ShouldContain("Request started");
    }

    protected override void ConfigureTestServices(IServiceCollection services)
    {
        base.ConfigureTestServices(services);  // Adds fake logging
        services.AddSingleton<IMyService, FakeMyService>();
    }
}

KestrelTestBase

Real Kestrel test base for HTTP/2, WebSockets, SSE, and Playwright testing with real network I/O.
using ANcpLua.Roslyn.Utilities.Testing.WebTesting;

public class WebSocketTests : KestrelTestBase<Program>
{
    public WebSocketTests(WebApplicationFactory<Program> factory) : base(factory) { }

    [Fact]
    public async Task WebSocket_Connects()
    {
        using var ws = new ClientWebSocket();
        await ws.ConnectAsync(new Uri(BaseAddress, "/ws"), CancellationToken.None);
        Assert.Equal(WebSocketState.Open, ws.State);
    }
}
PropertyDescription
ClientHttpClient configured for the test server
LogsFakeLogCollector for asserting log output
FactoryConfigured WebApplicationFactory
BaseAddressServer base address (KestrelTestBase only)

Analyzer Infrastructure

Base classes for building Roslyn analyzers and code fixes with common patterns.

DiagnosticAnalyzerBase

Base class for diagnostic analyzers with concurrent execution and registration helpers.
using ANcpLua.Roslyn.Utilities.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MyAnalyzer : DiagnosticAnalyzerBase
{
    public static readonly DiagnosticDescriptor Rule = new(
        "MY001", "Title", "Message", "Category",
        DiagnosticSeverity.Warning, isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

    protected override void RegisterAnalysis(AnalysisContext context)
    {
        context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
    }

    private void AnalyzeSymbol(SymbolAnalysisContext context) { /* ... */ }
}

CodeFixProviderBase

Base class for code fix providers with batch fixing and equivalence key support.
using ANcpLua.Roslyn.Utilities.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp)]
public class MyCodeFix : CodeFixProviderBase<SyntaxNode>
{
    public override ImmutableArray<string> FixableDiagnosticIds => ["MY001"];

    protected override string Title => "Fix the issue";
    protected override string EquivalenceKey => "MyCodeFix";

    protected override async Task<SyntaxNode?> GetTargetNodeAsync(
        Document document, TextSpan span, CancellationToken ct)
    {
        var root = await document.GetSyntaxRootAsync(ct);
        return root?.FindNode(span);
    }

    protected override async Task<Document> FixAsync(
        Document document, SyntaxNode node, CancellationToken ct)
    {
        var root = await document.GetSyntaxRootAsync(ct);
        var newNode = node.WithLeadingTrivia(/* ... */);
        return document.WithSyntaxRoot(root!.ReplaceNode(node, newNode));
    }
}

SyntaxModifierExtensions

Helpers for syntax transformations in code fixes.
// Add modifier
var newDecl = methodDecl.AddModifier(SyntaxKind.AsyncKeyword);

// Remove modifier
var newDecl = methodDecl.RemoveModifier(SyntaxKind.PublicKeyword);

// Replace modifier
var newDecl = methodDecl.ReplaceModifier(
    SyntaxKind.PrivateKeyword,
    SyntaxKind.PublicKeyword);

Integration Testing

For testing that requires real MSBuild execution (SDK features, package behavior, build-time code generation), see MSBuild Testing.

Handling Type Ambiguity

If you encounter ambiguous reference errors between Microsoft.CodeAnalysis.Testing.AnalyzerTest and ANcpLua.Roslyn.Utilities.Testing.AnalyzerTest, use a type alias:
using AnalyzerTestBase = ANcpLua.Roslyn.Utilities.Testing.AnalyzerTest<MyAnalyzer>;

    public class MyTests : AnalyzerTestBase
    {
        // ...
    }
    ```
    ````