> ## Documentation Index
> Fetch the complete documentation index at: https://ancplua.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing Infrastructure

> Testing frameworks for analyzers, code fixes, refactorings, and source generators

`ANcpLua.Roslyn.Utilities.Testing` provides fluent base classes for testing Roslyn components:

| Base Class                         | Purpose                            |
| ---------------------------------- | ---------------------------------- |
| `AnalyzerTest<TAnalyzer>`          | Test diagnostic analyzers          |
| `CodeFixTest<TAnalyzer, TCodeFix>` | Test code fix providers            |
| `RefactoringTest<TRefactoring>`    | Test code refactoring providers    |
| `Test<TGenerator>`                 | Test incremental source generators |

<Note>
  These base classes work with xUnit v3's ambient `TestContext`. Use
  `TestContext.Current.TestOutputHelper` for diagnostic output without
  constructor injection.
</Note>

## Installation

```xml theme={null}
<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

```csharp theme={null}
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(); }
        }
        """);
}
```

<Note>
  Use diagnostic markup `{|DIAGNOSTIC_ID:code|}` to mark expected diagnostic locations.
</Note>

### Testing with Additional Files

For analyzers that inspect non-C# files (MSBuild props, JSON configs, etc.):

```csharp theme={null}
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

```csharp theme={null}
// 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.

```csharp theme={null}
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(); }
            }
            """);
}
```

<Tip>
  The test framework automatically runs the analyzer, triggers the code fix, and
  verifies the result matches `fixedSource`.
</Tip>

## RefactoringTest

<Note>New in v1.14.0</Note>

Base class for testing code refactoring providers (lightbulb suggestions without diagnostics).

### Basic Usage

```csharp theme={null}
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;
            }
            """);
}
```

<Note>
  Use span markup `[|code|]` to mark the refactoring trigger location.
</Note>

### Verifying No Refactoring Offered

```csharp theme={null}
[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

```csharp theme={null}
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();
    }
}
```

<Note>
  The `Test<TGenerator>` static class automatically runs your generator twice to validate incremental caching
  behavior.
</Note>

### Custom Configuration

For complex scenarios, use the configuration overload:

```csharp theme={null}
[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

| Method                                | Description                                       |
| ------------------------------------- | ------------------------------------------------- |
| `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

```csharp theme={null}
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

```csharp theme={null}
result
.IsClean()      // No diagnostics of any severity
.Compiles();    // No errors (warnings allowed)
```

### Diagnostic Assertions

```csharp theme={null}
result
.HasDiagnostic("GEN001")                        // Diagnostic exists
.HasDiagnostic("GEN001", DiagnosticSeverity.Warning)  // With severity
.HasNoDiagnostic("GEN002");                     // Diagnostic absent
```

### Custom File Assertions

```csharp theme={null}
result.File("Output.g.cs", content =>
{
Assert.Contains("public partial class", content);
Assert.DoesNotContain("private", content);
});
```

<Tip>
  `GeneratorResult` implements `IDisposable` and calls `Verify()` on dispose.
  Use `using var result = ...` to automatically throw on assertion failures.
</Tip>

## 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

```csharp theme={null}
result.IsCached();  // All pipeline steps must be cached
```

### Checking Specific Steps

```csharp theme={null}
result.IsCached("TransformStep", "CombineStep");  // Only check named steps
```

### Understanding the Caching Report

```csharp theme={null}
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 Type  | Reason                                      |
| --------------- | ------------------------------------------- |
| `ISymbol`       | Not value-equal across compilations         |
| `Compilation`   | Contains mutable state                      |
| `SemanticModel` | Tied to specific compilation                |
| `SyntaxNode`    | Reference identity, not structural equality |
| `SyntaxTree`    | Reference identity, not structural equality |
| `IOperation`    | Compilation-specific                        |

```csharp theme={null}
// Validates no forbidden types in any step
result.HasNoForbiddenTypes();

// IsCached() automatically checks for forbidden types
result.IsCached();  // Fails if ISymbol, Compilation, etc. are cached
```

<Warning>
  Caching forbidden types causes memory leaks and IDE performance degradation.
  Extract primitive data (strings, booleans, etc.) from Roslyn types before
  caching.
</Warning>

## 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

| State       | Description                                          |
| ----------- | ---------------------------------------------------- |
| `Cached`    | Output reused from previous run (optimal)            |
| `Unchanged` | Recomputed but produced same value (equality worked) |
| `Modified`  | Value changed between runs (cache miss)              |
| `New`       | Additional item produced                             |
| `Removed`   | Item 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:

```csharp theme={null}
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:

```csharp theme={null}
result.IsCached("ParseStep", "CombineStep");
```

## Method Reference

### AnalyzerTest\<TAnalyzer>

| Method                                                       | Description                                                    |
| ------------------------------------------------------------ | -------------------------------------------------------------- |
| `VerifyAsync(source)`                                        | Verify analyzer produces expected diagnostics marked in source |
| `VerifyAsync(source, additionalFiles, expectedDiagnostics?)` | Verify with additional files and explicit diagnostics          |

### CodeFixTest\<TAnalyzer, TCodeFix>

| Method                             | Description                                          |
| ---------------------------------- | ---------------------------------------------------- |
| `VerifyAsync(source, fixedSource)` | Verify code fix transforms source to expected output |

### RefactoringTest\<TRefactoring>

| Method                             | Description                                             |
| ---------------------------------- | ------------------------------------------------------- |
| `VerifyAsync(source, fixedSource)` | Verify refactoring transforms source to expected output |
| `VerifyNoRefactoringAsync(source)` | Verify no refactoring is offered at the marked span     |

### Test\<TGenerator>

| Method           | Description                                 |
| ---------------- | ------------------------------------------- |
| `Run(source)`    | Run generator with source and return result |
| `Run(configure)` | Run generator with custom configuration     |

### GeneratorResult

| Method                               | Description                               |
| ------------------------------------ | ----------------------------------------- |
| `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

| Property                  | Description                             |
| ------------------------- | --------------------------------------- |
| `GeneratorName`           | Name of the generator being tested      |
| `ProducedOutput`          | Whether generator produced any files    |
| `ObservableSteps`         | List of user-defined pipeline steps     |
| `ForbiddenTypeViolations` | List of cached forbidden types detected |

### GeneratorStepAnalysis

| Property               | Description                                |
| ---------------------- | ------------------------------------------ |
| `StepName`             | Name of the pipeline step                  |
| `Cached`               | Count of cached outputs                    |
| `Unchanged`            | Count of unchanged outputs                 |
| `Modified`             | Count of modified outputs                  |
| `New`                  | Count of new outputs                       |
| `Removed`              | Count of removed outputs                   |
| `HasForbiddenTypes`    | Whether step has forbidden type violations |
| `IsCachedSuccessfully` | True if no Modified/New/Removed outputs    |

## Test Project Patterns

### Minimal Generator Test Project

A clean generator test project requires only essential packages:

```xml theme={null}
<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>
```

<Note>
  The `Analyzer` item loads ANcpLua.Roslyn.Utilities as an analyzer dependency
  alongside your generator.
</Note>

### Shared Test Base Class

Create a base class to centralize test infrastructure:

```csharp theme={null}
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:

```csharp theme={null}
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
```

<Tip>
  Keep tests focused: one test class per concern. Avoid duplicate test
  infrastructure across files.
</Tip>

## 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

```csharp theme={null}
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

| Method                    | Description                                |
| ------------------------- | ------------------------------------------ |
| `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

| Property      | Description                      |
| ------------- | -------------------------------- |
| `Succeeded`   | Whether compilation succeeded    |
| `Failed`      | Whether compilation failed       |
| `Assembly`    | Loaded assembly if succeeded     |
| `Compilation` | CSharpCompilation for inspection |
| `Diagnostics` | All diagnostics                  |
| `Errors`      | Error diagnostics only           |
| `Warnings`    | Warning diagnostics only         |

### CompileResult Query Methods

| Method                         | Description                      |
| ------------------------------ | -------------------------------- |
| `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

```csharp theme={null}
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

```csharp theme={null}
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

| Method                    | Description        |
| ------------------------- | ------------------ |
| `ShouldHaveCount(n)`      | At least N entries |
| `ShouldHaveExactCount(n)` | Exactly N entries  |
| `ShouldBeEmpty()`         | No entries         |

### Content Assertions

| Method                             | Description           |
| ---------------------------------- | --------------------- |
| `ShouldContain(text, comparison?)` | Contains text         |
| `ShouldNotContain(text)`           | Does not contain text |
| `ShouldMatch(pattern)`             | Matches regex pattern |

### Level Assertions

| Method                   | Description           |
| ------------------------ | --------------------- |
| `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

```csharp theme={null}
collector.ShouldHave(LogLevel.Error, "connection failed");
```

### Predicate Assertions

```csharp theme={null}
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:

```csharp theme={null}
// 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));
```

<Tip>
  Async assertions poll every 25ms and timeout after 5 seconds by default. Pass
  a custom `timeout` parameter for longer operations.
</Tip>

## Web Testing

ASP.NET Core integration test base classes with fake logging support. Available for **xUnit**, **NUnit**, **TUnit**, and **bUnit**.

### IntegrationTestBase (xUnit)

Fast integration test base using in-memory `TestServer` for API tests that don't require real network I/O.

```csharp theme={null}
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>();
    }
}
```

### IntegrationTestBase (NUnit)

```csharp theme={null}
using ANcpLua.Roslyn.Utilities.Testing.WebTesting.NUnit;

public class MyApiTests : IntegrationTestBase<Program>
{
    [Test]
    public async Task GetEndpoint_ReturnsOk()
    {
        var response = await Client.GetAsync("/api/items");
        response.EnsureSuccessStatusCode();
    }
}
```

<Note>
  NUnit version uses `[OneTimeSetUp]` for factory creation (shared across tests)
  and `[SetUp]`/`[TearDown]` for per-test client lifecycle.
</Note>

### IntegrationTestBase (TUnit)

```csharp theme={null}
using ANcpLua.Roslyn.Utilities.Testing.WebTesting.TUnit;

public class MyApiTests : IntegrationTestBase<Program>
{
    [Test]
    public async Task GetEndpoint_ReturnsOk()
    {
        var response = await Client.GetAsync("/api/items");
        response.EnsureSuccessStatusCode();
    }
}
```

<Note>
  TUnit version uses `[Before(HookType.Test)]` / `[After(HookType.Test)]` hooks.
  A fresh factory and client are created per test.
</Note>

### KestrelTestBase

Real Kestrel test base for HTTP/2, WebSockets, SSE, and Playwright testing with real network I/O. Available for xUnit, NUnit, and TUnit.

```csharp theme={null}
// xUnit
using ANcpLua.Roslyn.Utilities.Testing.WebTesting;

// NUnit
using ANcpLua.Roslyn.Utilities.Testing.WebTesting.NUnit;

// TUnit
using ANcpLua.Roslyn.Utilities.Testing.WebTesting.TUnit;

public class WebSocketTests : KestrelTestBase<Program>
{
    // xUnit only: constructor injection
    public WebSocketTests(WebApplicationFactory<Program> factory) : base(factory) { }

    [Fact] // or [Test] for NUnit/TUnit
    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);
    }
}
```

### BunitTestBase

Base class for bUnit Blazor component tests with integrated fake logging.

```csharp theme={null}
using ANcpLua.Roslyn.Utilities.Testing.WebTesting.Bunit;

public class MyComponentTests : BunitTestBase
{
    [Fact]
    public void Counter_Increments()
    {
        var cut = Render<Counter>();
        cut.Find("button").Click();
        cut.Find("p").MarkupMatches("<p>Count: 1</p>");

        // Assert on fake logs
        Logs.ShouldContain("Counter incremented");
    }
}
```

<Note>
  `BunitTestBase` extends `BunitContext` and pre-configures `FakeLogCollector`.
  Works with any test framework (xUnit, NUnit, TUnit).
</Note>

### Web Testing Properties

| Property      | Description                                |
| ------------- | ------------------------------------------ |
| `Client`      | HttpClient configured for the test server  |
| `Logs`        | FakeLogCollector for asserting log output  |
| `Factory`     | Configured WebApplicationFactory           |
| `BaseAddress` | Server base address (KestrelTestBase only) |

### Web Testing Namespaces

| Framework | Namespace                                           |
| --------- | --------------------------------------------------- |
| xUnit     | `ANcpLua.Roslyn.Utilities.Testing.WebTesting`       |
| NUnit     | `ANcpLua.Roslyn.Utilities.Testing.WebTesting.NUnit` |
| TUnit     | `ANcpLua.Roslyn.Utilities.Testing.WebTesting.TUnit` |
| bUnit     | `ANcpLua.Roslyn.Utilities.Testing.WebTesting.Bunit` |

## 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.

```csharp theme={null}
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.

```csharp theme={null}
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.

```csharp theme={null}
// 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](/utilities/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:

`````csharp theme={null}
using AnalyzerTestBase = ANcpLua.Roslyn.Utilities.Testing.AnalyzerTest<MyAnalyzer>;

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