Skip to main content
The Test SDK (ANcpLua.NET.Sdk.Test) provides xUnit v3 with Microsoft Testing Platform and injects base classes for integration testing.

Microsoft Testing Platform (MTP)

The SDK uses xUnit v3 with Microsoft Testing Platform instead of VSTest.

Auto-Configuration

MTP requires test projects to output executables. The SDK handles this automatically when you reference xunit.v3.mtp-v2:
<!-- SDK auto-detects this and sets OutputType=Exe -->
<PackageReference Include="xunit.v3.mtp-v2" />

CLI Syntax

MTP uses different command syntax than VSTest:
# Run all tests
dotnet test

# Run with filter (no -- separator needed)
dotnet test --filter-method "*MyTest*"

# List tests
dotnet test --list-tests

# Generate TRX report
dotnet test --report-trx
VSTest syntax causes exit code 5 with MTP. Use --filter-method instead of --filter "FQN~" and --report-trx instead of --logger trx.
.NET 10 includes native MTP support via global.json. The -- separator is only needed for .NET 8/9 with explicit MTP opt-in.

Troubleshooting

Exit CodeCauseFix
5Unknown CLI optionUse MTP syntax (--filter-method), not VSTest (--filter "FQN~")
8Zero tests discoveredCheck filter pattern; ensure test methods are public
N/ATests not foundVerify xunit.v3.mtp-v2 reference; SDK sets OutputType=Exe

xUnit v3 TestContext

xUnit v3 replaces constructor-injected ITestOutputHelper with ambient TestContext. Access test output anywhere without passing it through constructor chains.

Before vs After

AspectxUnit v2 (Before)xUnit v3 (After)
Test constructor paramsfixture, testOutputHelperfixture
Fields to store21
Builder constructor params43
Boilerplate per test class~3 lines0
How output is accessedPassed through chainAmbient TestContext.Current

Migration Example

public class MyApiTests : IntegrationTestBase<Program>
{
    private readonly ITestOutputHelper _output;
    private readonly MyFixture _fixture;

    public MyApiTests(MyFixture fixture, ITestOutputHelper output)
    {
        _fixture = fixture;
        _output = output;
    }

    [Fact]
    public async Task Get_ReturnsSuccess()
    {
        _output.WriteLine("Starting test...");
        var response = await Client.GetAsync("/api/items");
        response.EnsureSuccessStatusCode();
    }

}

TestContext.Current is available anywhere during test execution - in test methods, helper classes, or builder patterns - without explicit parameter passing.

IntegrationTestBase

In-memory TestServer for fast API testing.
public class MyApiTests : IntegrationTestBase<Program>
{
    [Fact]
    public async Task Get_ReturnsSuccess()
    {
        var response = await Client.GetAsync("/api/items");
        response.EnsureSuccessStatusCode();
    }
}
Uses WebApplicationFactory<TProgram> internally.

KestrelTestBase

Real Kestrel server for HTTP/2, WebSockets, SSE, or Playwright.
public class WebSocketTests : KestrelTestBase<Program>
{
    [Fact]
    public async Task WebSocket_ConnectsSuccessfully()
    {
        var ws = new ClientWebSocket();
        await ws.ConnectAsync(new Uri($"{BaseAddress}/ws"), CancellationToken.None);
    }
}
Starts on random port via UseKestrel(0).

FakeLogger

See Extensions for FakeLogCollector test helpers.

AOT/Trim Testing

The SDK provides infrastructure for testing Native AOT and trimmed applications. Tests run as separate processes with PublishAot=true or PublishTrimmed=true to verify runtime behavior.

Enabling AOT Testing

<Project Sdk="ANcpLua.NET.Sdk.Test">
  <PropertyGroup>
    <EnableAotTesting>true</EnableAotTesting>
  </PropertyGroup>
</Project>
This automatically references ANcpLua.AotTesting.Attributes.

Writing AOT Tests

AOT tests are methods returning int with exit code 100 for success:
using ANcpLua.AotTesting;

public class MyAotTests
{
    [AotTest]
    public static int BasicAotTest()
    {
        var list = new List<int> { 1, 2, 3 };
        if (list.Sum() != 6)
        {
            Console.Error.WriteLine("FAIL: Sum incorrect");
            return 1;
        }
        return 100; // Success
    }

    [TrimTest(TrimMode = TrimMode.Full)]
    public static int VerifyTrimmingWorks()
    {
        // Test trimming behavior
        return 100;
    }
}

Test Attributes

Publishes with PublishAot=true and executes the native binary.
[AotTest(
    SkipOnPlatform = "osx",           // Skip on macOS
    RuntimeIdentifier = "win-x64",     // Override RID
    DisabledFeatureSwitches = new[] { FeatureSwitches.JsonReflection },
    Configuration = "Release",         // Default: Release
    TimeoutSeconds = 300               // Default: 5 minutes
)]
public static int MyAotTest() => 100;
Publishes with PublishTrimmed=true for testing IL trimming.
[TrimTest(
    TrimMode = TrimMode.Full,    // Full or Partial
    SkipOnPlatform = "linux",
    TimeoutSeconds = 180
)]
public static int MyTrimTest() => 100;
In-process markers verified by analyzers (AL0043, AL0044):
[TrimSafe]  // Analyzer verifies no RequiresUnreferencedCode calls
public void ProcessData<T>(T data) { }

[AotSafe]   // Analyzer verifies no RequiresDynamicCode calls
public void CreateService() { }
Marks code that intentionally uses AOT-incompatible patterns. AL0052 prevents [AotSafe] code from calling [AotUnsafe] code, and AL0053 flags unnecessary [AotUnsafe] attributes.
[AotUnsafe("Uses reflection for plugin loading")]
public void LoadPlugin(string typeName) { }

[AotSafe]
public void Process()
{
    // AL0052: [AotSafe] code must not call [AotUnsafe] code
    // LoadPlugin("MyPlugin");
}

TrimAssert Helpers

Verify types are trimmed or preserved at runtime:
[TrimTest(TrimMode = TrimMode.Full)]
public static int VerifyUnusedTypeTrimmed()
{
    // Assert type was trimmed away
    TrimAssert.TypeTrimmed(
        "MyNamespace.UnusedService",
        "MyAssembly");

    // Assert essential type survives
    TrimAssert.TypePreserved(
        "System.Collections.Generic.List`1",
        "System.Private.CoreLib");

    return 100;
}
Do not use typeof(X) in assertions—it roots the type and prevents trimming. Use string-based TrimAssert methods instead.

Feature Switches

Disable runtime features during tests:
[AotTest(DisabledFeatureSwitches = new[] {
    FeatureSwitches.JsonReflection,
    FeatureSwitches.EventSourceSupport
})]
public static int TestWithoutReflection() => 100;
ConstantFeature Switch
JsonReflectionSystem.Text.Json.JsonSerializer.IsReflectionEnabledByDefault
DebuggerSupportSystem.Diagnostics.Debugger.IsSupported
EventSourceSupportSystem.Diagnostics.Tracing.EventSource.IsSupported
MetricsSupportSystem.Diagnostics.Metrics.Meter.IsSupported
XmlSerializationSystem.Xml.XmlSerializer.IsEnabled
BinaryFormatterSystem.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization
InvariantGlobalizationSystem.Globalization.Invariant
HttpActivityPropagationSystem.Net.Http.EnableActivityPropagation
StartupHooksSystem.StartupHookProvider.IsSupported

How It Works

  1. Discovery: MSBuild target scans for [AotTest]/[TrimTest] methods
  2. Project Generation: Creates temporary .csproj for each test
  3. Publish: Runs dotnet publish with AOT/Trim flags
  4. Execute: Runs the published executable
  5. Validate: Checks exit code (100 = success)

Exit Code Convention

CodeMeaning
100Success
1-99Test failure
-1TrimAssert.TypeTrimmed failed (type exists)
-2TrimAssert.TypePreserved failed (type missing)
RuleDescription
AL0041[AotTest]/[TrimTest] must return int
AL0042Should return 100 for success
AL0043[TrimSafe] cannot call [RequiresUnreferencedCode]
AL0044[AotSafe] cannot call [RequiresDynamicCode]
AL0052[AotSafe] must not call [AotUnsafe]
AL0053Unnecessary [AotUnsafe] attribute
AL0094Avoid dynamic keyword in AOT
AL0095Avoid Expression.Compile() in AOT
AL0101Avoid Activator.CreateInstance in AOT
AL0102Avoid Type.GetType with dynamic name