-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Migrate xUnit + FluentAssertions to NUnit 4.4.0 with Controlled Concurrency
Goal: Convert all test projects from xUnit to NUnit 4.4.0, remove FluentAssertions, use
Assert.That
style, and configure per-class sequential execution with controlled parallelism across classes for ReactiveUI's static-heavy codebase. If using the static classes inside ReactiveUI, only sequential access to ensure the tests run, otherwise parallelise.
1. Update Test Project Packages
Remove
xunit
xunit.runner.visualstudio
FluentAssertions
Add
NUnit
(4.4.0)NUnit3TestAdapter
(latest stable, e.g., 5.*)Microsoft.NET.Test.Sdk
(latest stable, e.g., 17.*)
Updated .csproj
Example:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
</ItemGroup>
</Project>
Tip: Use version ranges (e.g., 5.*
, 17.*
) for CI to track minor updates, or lock to specific versions after validation.
2. Configure Concurrency for Static Classes
To prevent concurrency issues with ReactiveUI's static classes, run tests sequentially within each test fixture (class) but allow parallelism across fixtures.
2.1 Assembly-Level Concurrency
Add an AssemblyInfo.Parallel.cs
file (outside any namespace) in each test project:
using NUnit.Framework;
[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]
ParallelScope.Fixtures
: Runs test fixtures in parallel, but tests within each fixture sequentially.LevelOfParallelism(4)
: Caps parallel workers (adjust based on CI resources, e.g., CPU count/2).
2.2 CI Runsettings
Create a tests.runsettings
file at the repo root:
<RunSettings>
<NUnit>
<NumberOfTestWorkers>4</NumberOfTestWorkers>
</NUnit>
</RunSettings>
Usage:
dotnet test --settings tests.runsettings
Override for full serialization (if static state is fragile):
dotnet test -- NUnit.NumberOfTestWorkers=1
3. xUnit to NUnit Attribute & API Mappings
xUnit | NUnit 4.4.0 Equivalent |
---|---|
[Fact] |
[Test] |
[Theory] + [InlineData(...)] |
[TestCase(...)] |
[Theory] + [MemberData] |
[TestCaseSource(nameof(Data))] |
[Theory] + [ClassData<T>] |
[TestCaseSource(typeof(T))] |
Assert.Equal(a,b) |
Assert.That(b, Is.EqualTo(a)) |
Assert.NotEqual(a,b) |
Assert.That(b, Is.Not.EqualTo(a)) |
Assert.True(expr) |
Assert.That(expr, Is.True) |
Assert.False(expr) |
Assert.That(expr, Is.False) |
Assert.Null(x) |
Assert.That(x, Is.Null) |
Assert.NotNull(x) |
Assert.That(x, Is.Not.Null) |
Assert.Throws<T>(...) |
Assert.Throws<T>(...) or Assert.That(..., Throws.TypeOf<T>()) |
Assert.Collection/Contains |
Assert.That(coll, Does.Contain(item)) |
Trait("Category","X") |
[Category("X")] |
IClassFixture<T> |
[OneTimeSetUp]/[OneTimeTearDown] with shared state |
[Collection("name")] |
[NonParallelizable] on conflicting fixtures |
4. Remove FluentAssertions: Convert to Assert.That
Common Conversions:
FluentAssertions | NUnit 4.4.0 Equivalent |
---|---|
actual.Should().Be(expected) |
Assert.That(actual, Is.EqualTo(expected)) |
actual.Should().NotBe(expected) |
Assert.That(actual, Is.Not.EqualTo(expected)) |
flag.Should().BeTrue() |
Assert.That(flag, Is.True) |
value.Should().BeNull() |
Assert.That(value, Is.Null) |
str.Should().Contain("x") |
Assert.That(str, Does.Contain("x")) |
str.Should().StartWith("pre") |
Assert.That(str, Does.StartWith("pre")) |
n.Should().BeGreaterThan(5) |
Assert.That(n, Is.GreaterThan(5)) |
n.Should().BeInRange(1,10) |
Assert.That(n, Is.InRange(1,10)) |
items.Should().BeEmpty() |
Assert.That(items, Is.Empty) |
items.Should().BeEquivalentTo(new[] {1,2,3}) |
Assert.That(items, Is.EquivalentTo(new[] {1,2,3})) |
act.Should().Throw<InvalidOperationException>() |
Assert.That(act, Throws.TypeOf<InvalidOperationException>()) |
await actAsync.Should().ThrowAsync<InvalidOperationException>() |
Assert.ThatAsync(async () => await actAsync(), Throws.TypeOf<InvalidOperationException>()) |
Complex Objects: Replace FluentAssertions' deep equivalence with explicit property checks in Assert.Multiple(() => { ... })
.
5. Example Conversion
Before (xUnit + FluentAssertions):
using System.Threading.Tasks;
using Xunit;
using FluentAssertions;
public class BlobCacheFacts
{
[Fact]
public void It_returns_value()
{
var result = 42;
result.Should().Be(42);
}
[Theory]
[InlineData(1, 2)]
[InlineData(2, 3)]
public void It_adds(int a, int b)
{
(a + 1).Should().Be(b);
}
[Fact]
public async Task It_throws_async()
{
Func<Task> act = async () => { await Task.Yield(); throw new InvalidOperationException(); };
await act.Should().ThrowAsync<InvalidOperationException>();
}
}
After (NUnit 4.4.0):
using System;
using System.Threading.Tasks;
using NUnit.Framework;
[TestFixture]
[Category("Akavache")]
public class BlobCacheTests
{
[Test]
public void It_returns_value()
{
var result = 42;
Assert.That(result, Is.EqualTo(42));
}
[TestCase(1, 2)]
[TestCase(2, 3)]
public void It_adds(int a, int b)
{
Assert.That(a + 1, Is.EqualTo(b));
}
[Test]
public async Task It_throws_async()
{
Assert.ThatAsync(async () =>
{
await Task.Yield();
throw new InvalidOperationException();
}, Throws.TypeOf<InvalidOperationException>());
}
}
6. Handle Akavache's Static State
- Sequential per Fixture: Ensured by
ParallelScope.Fixtures
. - Global Static State: Mark sensitive fixtures with
[NonParallelizable]
:[TestFixture, NonParallelizable] public class GlobalStateSensitiveTests { ... }
- Setup/Teardown: Use
[OneTimeSetUp]
/[OneTimeTearDown]
for fixture-wide state,[SetUp]
/[TearDown]
for per-test isolation. - Tip: Prefer isolated instances over singletons where possible.
7. Search & Replace for Migration
Find xUnit/FluentAssertions:
rg -n "using Xunit|\\[Fact|\\[Theory|InlineData|MemberData|ClassData|Trait\\(|FluentAssertions" tests/
Replace:
using Xunit;
→using NUnit.Framework;
[Fact]
→[Test]
[Theory]
→[Test]
[InlineData(
→[TestCase(
Trait("Category", "X")
→[Category("X")]
- Remove
using FluentAssertions;
- Convert
.Should()
per section 4.
Tip: Use structural search/replace in editors and review changes manually.
8. Test Setup Conversion
xUnit IClassFixture:
public class Tests : IClassFixture<MyFixture>
{
private readonly MyFixture _fx;
public Tests(MyFixture fx) => _fx = fx;
}
NUnit Equivalent:
public class MyFixture
{
public Resource R { get; private set; } = default!;
[OneTimeSetUp] public void OneTimeSetUp() => R = new Resource();
[OneTimeTearDown] public void OneTimeTearDown() => R.Dispose();
}
[TestFixture]
public class Tests
{
private static readonly MyFixture Fx = new();
[Test] public void Uses_resource() => Assert.That(Fx.R.IsReady, Is.True);
}
9. CI & Commands
Run Tests:
dotnet test
Force Full Serialization:
dotnet test -- NUnit.NumberOfTestWorkers=1
Ensure: CI uses .NET 8/9 SDK as needed.
#Constraints
Take full adavantage of the nunit format
eg
Assert.That(createdAt >= beforeInsert, Is.True);
there is a is.GreaterThan overload from meory here is the list of all the constraints https://docs.nunit.org/articles/nunit/writing-tests/constraints/Constraints.html
Multiple
Make full use of the NUnit.Multiple(() => feature, not being used properly yet.
10. Done Checklist
- Removed
xunit
,xunit.runner.visualstudio
,FluentAssertions
packages. - Added
NUnit
(4.4.0),NUnit3TestAdapter
(5.),Microsoft.NET.Test.Sdk
(17.). - Added
[Parallelizable(ParallelScope.Fixtures)]
and optional[LevelOfParallelism]
. - Committed
tests.runsettings
withNumberOfTestWorkers
. - Migrated attributes and APIs; tests compile.
- Marked static/global state fixtures with
[NonParallelizable]
. - CI runs
dotnet test
with runsettings or overrides. - Tests pass.