Skip to content

Comments

.NET: Upgrade to XUnit 3 and Microsoft Testing Platform#4176

Merged
westey-m merged 13 commits intomicrosoft:feature-xunit3-mtp-upgradefrom
westey-m:xunit3-mtp-upgrade
Feb 24, 2026
Merged

.NET: Upgrade to XUnit 3 and Microsoft Testing Platform#4176
westey-m merged 13 commits intomicrosoft:feature-xunit3-mtp-upgradefrom
westey-m:xunit3-mtp-upgrade

Conversation

@westey-m
Copy link
Contributor

Motivation and Context

Our integration tests are running too slowly, but with XUnit 3 and MTP we have more options for running tests in parallel.

Description

  • Upgrade to XUnit 3 and Microsoft Testing Platform

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

@markwallace-microsoft markwallace-microsoft added documentation Improvements or additions to documentation .NET labels Feb 23, 2026
@github-actions github-actions bot changed the title Upgrade to XUnit 3 and Microsoft Testing Platform .NET: Upgrade to XUnit 3 and Microsoft Testing Platform Feb 23, 2026
@westey-m westey-m changed the base branch from main to feature-xunit3-mtp-upgrade February 24, 2026 11:05
@westey-m westey-m marked this pull request as ready for review February 24, 2026 11:25
Copilot AI review requested due to automatic review settings February 24, 2026 11:25
@westey-m westey-m merged commit 6c3b9d4 into microsoft:feature-xunit3-mtp-upgrade Feb 24, 2026
20 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades the .NET test infrastructure from XUnit 2 to XUnit 3 and migrates to the Microsoft Testing Platform (MTP) to enable better test parallelization and improve integration test performance.

Changes:

  • Upgraded XUnit from v2 (2.9.3) to v3 (3.2.2) with MTP support
  • Switched code coverage from coverlet.collector to Microsoft.Testing.Extensions.CodeCoverage
  • Updated IAsyncLifetime implementations to use ValueTask instead of Task
  • Migrated test skip patterns from [Fact(Skip)] and [SkippableFact] to Assert.Skip/Assert.SkipWhen
  • Updated CI/CD workflows to use new MTP command-line syntax
  • Removed Xunit.Abstractions imports that are no longer needed

Reviewed changes

Copilot reviewed 113 out of 113 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
dotnet/Directory.Packages.props Updated package versions for XUnit v3, xRetry, and code coverage
dotnet/tests/Directory.Build.props Added MTP configuration properties and updated package references
dotnet/global.json Added MTP test runner configuration
dotnet/tests/coverage.runsettings New code coverage settings file for MTP
dotnet/tests/*/Fixture.cs (multiple) Changed IAsyncLifetime methods from Task to ValueTask and added GC.SuppressFinalize
dotnet/tests/*Tests.cs (multiple) Replaced [Fact(Skip)] and [SkippableFact] with Assert.Skip/Assert.SkipWhen patterns
dotnet/tests//.cs (multiple) Removed unused Xunit.Abstractions imports
.github/workflows/dotnet-build-and-test.yml Updated dotnet test commands to use MTP syntax with --project, --report-xunit-trx, --ignore-exit-code 8, and --filter-not-trait
dotnet/.github/skills/build-and-test/SKILL.md Updated documentation with MTP usage examples and command syntax
dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/*.csproj Removed Xunit.SkippableFact package reference
dotnet/tests/IntegrationTests/.csproj (multiple) Added NoWarn for CS8793 (return type mismatch warning)

Comment on lines +12 to +14
public override Task RunWithResponseFormatReturnsExpectedResultAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason!);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition SkipReason is not null will always be true because SkipReason is a const string. This means the test will always be skipped. If the intent is to conditionally skip the test, consider using a nullable string with a null default value, or remove the null check entirely if the test should always be skipped.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
public override Task RunWithGenericTypeReturnsExpectedResultAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason!);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition SkipReason is not null will always be true because SkipReason is a const string. This means the test will always be skipped. If the intent is to conditionally skip the test, consider using a nullable string with a null default value, or remove the null check entirely if the test should always be skipped.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +26
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync()
{
Assert.SkipWhen(SkipReason is not null, SkipReason!);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition SkipReason is not null will always be true because SkipReason is a const string. This means the test will always be skipped. If the intent is to conditionally skip the test, consider using a nullable string with a null default value, or remove the null check entirely if the test should always be skipped.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +41
public override Task RunWithChatMessageReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}

public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}

public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithNoMessageDoesNotFailAsync();
}

public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithStringReturnsExpectedResultAsync();
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition ManualVerification is not null will always be true because ManualVerification is a const string. This means these tests will always be skipped. According to the comment "Set to null to run the tests", it appears the intent is to conditionally skip. Consider changing ManualVerification to private const string? ManualVerification = "For manual verification"; to make it nullable, allowing developers to set it to null when needed.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +37
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}

public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithNoMessageDoesNotFailAsync();
}

public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}

public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithStringReturnsExpectedResultAsync();
}

public override Task SessionMaintainsHistoryAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition AnthropicChatCompletionFixture.SkipReason is not null will always be true because SkipReason is a const string. This means these tests will always be skipped. If the intent is to conditionally skip these tests, consider making SkipReason nullable or using a different pattern such as conditional compilation directives.

Suggested change
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}
public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithNoMessageDoesNotFailAsync();
}
public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}
public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithStringReturnsExpectedResultAsync();
}
public override Task SessionMaintainsHistoryAsync()
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
Assert.Skip(AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}
public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.Skip(AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithNoMessageDoesNotFailAsync();
}
public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.Skip(AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}
public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.Skip(AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithStringReturnsExpectedResultAsync();
}
public override Task SessionMaintainsHistoryAsync()
{
Assert.Skip(AnthropicChatCompletionFixture.SkipReason!);

Copilot uses AI. Check for mistakes.
Comment on lines 11 to +21
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
}

[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition AnthropicChatCompletionFixture.SkipReason is not null will always be true because SkipReason is a const string. This means these tests will always be skipped. If the intent is to conditionally skip these tests, consider making SkipReason nullable or using a different pattern such as conditional compilation directives.

Copilot uses AI. Check for mistakes.
}

async Task IAsyncLifetime.DisposeAsync()
async ValueTask IAsyncDisposable.DisposeAsync()
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In XUnit v3, IAsyncLifetime.DisposeAsync should return ValueTask, not be implemented as IAsyncDisposable.DisposeAsync. The interface should be explicitly implemented as ValueTask IAsyncLifetime.DisposeAsync() to match the IAsyncLifetime interface definition.

Copilot uses AI. Check for mistakes.
}

async Task IAsyncLifetime.DisposeAsync()
async ValueTask IAsyncDisposable.DisposeAsync()
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In XUnit v3, IAsyncLifetime.DisposeAsync should return ValueTask, not be implemented as IAsyncDisposable.DisposeAsync. The interface should be explicitly implemented as ValueTask IAsyncLifetime.DisposeAsync() to match the IAsyncLifetime interface definition.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +41
public override Task RunWithChatMessageReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithChatMessageReturnsExpectedResultAsync();
}

public override Task RunWithChatMessagesReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithChatMessagesReturnsExpectedResultAsync();
}

public override Task RunWithNoMessageDoesNotFailAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithNoMessageDoesNotFailAsync();
}

public override Task RunWithStringReturnsExpectedResultAsync()
{
Assert.SkipWhen(ManualVerification is not null, ManualVerification!);
return base.RunWithStringReturnsExpectedResultAsync();
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition ManualVerification is not null will always be true because ManualVerification is a const string. This means these tests will always be skipped. According to the comment "Set to null to run the tests", it appears the intent is to conditionally skip. Consider changing ManualVerification to private const string? ManualVerification = "For manual verification"; to make it nullable, allowing developers to set it to null when needed.

Copilot uses AI. Check for mistakes.
Comment on lines 11 to +21
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
}

[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
{
Assert.SkipWhen(AnthropicChatCompletionFixture.SkipReason is not null, AnthropicChatCompletionFixture.SkipReason!);
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition AnthropicChatCompletionFixture.SkipReason is not null will always be true because SkipReason is a const string. This means these tests will always be skipped. If the intent is to conditionally skip these tests, consider making SkipReason nullable or using a different pattern such as conditional compilation directives.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants