Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 8, 2026

Summary

This PR enhances the NUnit migration analyzer code fixers with support for additional NUnit patterns.

Completed Work

  • Generic Type Constraint Support

    • Is.TypeOf<T>()IsTypeOf<T>()
    • Is.InstanceOf<T>()IsAssignableTo<T>()
    • Is.Not.TypeOf<T>()IsNotTypeOf<T>()
    • Is.Not.InstanceOf<T>()IsNotAssignableTo<T>()
  • Collection Membership Support

    • Has.Member(item).Contains(item)
  • Apartment Attribute Support

    • [Apartment(ApartmentState.STA)][STAThreadExecutor]
  • Chained Constraint Support

    • Is.EqualTo(5).Within(2)IsEqualTo(5).Within(2)
  • DoesNotThrow Support

    • Assert.DoesNotThrow(() => action)await Assert.That(() => action).ThrowsNothing()
  • Assert.Ignore Support

    • Assert.Ignore("reason")Skip.Test("reason")
  • Assert.Fail Support

    • Assert.Fail("reason")Fail.Test("reason")
  • Code Quality

    • Moved CreateTUnitGenericAssertion to base AssertionRewriter class
    • Made IsEmptyOrNullMessage protected to eliminate duplication
  • Tests

    • 11 new tests (61 total NUnit migration tests)
    • All 455 analyzer tests pass
Original prompt

This section details on the original issue you should resolve

<issue_title>[Feature]: Enhance NUnit analyzer converters</issue_title>
<issue_description>### Problem Statement

While converting ReactiveUI project to use TUnit (from NUnit), I discovered some missing cases in the converter. I ended up writing a bunch of these and would be happy to create a PR for you. So it's a large project with a established NUnit base so be useful for your conversion process.

Below is a list of what I found:
Problem: TUnit requires explicit [Test] attribute when using [Arguments] or [MethodDataSource], but NUnit allows [TestCase]/[TestCaseSource] alone.

Current Behavior: Build fails with cryptic error

[MethodDataSource(nameof(TestData))]  // NUnit allows this
public void MyTest(int value) { }     // TUnit: Build fails

Expected Behavior: Analyzer should detect and offer code fix

Proposed Fix:

  • Diagnostic: TUnit1001 - "Method with data attribute must have [Test] attribute"
  • Severity: Error
  • Code Fix: Automatically add [Test] attribute

Problem: NUnit's Assert.Ignore() has no direct equivalent; requires throwing SkipTestException

Current Behavior: No guidance, developers must discover pattern through documentation

// NUnit
if (!IsSupported)
    Assert.Ignore("Feature not supported on this platform");

// TUnit - not obvious
if (!IsSupported)
    throw new SkipTestException("Feature not supported on this platform");

**Proposed Fix**:
- **Diagnostic**: `TUnit2001` - "Use SkipTestException instead of Assert.Ignore"
- **Severity**: Warning
- **Code Fix**: Convert to throw statement with automatic `using TUnit.Core.Exceptions;`

**Problem**: Semantic model resolution fails when namespaces are imported via implicit usings

**Context**: Modern projects use Directory.Build.props:
```xml
<ItemGroup>
  <Using Include="NUnit.Framework" />
</ItemGroup>

Impact: Analyzers that rely on ISymbolInfo to detect NUnit types may fail

Problem: NUnit's lifecycle attributes need explicit scope in TUnit

// NUnit
[SetUp] public void Setup() { }
[TearDown] public void Teardown() { }
[OneTimeSetUp] public void ClassSetup() { }
[OneTimeTearDown] public void ClassTeardown() { }

// TUnit
[Before(HookType.Test)] public void Setup() { }
[After(HookType.Test)] public void Teardown() { }
[Before(HookType.Class)] public void ClassSetup() { }
[After(HookType.Class)] public void ClassTeardown() { }

Proposed Fix:

  • Diagnostic: TUnit3001 - "Use [Before(HookType.X)] instead of [SetUp]"
  • Code Fix: Auto-convert with correct HookType parameter

Problem: NUnit's [Apartment(ApartmentState.STA)] maps to [STAThreadExecutor]

// NUnit
[Apartment(ApartmentState.STA)]
public void WpfTest() { }

// TUnit
[STAThreadExecutor]  // Requires: using TUnit.Core.Executors;
public void WpfTest() { }

Proposed Fix:

  • Diagnostic: TUnit3002 - "Use [STAThreadExecutor] instead of [Apartment(ApartmentState.STA)]"
  • Code Fix: Convert with automatic using directive

// NUnit - AssemblyInfo.cs
[assembly: Parallelizable(ParallelScope.All)]
[assembly: LevelOfParallelism(4)]

// TUnit - no assembly-level equivalent
// Conservative migration: [assembly: NotInParallel]
// Better: Per-class [NotInParallel] where needed


**Proposed Fix**:
- **Diagnostic**: `TUnit4001` - "Assembly-level parallelization detected - review and convert to class-level"
- **Severity**: Warning
- **Code Fix**: Add `[assembly: NotInParallel]` with comment explaining next steps

**Problem**: TUnit has `IsPositive()` and `IsNegative()` but no `IsZero()` or `IsNotZero()`

```csharp
// NUnit
Assert.That(count, Is.Zero);
Assert.That(count, Is.Not.Zero);

// TUnit - must use equality
await Assert.That(count).IsEqualTo(0);
await Assert.That(count).IsNotEqualTo(0);

Recommendation: Consider adding extension methods for migration compatibility:

public static class NumericAssertionExtensions
{
    public static async Task IsZero<T>(this IValueSource<T> valueSource)
        where T : INumber<T>
        => await valueSource.IsEqualTo(T.Zero);

    public static async Task IsNotZero<T>(this IValueSource<T> valueSource)
        where T : INumber<T>
        => await valueSource.IsNotEqualTo(T.Zero);
}

Problem: NUnit's scope-based multiple assertions differ from TUnit's lambda approach

// NUnit 3.x
Assert.Multiple(() =>
{
    Assert.That(x, Is.EqualTo(1));
    Assert.That(y, Is.EqualTo(2));
});

// NUnit 4.x
using (Assert.Multiple())
{
    Assert.That(x, Is.EqualTo(1));
    Assert.That(y, Is.EqualTo(2));
}

// NUnit 4.x alternative
using var scope = Assert.EnterMultipleScope();
Assert.That(x, Is.EqualTo(1));
Assert.That(y, Is.EqualTo(2));

// TUnit
await Assert.Multiple(() =>
{
    await Assert.That(x).IsEqualTo(1);
    await Assert.That(y).IsEqualTo(2);
});

Proposed Fix:

  • Diagnostic: TUnit5001 - "Convert...

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 8, 2026 11:05
Copilot AI changed the title [WIP] Add support for TUnit conversion in NUnit analyzer Enhance NUnit analyzer converters with generic type constraints, Has.Member, and Apartment attribute support Jan 8, 2026
Copilot AI requested a review from thomhurst January 8, 2026 11:20
@thomhurst
Copy link
Owner

@copilot More scenarios to consider:

I also found a problem with converting Within(delta). Here is an example:

// NUnit source
public class WithinDeltaFailureExample
{
    [Test]
    public void Example()
    {
        Assert.That(10, Is.EqualTo(5).Within(2));
    }
}

It gets converted to

// TUnit actual
public class WithinDeltaFailureExample
{
    [Test]
    public async Task Example()
    {
        await Assert.That(10).IsEqualTo(Is.EqualTo(5).Within(2));
    }
}

When the correct conversion is

// TUnit expected
public class WithinDeltaFailureExample
{
    [Test]
    public async Task Example()
    {
        await Assert.That(10).IsEqualTo(5).Within(2);
    }
}

Another case: DoesNotThrow

// NUnit source
Assert.DoesNotThrow(() => x += y);

Should become

// TUnit expected
await Assert.That(() => x += y).ThrowsNothing();
// NUnit source
Assert.Ignore();

Should become

// TUnit expected
Skip.Test("Reason");
// NUnit source
Assert.Fail();

Should become

// TUnit expected
Fail.Test("Reason");

Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot More scenarios to consider:

I also found a problem with converting Within(delta). Here is an example:
...

Added support for all the scenarios you mentioned in commit ca35aae:

  • Is.EqualTo(5).Within(2)IsEqualTo(5).Within(2) (chained constraint)
  • Assert.DoesNotThrow(() => action)await Assert.That(() => action).ThrowsNothing()
  • Assert.Ignore("reason")Skip.Test("reason")
  • Assert.Fail("reason")Fail.Test("reason")

@thomhurst thomhurst marked this pull request as ready for review January 8, 2026 12:27
Copilot AI review requested due to automatic review settings January 8, 2026 12:27
@thomhurst thomhurst enabled auto-merge (squash) January 8, 2026 12:28
Copy link

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 enhances the NUnit migration analyzer code fixers with support for additional NUnit patterns that were missing from the conversion tooling. The changes enable automatic conversion of generic type constraints, collection membership assertions, apartment threading attributes, chained constraint modifiers, and special assertion methods like DoesNotThrow, Ignore, and Fail.

Key Changes

  • Added support for generic type constraint assertions (Is.TypeOf<T>(), Is.InstanceOf<T>()) with proper mapping to TUnit's IsTypeOf<T>() and IsAssignableTo<T>()
  • Implemented conversion for Has.Member(item) to .Contains(item)
  • Added [Apartment(ApartmentState.STA)] to [STAThreadExecutor] attribute conversion
  • Implemented chained constraint support (e.g., Is.EqualTo(5).Within(2))
  • Added conversion for Assert.DoesNotThrow(), Assert.Ignore(), and Assert.Fail()
  • Refactored CreateTUnitGenericAssertion method to base AssertionRewriter class for reusability

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
TUnit.Analyzers/Migrators/Base/MigrationHelpers.cs Added mapping for "Apartment" → "STAThreadExecutor" attribute name conversion
TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs Implemented conversion logic for generic constraints, Has.Member, chained constraints, DoesNotThrow, and Apartment attributes; updated Fail/Skip assertion conversions
TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs Moved CreateTUnitGenericAssertion method from derived class to base class; changed IsEmptyOrNullMessage visibility to protected for reuse
TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs Added 11 comprehensive test cases covering all new conversion patterns

@thomhurst thomhurst merged commit f274cd9 into main Jan 8, 2026
17 of 19 checks passed
@thomhurst thomhurst deleted the copilot/enhance-nunit-analyzer-converters branch January 8, 2026 12:50
This was referenced Jan 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Enhance NUnit analyzer converters

2 participants