
Xunit
- 37 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
xunit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- xunit
- AI & Agent Building
- AI-coding skill
Xunit by the numbers
- 37 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #8,516 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill xunitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 37 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
xUnit.net
Trigger On
- the repo uses xUnit v2 or xUnit v3
- you need to add, run, debug, or repair xUnit tests
- the team is unsure whether a project is using VSTest or Microsoft.Testing.Platform
Value
- produce a concrete project delta: code, docs, config, tests, CI, or review artifact
- reduce ambiguity through explicit planning, verification, and final validation skills
- leave reusable project context so future tasks are faster and safer
Do Not Use For
- TUnit projects
- MSTest projects
- generic test strategy with no xUnit-specific mechanics
Inputs
- the nearest
AGENTS.md - the test project file and package references
- the active runner model for the test project
Quick Start
1. Read the nearest AGENTS.md and confirm scope and constraints. 2. Run this skill's Workflow through the Ralph Loop until outcomes are acceptable. 3. Return the Required Result Format with concrete artifacts and verification evidence.
Workflow
1. Detect the active xUnit model before changing commands:
xunitusually means v2xunit.v3means v3xunit.runner.visualstudioplusMicrosoft.NET.Test.Sdkusually means VSTest compatibility is enabledTestingPlatformDotnetTestSupportorUseMicrosoftTestingPlatformRunnermeans Microsoft.Testing.Platform is in play
2. Read the repo's real test command from AGENTS.md. If the repo has no explicit command yet, start with dotnet test PROJECT_OR_SOLUTION. 3. Keep the runner model consistent:
- xUnit v2 usually runs through VSTest
- xUnit v3 can run as a standalone executable with
dotnet run - xUnit v3 can also integrate with Microsoft.Testing.Platform
- do not mix VSTest-only switches into Microsoft.Testing.Platform runs
4. Run the narrowest useful scope first:
- one project
- one class
- one trait
- one method
5. Prefer [Theory] for stable data-driven coverage and [Fact] for single-path invariant checks. 6. Keep xunit.analyzers enabled when present. Fix analyzer findings instead of muting them casually.
Bootstrap When Missing
If xUnit is requested but not configured:
1. Detect current framework first:
rg -n "xunit(\\.v3)?|xunit\\.runner\\.visualstudio|TestingPlatformDotnetTestSupport|UseMicrosoftTestingPlatformRunner|TUnit|MSTest" -g '*.csproj' .
2. If the repo currently uses TUnit or MSTest, do not auto-migrate. Return status: not_applicable unless migration is explicitly requested. 3. For explicit xUnit adoption, add packages to the target test project:
dotnet add TEST_PROJECT.csproj package xunit.v3- optional VSTest bridge:
dotnet add TEST_PROJECT.csproj package xunit.runner.visualstudio
4. Add repo test commands and runner notes to AGENTS.md. 5. Run dotnet test TEST_PROJECT.csproj or repo-defined xUnit command and return status: configured or status: improved.
Deliver
- xUnit tests that match the repo's active xUnit version and runner
- commands that work in local and CI runs
- focused verification before broader suite execution
Validate
- the chosen CLI matches the active runner model
- test filters or focused runs are valid for that runner
- tests use deterministic inputs and assertions
- xUnit-specific analyzers remain active unless the repo documents an exception
Ralph Loop
Use the Ralph Loop for every task, including docs, architecture, testing, and tooling work.
1. Plan first (mandatory):
- analyze current state
- define target outcome, constraints, and risks
- write a detailed execution plan
- list final validation skills to run at the end, with order and reason
2. Execute one planned step and produce a concrete delta. 3. Review the result and capture findings with actionable next fixes. 4. Apply fixes in small batches and rerun the relevant checks or review steps. 5. Update the plan after each iteration. 6. Repeat until outcomes are acceptable or only explicit exceptions remain. 7. If a dependency is missing, bootstrap it or return status: not_applicable with explicit reason and fallback path.
Required Result Format
status:complete|clean|improved|configured|not_applicable|blockedplan: concise plan and current iteration stepactions_taken: concrete changes madevalidation_skills: final skills run, or skipped with reasonsverification: commands, checks, or review evidence summaryremaining: top unresolved items ornone
For setup-only requests with no execution, return status: configured and exact next commands.
Load References
- references/xunit.md
- references/patterns.md
- references/anti-patterns.md
Example Requests
- "Run this xUnit suite correctly."
- "Fix our xUnit v3 test command."
- "Add an xUnit regression test and keep CI compatible."
{
"version": "1.0.0",
"category": "Testing",
"packages": [
"xunit",
"xunit.v3"
]
}
xUnit Anti-Patterns
Avoid these common testing mistakes.
Test Interdependence
Tests must not depend on execution order or shared mutable state.
Bad:
public class UserServiceTests
{
private static int _testUserId;
[Fact]
public void CreateUser_ValidData_ReturnsId()
{
var service = new UserService();
_testUserId = service.CreateUser("test@example.com"); // sets static state
Assert.True(_testUserId > 0);
}
[Fact]
public void GetUser_ExistingId_ReturnsUser()
{
var service = new UserService();
var user = service.GetUser(_testUserId); // depends on other test running first
Assert.NotNull(user);
}
}Good:
public class UserServiceTests
{
[Fact]
public void CreateUser_ValidData_ReturnsId()
{
// Arrange
var service = new UserService();
// Act
var userId = service.CreateUser("test@example.com");
// Assert
Assert.True(userId > 0);
}
[Fact]
public void GetUser_ExistingId_ReturnsUser()
{
// Arrange
var service = new UserService();
var userId = service.CreateUser("test@example.com"); // each test creates its own data
// Act
var user = service.GetUser(userId);
// Assert
Assert.NotNull(user);
}
}Excessive Mocking
Over-mocking creates brittle tests that verify implementation rather than behavior.
Bad:
public class OrderServiceTests
{
[Fact]
public void PlaceOrder_Valid_CallsAllMethods()
{
// Arrange
var loggerMock = new Mock<ILogger<OrderService>>();
var repoMock = new Mock<IOrderRepository>();
var validatorMock = new Mock<IOrderValidator>();
var pricingMock = new Mock<IPricingService>();
var inventoryMock = new Mock<IInventoryService>();
var notificationMock = new Mock<INotificationService>();
validatorMock.Setup(v => v.Validate(It.IsAny<Order>())).Returns(true);
pricingMock.Setup(p => p.CalculateTotal(It.IsAny<Order>())).Returns(100m);
inventoryMock.Setup(i => i.Reserve(It.IsAny<Order>())).Returns(true);
repoMock.Setup(r => r.Save(It.IsAny<Order>())).Returns(1);
var service = new OrderService(loggerMock.Object, repoMock.Object,
validatorMock.Object, pricingMock.Object, inventoryMock.Object, notificationMock.Object);
// Act
service.PlaceOrder(new Order());
// Assert - verifying every internal call creates coupling to implementation
validatorMock.Verify(v => v.Validate(It.IsAny<Order>()), Times.Once);
pricingMock.Verify(p => p.CalculateTotal(It.IsAny<Order>()), Times.Once);
inventoryMock.Verify(i => i.Reserve(It.IsAny<Order>()), Times.Once);
repoMock.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
notificationMock.Verify(n => n.Send(It.IsAny<string>()), Times.Once);
}
}Good:
public class OrderServiceTests
{
[Fact]
public void PlaceOrder_ValidOrder_ReturnsOrderId()
{
// Arrange - use real implementations where cheap, mock only external boundaries
var repository = new InMemoryOrderRepository();
var notificationService = Substitute.For<INotificationService>();
var service = new OrderService(repository, notificationService);
var order = new Order { CustomerId = 1, Items = [new OrderItem { ProductId = 1, Quantity = 1 }] };
// Act
var orderId = service.PlaceOrder(order);
// Assert - verify observable outcomes
Assert.True(orderId > 0);
var savedOrder = repository.GetById(orderId);
Assert.NotNull(savedOrder);
Assert.Equal(OrderStatus.Placed, savedOrder.Status);
}
}Testing Implementation Instead of Behavior
Tests should verify what the code does, not how it does it.
Bad:
public class CacheTests
{
[Fact]
public void Get_CacheMiss_CallsUnderlyingStore()
{
// Arrange
var storeMock = new Mock<IDataStore>();
storeMock.Setup(s => s.Get("key")).Returns("value");
var cache = new Cache(storeMock.Object);
// Act
cache.Get("key");
cache.Get("key");
// Assert - tests internal caching logic, not behavior
storeMock.Verify(s => s.Get("key"), Times.Once);
}
}Good:
public class CacheTests
{
[Fact]
public void Get_SameKey_ReturnsSameValue()
{
// Arrange
var store = new InMemoryDataStore();
store.Set("key", "value");
var cache = new Cache(store);
// Act
var result1 = cache.Get("key");
var result2 = cache.Get("key");
// Assert - verifies behavior: caching returns consistent results
Assert.Equal("value", result1);
Assert.Equal("value", result2);
Assert.Same(result1, result2); // if reference equality matters
}
[Fact]
public void Get_AfterExpiry_ReturnsUpdatedValue()
{
// Arrange
var store = new InMemoryDataStore();
store.Set("key", "original");
var timeProvider = new FakeTimeProvider();
var cache = new Cache(store, timeProvider, ttl: TimeSpan.FromMinutes(5));
// Act
var result1 = cache.Get("key");
store.Set("key", "updated");
timeProvider.Advance(TimeSpan.FromMinutes(10));
var result2 = cache.Get("key");
// Assert
Assert.Equal("original", result1);
Assert.Equal("updated", result2);
}
}Ignoring Test Isolation
Each test must run in isolation without affecting others.
Bad:
public class ConfigurationTests
{
[Fact]
public void SetGlobalConfig_UpdatesEnvironment()
{
Environment.SetEnvironmentVariable("APP_MODE", "test"); // pollutes other tests
var config = new AppConfiguration();
Assert.Equal("test", config.Mode);
}
}Good:
public class ConfigurationTests : IDisposable
{
private readonly string? _originalValue;
public ConfigurationTests()
{
_originalValue = Environment.GetEnvironmentVariable("APP_MODE");
}
[Fact]
public void SetGlobalConfig_UpdatesEnvironment()
{
// Arrange
Environment.SetEnvironmentVariable("APP_MODE", "test");
var config = new AppConfiguration();
// Assert
Assert.Equal("test", config.Mode);
}
public void Dispose()
{
Environment.SetEnvironmentVariable("APP_MODE", _originalValue);
}
}Even better - inject configuration:
public class ConfigurationTests
{
[Fact]
public void AppConfiguration_TestMode_ReturnsTestBehavior()
{
// Arrange
var settings = new Dictionary<string, string?> { ["APP_MODE"] = "test" };
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(settings)
.Build();
var config = new AppConfiguration(configuration);
// Assert
Assert.Equal("test", config.Mode);
}
}Non-Deterministic Tests
Tests must produce the same result every time.
Bad:
public class TimestampTests
{
[Fact]
public void CreateRecord_SetsTimestamp()
{
// Arrange
var service = new RecordService();
// Act
var record = service.CreateRecord("data");
// Assert - fails intermittently when clock rolls over
Assert.Equal(DateTime.Now.Date, record.CreatedAt.Date);
}
}
public class RandomTests
{
[Fact]
public void GenerateId_ReturnsUniqueId()
{
var service = new IdGenerator();
var id1 = service.Generate();
var id2 = service.Generate();
Assert.NotEqual(id1, id2); // usually passes, sometimes fails
}
}Good:
public class TimestampTests
{
[Fact]
public void CreateRecord_SetsTimestampFromProvider()
{
// Arrange
var fixedTime = new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
var timeProvider = new FakeTimeProvider(fixedTime);
var service = new RecordService(timeProvider);
// Act
var record = service.CreateRecord("data");
// Assert
Assert.Equal(fixedTime, record.CreatedAt);
}
}
public class RandomTests
{
[Fact]
public void GenerateId_WithSeed_ReturnsExpectedId()
{
// Arrange
var randomProvider = new SeededRandomProvider(42);
var service = new IdGenerator(randomProvider);
// Act
var id = service.Generate();
// Assert
Assert.Equal("expected-seeded-value", id);
}
[Fact]
public void GenerateId_ReturnsValidFormat()
{
// Arrange
var service = new IdGenerator();
// Act
var id = service.Generate();
// Assert - verify format rather than specific value
Assert.Matches(@"^[a-f0-9]{32}$", id);
}
}Swallowing Exceptions
Tests must not catch exceptions without re-throwing or asserting.
Bad:
public class FileServiceTests
{
[Fact]
public void ReadFile_MissingFile_HandlesGracefully()
{
try
{
var service = new FileService();
var content = service.ReadFile("/nonexistent/path");
Assert.NotNull(content);
}
catch
{
// test passes either way - hides real failures
}
}
}Good:
public class FileServiceTests
{
[Fact]
public void ReadFile_MissingFile_ThrowsFileNotFoundException()
{
// Arrange
var service = new FileService();
// Act & Assert
var exception = Assert.Throws<FileNotFoundException>(
() => service.ReadFile("/nonexistent/path"));
Assert.Contains("nonexistent", exception.FileName);
}
[Fact]
public void TryReadFile_MissingFile_ReturnsFalse()
{
// Arrange
var service = new FileService();
// Act
var success = service.TryReadFile("/nonexistent/path", out var content);
// Assert
Assert.False(success);
Assert.Null(content);
}
}Magic Numbers and Strings
Unexplained literals reduce test clarity.
Bad:
public class DiscountTests
{
[Fact]
public void CalculateDiscount_ReturnsExpected()
{
var service = new DiscountService();
var discount = service.Calculate(150, 3, true);
Assert.Equal(22.5m, discount);
}
}Good:
public class DiscountTests
{
[Fact]
public void CalculateDiscount_GoldMemberWithBulkOrder_AppliesCombinedDiscount()
{
// Arrange
const decimal orderTotal = 150m;
const int itemCount = 3;
const bool isGoldMember = true;
const decimal expectedBulkDiscount = 15m; // 10% for 3+ items
const decimal expectedMemberDiscount = 7.5m; // 5% gold member bonus
const decimal expectedTotalDiscount = 22.5m;
var service = new DiscountService();
// Act
var discount = service.Calculate(orderTotal, itemCount, isGoldMember);
// Assert
Assert.Equal(expectedTotalDiscount, discount);
}
}Asserting Too Much or Too Little
Each test should verify one logical concept.
Bad - too many assertions:
public class UserRegistrationTests
{
[Fact]
public void RegisterUser_ValidData_EverythingWorks()
{
var service = new UserService();
var result = service.Register("test@example.com", "password123");
Assert.True(result.Success);
Assert.NotNull(result.User);
Assert.Equal("test@example.com", result.User.Email);
Assert.True(result.User.IsActive);
Assert.NotNull(result.User.CreatedAt);
Assert.True(result.EmailSent);
Assert.Equal(1, service.GetUserCount());
Assert.NotNull(service.GetUserByEmail("test@example.com"));
// continues for 20 more assertions
}
}Bad - too few assertions:
public class UserRegistrationTests
{
[Fact]
public void RegisterUser_ValidData_Succeeds()
{
var service = new UserService();
var result = service.Register("test@example.com", "password123");
Assert.True(result.Success); // what about the user? was it actually created?
}
}Good:
public class UserRegistrationTests
{
[Fact]
public void RegisterUser_ValidData_CreatesActiveUser()
{
// Arrange
var service = new UserService();
const string email = "test@example.com";
// Act
var result = service.Register(email, "password123");
// Assert
Assert.True(result.Success);
Assert.NotNull(result.User);
Assert.Equal(email, result.User.Email);
Assert.True(result.User.IsActive);
}
[Fact]
public void RegisterUser_ValidData_SendsWelcomeEmail()
{
// Arrange
var emailService = Substitute.For<IEmailService>();
var service = new UserService(emailService);
// Act
service.Register("test@example.com", "password123");
// Assert
emailService.Received(1).SendWelcomeEmail("test@example.com");
}
[Fact]
public void RegisterUser_ValidData_PersistsUser()
{
// Arrange
var repository = new InMemoryUserRepository();
var service = new UserService(repository);
// Act
var result = service.Register("test@example.com", "password123");
// Assert
var persistedUser = repository.GetByEmail("test@example.com");
Assert.NotNull(persistedUser);
Assert.Equal(result.User.Id, persistedUser.Id);
}
}Async Void Tests
xUnit does not properly await async void test methods.
Bad:
public class AsyncTests
{
[Fact]
public async void FetchData_ReturnsData() // async void - xUnit won't wait
{
var service = new DataService();
var data = await service.FetchAsync();
Assert.NotEmpty(data); // may not execute before test completes
}
}Good:
public class AsyncTests
{
[Fact]
public async Task FetchData_ReturnsData() // async Task - xUnit awaits properly
{
// Arrange
var service = new DataService();
// Act
var data = await service.FetchAsync();
// Assert
Assert.NotEmpty(data);
}
}Constructor Abuse
Test constructors are for shared setup, not test-specific logic.
Bad:
public class PaymentTests
{
private readonly PaymentResult _result;
public PaymentTests()
{
var service = new PaymentService();
_result = service.ProcessPayment(new Payment { Amount = 100 }); // runs for every test
}
[Fact]
public void ProcessPayment_ValidAmount_Succeeds() => Assert.True(_result.Success);
[Fact]
public void ProcessPayment_ValidAmount_ReturnsTransactionId() => Assert.NotNull(_result.TransactionId);
}Good:
public class PaymentTests(PaymentFixture fixture) : IClassFixture<PaymentFixture>
{
[Fact]
public void ProcessPayment_ValidAmount_Succeeds()
{
// Arrange
var service = new PaymentService(fixture.Gateway);
var payment = new Payment { Amount = 100 };
// Act
var result = service.ProcessPayment(payment);
// Assert
Assert.True(result.Success);
}
[Fact]
public void ProcessPayment_ValidAmount_ReturnsTransactionId()
{
// Arrange
var service = new PaymentService(fixture.Gateway);
var payment = new Payment { Amount = 100 };
// Act
var result = service.ProcessPayment(payment);
// Assert
Assert.NotNull(result.TransactionId);
}
}
public class PaymentFixture
{
public IPaymentGateway Gateway { get; } = new TestPaymentGateway();
}Sources
xUnit Testing Patterns
Arrange-Act-Assert (AAA)
Structure every test method in three distinct phases:
public class CalculatorTests
{
[Fact]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
var a = 5;
var b = 3;
// Act
var result = calculator.Add(a, b);
// Assert
Assert.Equal(8, result);
}
}Keep the phases visually separated. Avoid mixing setup, execution, and verification in the same block.
Class Fixtures
Use class fixtures to share expensive setup across all tests in a class. Primary constructors inject the fixture directly:
public class DatabaseFixture : IAsyncLifetime
{
public IDbConnection Connection { get; private set; } = null!;
public async Task InitializeAsync()
{
Connection = new SqliteConnection("Data Source=:memory:");
await Connection.OpenAsync();
await SeedTestDataAsync();
}
public async Task DisposeAsync()
{
await Connection.DisposeAsync();
}
private async Task SeedTestDataAsync()
{
// seed shared test data
}
}
public class UserRepositoryTests(DatabaseFixture fixture) : IClassFixture<DatabaseFixture>
{
[Fact]
public async Task GetUser_ExistingId_ReturnsUser()
{
// Arrange
var repository = new UserRepository(fixture.Connection);
// Act
var user = await repository.GetUserAsync(1);
// Assert
Assert.NotNull(user);
Assert.Equal("TestUser", user.Name);
}
}Collection Fixtures
Share expensive resources across multiple test classes:
public class IntegrationTestFixture : IAsyncLifetime
{
public HttpClient Client { get; private set; } = null!;
private WebApplicationFactory<Program> _factory = null!;
public async Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>();
Client = _factory.CreateClient();
await Task.CompletedTask;
}
public async Task DisposeAsync()
{
Client.Dispose();
await _factory.DisposeAsync();
}
}
[CollectionDefinition("Integration")]
public class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture>;
[Collection("Integration")]
public class OrdersApiTests(IntegrationTestFixture fixture)
{
[Fact]
public async Task GetOrders_ReturnsOk()
{
// Act
var response = await fixture.Client.GetAsync("/api/orders");
// Assert
response.EnsureSuccessStatusCode();
}
}
[Collection("Integration")]
public class ProductsApiTests(IntegrationTestFixture fixture)
{
[Fact]
public async Task GetProducts_ReturnsOk()
{
// Act
var response = await fixture.Client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
}
}Theory Data Patterns
InlineData for Simple Cases
public class ValidationTests
{
[Theory]
[InlineData("", false)]
[InlineData("a", false)]
[InlineData("ab", false)]
[InlineData("abc", true)]
[InlineData("valid-password-123", true)]
public void IsValidPassword_VariousInputs_ReturnsExpected(string password, bool expected)
{
// Arrange
var validator = new PasswordValidator(minLength: 3);
// Act
var result = validator.IsValid(password);
// Assert
Assert.Equal(expected, result);
}
}MemberData for Complex Objects
public class OrderProcessorTests
{
public static TheoryData<Order, decimal> OrderDiscountData => new()
{
{ new Order { Total = 100m, CustomerTier = CustomerTier.Standard }, 0m },
{ new Order { Total = 100m, CustomerTier = CustomerTier.Silver }, 5m },
{ new Order { Total = 100m, CustomerTier = CustomerTier.Gold }, 10m },
{ new Order { Total = 500m, CustomerTier = CustomerTier.Gold }, 75m }
};
[Theory]
[MemberData(nameof(OrderDiscountData))]
public void CalculateDiscount_VariousOrders_ReturnsExpectedDiscount(Order order, decimal expectedDiscount)
{
// Arrange
var processor = new OrderProcessor();
// Act
var discount = processor.CalculateDiscount(order);
// Assert
Assert.Equal(expectedDiscount, discount);
}
}ClassData for Reusable Data Sets
public class EdgeCaseStringData : TheoryData<string?>
{
public EdgeCaseStringData()
{
Add(null);
Add("");
Add(" ");
Add("\t");
Add("\n");
Add(" \t\n ");
}
}
public class StringUtilityTests
{
[Theory]
[ClassData(typeof(EdgeCaseStringData))]
public void IsNullOrWhitespace_EdgeCases_ReturnsTrue(string? input)
{
// Act
var result = string.IsNullOrWhiteSpace(input);
// Assert
Assert.True(result);
}
}Mocking with NSubstitute
Prefer NSubstitute for readable substitute configuration:
public class OrderServiceTests(ITestOutputHelper output)
{
[Fact]
public async Task PlaceOrder_ValidOrder_SendsNotification()
{
// Arrange
var notificationService = Substitute.For<INotificationService>();
var orderRepository = Substitute.For<IOrderRepository>();
orderRepository.SaveAsync(Arg.Any<Order>()).Returns(Task.FromResult(true));
var service = new OrderService(orderRepository, notificationService);
var order = new Order { CustomerId = 1, Items = [new OrderItem { ProductId = 1, Quantity = 2 }] };
// Act
await service.PlaceOrderAsync(order);
// Assert
await notificationService.Received(1).SendOrderConfirmationAsync(Arg.Is<Order>(o => o.CustomerId == 1));
output.WriteLine("Order placed and notification sent");
}
[Fact]
public async Task PlaceOrder_RepositoryFails_ThrowsException()
{
// Arrange
var notificationService = Substitute.For<INotificationService>();
var orderRepository = Substitute.For<IOrderRepository>();
orderRepository.SaveAsync(Arg.Any<Order>()).ThrowsAsync(new DataException("Connection failed"));
var service = new OrderService(orderRepository, notificationService);
var order = new Order { CustomerId = 1 };
// Act & Assert
await Assert.ThrowsAsync<DataException>(() => service.PlaceOrderAsync(order));
await notificationService.DidNotReceive().SendOrderConfirmationAsync(Arg.Any<Order>());
}
}Mocking with Moq
Use Moq when the project already depends on it:
public class PaymentProcessorTests
{
[Fact]
public async Task ProcessPayment_ValidCard_ReturnsSuccess()
{
// Arrange
var gatewayMock = new Mock<IPaymentGateway>();
gatewayMock
.Setup(g => g.ChargeAsync(It.IsAny<string>(), It.Is<decimal>(d => d > 0)))
.ReturnsAsync(new PaymentResult { Success = true, TransactionId = "TX123" });
var processor = new PaymentProcessor(gatewayMock.Object);
// Act
var result = await processor.ProcessPaymentAsync("4111111111111111", 99.99m);
// Assert
Assert.True(result.Success);
Assert.Equal("TX123", result.TransactionId);
gatewayMock.Verify(g => g.ChargeAsync("4111111111111111", 99.99m), Times.Once);
}
}Output and Diagnostics
Use ITestOutputHelper for test diagnostics:
public class DiagnosticTests(ITestOutputHelper output)
{
[Fact]
public void ComplexCalculation_LargeInput_CompletesWithinTimeout()
{
// Arrange
var calculator = new ComplexCalculator();
var input = GenerateLargeInput();
output.WriteLine($"Testing with input size: {input.Length}");
var stopwatch = Stopwatch.StartNew();
// Act
var result = calculator.Process(input);
// Assert
stopwatch.Stop();
output.WriteLine($"Completed in {stopwatch.ElapsedMilliseconds}ms");
Assert.True(stopwatch.ElapsedMilliseconds < 5000, "Calculation took too long");
}
private static int[] GenerateLargeInput() => Enumerable.Range(0, 100_000).ToArray();
}Async Test Patterns
xUnit handles async tests natively:
public class AsyncServiceTests
{
[Fact]
public async Task FetchData_ValidEndpoint_ReturnsData()
{
// Arrange
var service = new DataService();
// Act
var data = await service.FetchDataAsync("/api/items");
// Assert
Assert.NotEmpty(data);
}
[Fact]
public async Task FetchData_Timeout_ThrowsOperationCanceledException()
{
// Arrange
var service = new DataService();
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1));
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => service.FetchDataAsync("/api/slow-endpoint", cts.Token));
}
}Trait-Based Organization
Use traits sparingly for CI filtering:
public class IntegrationTests
{
[Fact]
[Trait("Category", "Integration")]
[Trait("Database", "SqlServer")]
public async Task CreateUser_ValidData_PersistsToDatabase()
{
// integration test implementation
}
}
public class UnitTests
{
[Fact]
[Trait("Category", "Unit")]
public void ValidateEmail_InvalidFormat_ReturnsFalse()
{
// unit test implementation
}
}Run filtered:
dotnet test --filter "Category=Unit"
dotnet test --filter "Category!=Integration"Sources
xUnit in MCAF
Open/Free Status
- open source
- free to use
Install
xUnit v3 package setup:
dotnet add package xunit.v3VSTest compatibility package when the repo intentionally uses that runner:
dotnet add package xunit.runner.visualstudioVerify First
Before adding packages, check what the repo already references:
rg -n "xunit(\\.v3)?|xunit\\.runner\\.visualstudio|TestingPlatformDotnetTestSupport|UseMicrosoftTestingPlatformRunner" -g '*.csproj' .Use this reference when the repository already chose xUnit and you need framework-specific commands, package checks, or CI guardrails.
Detect the xUnit Model
Use the project file as the source of truth:
rg -n "xunit\\.v3|xunit.runner.visualstudio|Microsoft\\.NET\\.Test\\.Sdk|TestingPlatformDotnetTestSupport|UseMicrosoftTestingPlatformRunner" -g '*.csproj' .Typical markers:
- xUnit v2:
xunit - xUnit v3:
xunit.v3 - VSTest compatibility:
xunit.runner.visualstudioandMicrosoft.NET.Test.Sdk - Microsoft.Testing.Platform support:
TestingPlatformDotnetTestSupportorUseMicrosoftTestingPlatformRunner
Common Commands
Start with the repo's test command from AGENTS.md. If the repo has not documented one yet, these are the safe defaults:
dotnet test MySolution.sln
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj --no-buildFocused VSTest-style run:
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj --filter "FullyQualifiedName~Namespace.TypeName"xUnit v3 standalone runner:
dotnet run --project tests/MyProject.Tests/MyProject.Tests.csprojxUnit v3 with Microsoft.Testing.Platform-style class filtering:
dotnet run --project tests/MyProject.Tests/MyProject.Tests.csproj -- --filter-class Namespace.TypeNameIf the project enables TestingPlatformDotnetTestSupport, dotnet test can forward into Microsoft.Testing.Platform. Keep those switches consistent with the runner the project actually uses.
CI Notes
- Use one runner model per project. Do not mix VSTest-only flags and Microsoft.Testing.Platform flags in the same command.
- Build first, then use
--no-buildfor repeat test runs. - Keep coverage driver aligned with the runner:
- VSTest:
coverlet.collectoror--collect:"XPlat Code Coverage" - Microsoft.Testing.Platform:
coverlet.MTP - Keep xUnit analyzers on:
- xUnit v2 2.3+ usually brings them through the main
xunitpackage - xUnit v3 brings analyzer guidance through the main package set unless the repo split packages explicitly
Good Defaults
- prefer
[Theory]plus stable inline or member data for variant-heavy behavior - use traits only when the repo already relies on them for filtering
- avoid runner rewrites in the same change as behavior work unless the current command is already broken