Portamical.xUnit_v3 2.0.0

dotnet add package Portamical.xUnit_v3 --version 2.0.0
                    
NuGet\Install-Package Portamical.xUnit_v3 -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Portamical.xUnit_v3" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Portamical.xUnit_v3" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Portamical.xUnit_v3" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Portamical.xUnit_v3 --version 2.0.0
                    
#r "nuget: Portamical.xUnit_v3, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Portamical.xUnit_v3@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Portamical.xUnit_v3&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Portamical.xUnit_v3&version=2.0.0
                    
Install as a Cake Tool

Portamical.xUnit_v3

xUnit v3 adapter for Portamical: Universal, identity-driven test data modeling for .NET.

Portamical.xUnit_v3 bridges Portamical.Core test data to xUnit v3 (3.2.2+), enabling strongly-typed, reusable test data with automatic deduplication and self-documenting test names.


Portamical.xUnit_v3 [2.0.0] - 2026-03-16

⚠️ BREAKING CHANGES

Portamical.TestBases.TestBase (inherited from Portamical 2.0.0)

Removed IDisposable Implementation

// BEFORE (v1):
public abstract class TestBase : IDisposable
{
    protected static ArgsCode ArgsCode { get; set; } = AsInstance;
    
    protected static long ResetLogCounter()
        => Resolver.ResetLogCounter();
    
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// AFTER (v2):
public abstract class TestBase  // ❌ No IDisposable
{
    // ❌ Removed: ArgsCode property with setter
    // ❌ Removed: ResetLogCounter() method
    // ❌ Removed: Dispose() methods
    
    protected static ArgsCode AsInstance => ArgsCode.Instance;
    protected static ArgsCode AsProperties => ArgsCode.Properties;
    
    // ✅ Added: ConvertAsInstance() helpers
}

Removed Members:

❌ `IDisposable` interface implementation
❌ `public void Dispose()`
❌ `protected virtual void Dispose(bool disposing)`
❌ `protected static long ResetLogCounter()`
❌ `protected static ArgsCode ArgsCode { get; set; }`

Migration Required:

// v1 Code:
public class MyTests : TestBase
{
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            ArgsCode = AsInstance;      // ❌ Property removed
            ResetLogCounter();          // ❌ Method removed
        }
        base.Dispose(disposing);        // ❌ Base class no longer IDisposable
    }
}

// v2 Migration:
using Portamical.Core.Safety;  // ✅ Add namespace

public class MyTests : TestBase  // ✅ Remove IDisposable inheritance
{
    // ✅ Use xUnit v3's lifecycle hooks instead:
    public MyTests()
    {
        // Setup
    }
    
    // ✅ Use IAsyncLifetime or similar for cleanup:
    public void Dispose()  // ✅ Implement directly if needed
    {
        Resolver.ResetLogCounter();  // ✅ Call directly
    }
}

Install

dotnet add package Portamical.xUnit_v3

Prerequisites:

  • xunit.v3 3.2.2+
  • .NET 10.0

Example

1. Create Test Data (Framework-Agnostic)

using static Portamical.Core.Factories.TestDataFactory;

public class CalculatorDataSource
{
    public static IEnumerable<TestData<int, int>> AddCases()
    {
        yield return CreateTestData(
            definition: "adding two positive numbers",
            result: "returns their sum",
            arg1: 2,
            arg2: 3);

        yield return CreateTestData(
            definition: "adding with zero",
            result: "returns the other number",
            arg1: 0,
            arg2: 5);
    }
}

2. Consume in xUnit v3 Tests

Option A: Shared Style (Framework-Agnostic)
using Portamical.TestBases.TestDataCollection;

public class CalculatorTests : TestBase
{
    public static IEnumerable<TestData<int, int>> AddArgs
        => Convert(CalculatorDataSource.AddCases());

    [Theory, MemberData(nameof(AddArgs))]
    public void Add_validInputs_returnsSum(TestData<int, int> testData)
    {
        // Arrange
        var sut = new Calculator();

        // Act
        var actual = sut.Add(testData.Arg1, testData.Arg2);

        // Assert
        Assert.Equal(5, actual); // (example expected)
    }
}
Option B: Native Style (xUnit v3-Specific)
using Portamical.xUnit_v3.Attributes;
using Portamical.xUnit_v3.TestBases;

public class CalculatorTests : TestBase
{
    public static TheoryTestData<TestData<int, int>> AddArgs
        => Convert(CalculatorDataSource.AddCases());

    [Theory, PortamicalData(nameof(AddArgs))]
    public void Add_validInputs_returnsSum(TestData<int, int> testData)
    {
        // Arrange
        var sut = new Calculator();

        // Act
        var actual = sut.Add(testData.Arg1, testData.Arg2);

        // Assert
        Assert.Equal(5, actual);
    }
}

What's Included

Converters

Transform test data collections into xUnit v3's TheoryTestData<T>:

using Portamical.xUnit_v3.Converters;

var theoryData = testDataCollection.ToTheoryTestData(
    ArgsCode.Instance,
    testMethodName: nameof(MyTest));

Data Providers

TheoryTestData<TTestData> — xUnit v3's native type-safe wrapper:

public class TheoryTestData<TTestData> : TheoryData<TTestData>
where TTestData : notnull, ITestData
{
    // Inherits from xUnit v3's TheoryData<T>
    // Adds automatic deduplication via ITheoryTestDataRow
}

Features:

  • xUnit v3 native — Extends TheoryData<T>
  • Automatic deduplication — Uses ITheoryTestDataRow identity
  • Self-documenting names — Via TestDisplayName property
  • Type safety — Generic TTestData instead of raw object?[]

Theory Data Rows

TheoryTestDataRow — Implements ITheoryTestDataRow:

public interface ITheoryTestDataRow : ITheoryDataRow, INamedCase
{
    string? TestDisplayName { get; init; }
}

public class TheoryTestDataRow : ITheoryTestDataRow
{
    public string TestCaseName { get; init; }
    public string? TestDisplayName { get; init; }
    public object?[] GetData() => ...;
}

Key Features:

  • Identity-based equality — Via TestCaseName
  • Custom display names — Via TestDisplayName
  • xUnit v3 integration — Implements ITheoryDataRow

Attributes

PortamicalDataAttribute — xUnit v3-native attribute for test data:

[Theory, PortamicalData(nameof(Args))]
public void MyTest(TestData<int> testData) { ... }

Automatically:

  • ✅ Reads data from TheoryTestData<T> or IEnumerable<ITheoryDataRow>
  • ✅ Sets test case names to "definition => result"
  • ✅ Deduplicates test cases via ITheoryTestDataRow identity
  • ✅ Extracts TestDisplayName for test runner

TestBases

Abstract base classes with Convert() methods:

using Portamical.xUnit_v3.TestBases;

public class MyTests : TestBase
{
    protected static TheoryTestData<TestData<int>> Args
        => Convert(dataSource.GetArgs());
}

Assertions

xUnit v3-specific assertion helpers:

using Portamical.xUnit_v3.Assertions;

PortamicalAssert.ThrowsDetails(
    attempt: () => Sut.Method(null),
    expected: new ArgumentNullException("paramName"));

Validates:

  • ✅ Exception type
  • ✅ Exception message
  • ✅ Parameter name (for ArgumentException)

Data Strategies

Strategy 1: TestData Mode (Pass Entire Object)

// Data source
public static TheoryTestData<TestData<int>> Args
    => Convert(dataSource.GetArgs());  // ← ArgsCode.Instance (default)

// Test signature
[Theory, PortamicalData(nameof(Args))]
public void Test(TestData<int> testData)  // ← Receives object
{
    var actual = Sut.Method(testData.Arg1);
    Assert.Equal(expected, actual);
}

Best for: Tests needing access to TestCaseName or full test data object.


Strategy 2: Properties Mode (Flatten Parameters)

// Data source
public static TheoryTestData<TestData<int>> Args
    => Convert(dataSource.GetArgs(), AsProperties);  // ← ArgsCode.Properties

// Test signature
[Theory, PortamicalData(nameof(Args))]
public void Test(int arg1)  // ← Receives flattened parameter
{
    var actual = Sut.Method(arg1);
    Assert.Equal(expected, actual);
}

Best for: Tests preferring traditional parameter signatures.


Strategy 3: Return-Value Tests with TestDataReturns

using static Portamical.Core.Factories.TestDataFactory;

public class DataSource
{
    public static IEnumerable<TestDataReturns<int, int, int>> AddCases()
    {
        yield return CreateTestDataReturns(
            definition: "adding 2 and 3",
            expected: 5,
            arg1: 2,
            arg2: 3);
    }
}

// Test
public static TheoryTestData<TestDataReturns<int, int, int>> Args
    => Convert(DataSource.AddCases(), AsProperties);

[Theory, PortamicalData(nameof(Args))]
public void Add_validInputs_returnsExpected(int arg1, int arg2, int expected)
{
    var actual = new Calculator().Add(arg1, arg2);
    Assert.Equal(expected, actual);
}

Strategy 4: Exception Tests with TestDataThrows

using static Portamical.Core.Factories.TestDataFactory;

public class DataSource
{
    public static IEnumerable<TestDataThrows<ArgumentNullException, string>> NullArgCases()
    {
        yield return CreateTestDataThrows(
            definition: "name is null",
            expected: new ArgumentNullException("name"),
            arg1: (string?)null);
    }
}

// Test
public static TheoryTestData<TestDataThrows<ArgumentNullException, string>> Args
    => Convert(DataSource.NullArgCases());

[Theory, PortamicalData(nameof(Args))]
public void Constructor_nullName_throwsArgumentNullException(
    TestDataThrows<ArgumentNullException, string> testData)
{
    // Arrange
    var expected = testData.Expected;
    var name = testData.Arg1;

    // Act & Assert
    PortamicalAssert.ThrowsDetails(
        attempt: () => new SomeClass(name!),
        expected: expected);
}

Shared vs. Native Styles

Shared Style (Framework-Agnostic)

Use: Portamical.TestBases.TestDataCollection.TestBase

Benefits:

  • ✅ Same code works in xUnit v2, xUnit v3, MSTest, NUnit
  • ✅ Easy migration between frameworks
  • ✅ Uses standard [MemberData] attribute
  • ✅ Returns IEnumerable<TTestData>

Example:

using Portamical.TestBases.TestDataCollection;

public class MyTests : TestBase
{
    public static IEnumerable<TestData<int>> Args
        => Convert(dataSource.GetArgs());

    [Theory, MemberData(nameof(Args))]
    public void Test(TestData<int> testData) { ... }
}

Native Style (xUnit v3-Specific)

Use: Portamical.xUnit_v3.TestBases.TestBase

Benefits:

  • ✅ xUnit v3-specific optimizations
  • ✅ Type-safe TheoryTestData<T> (extends xUnit v3's TheoryData<T>)
  • PortamicalDataAttribute with automatic deduplication
  • ✅ Better integration with xUnit v3 Test Explorer
  • TestDisplayName for custom test names

Example:

using Portamical.xUnit_v3.Attributes;
using Portamical.xUnit_v3.TestBases;

public class MyTests : TestBase
{
    public static TheoryTestData<TestData<int>> Args
        => Convert(dataSource.GetArgs());

    [Theory, PortamicalData(nameof(Args))]
    public void Test(TestData<int> testData) { ... }
}

xUnit v3 Integration Details

How TheoryTestData<T> Works

public class TheoryTestData<TTestData> : TheoryData<TTestData>
where TTestData : notnull, ITestData
{
    private readonly HashSet<INamedCase> _namedCases = new(NamedCase.Comparer);
    
    public override void Add(TTestData testData)
    {
        if (_namedCases.Add(testData))  // ← Deduplication via identity
        {
            base.Add(testData);  // ← Adds to xUnit v3's TheoryData<T>
        }
    }
}

Key Features:

  1. xUnit v3 native — Extends TheoryData<T> (not a wrapper)
  2. Automatic deduplicationHashSet<INamedCase> with identity comparer
  3. Type-safe — Generic TTestData instead of object?[]
  4. Lazy evaluation — Inherits from xUnit v3's collection

How ITheoryTestDataRow Works

public interface ITheoryTestDataRow : ITheoryDataRow, INamedCase
{
    string? TestDisplayName { get; init; }
}

public class TheoryTestDataRow : ITheoryTestDataRow
{
    private readonly ITestData _testData;
    private readonly ArgsCode _argsCode;
    private readonly PropsCode _propsCode;
    
    public string TestCaseName { get; init; }  // ← From INamedCase
    public string? TestDisplayName { get; init; }  // ← Custom display name
    
    public object?[] GetData()  // ← From ITheoryDataRow
    => _testData.ToArgs(_argsCode, _propsCode);
    
    public bool Equals(INamedCase? other)  // ← Identity-based equality
    => NamedCase.Comparer.Equals(this, other);
}

Key Features:

  1. xUnit v3 contract — Implements ITheoryDataRow
  2. Portamical identity — Implements INamedCase
  3. Custom display namesTestDisplayName property
  4. Lazy serializationGetData() called on demand

How PortamicalDataAttribute Works

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class PortamicalDataAttribute : MemberDataAttribute
{
    public PortamicalDataAttribute(string memberName)
        : base(memberName)
    {
    }
    
    public override ValueTask<IReadOnlyCollection<ITheoryDataRow>?> GetData(
        MethodInfo testMethod,
        DisposalTracker disposalTracker)
    {
        // Get data from member
        var data = base.GetData(testMethod, disposalTracker).Result;
        
        if (data is null) return new(data);
        
        // Deduplicate via ITheoryTestDataRow identity
        HashSet<ITheoryTestDataRow> deduplicatedRows = new(NamedCase.Comparer);
        
        foreach (var row in data)
        {
            if (row is ITheoryTestDataRow ttdr)
            {
                // Set TestDisplayName if not set
                if (string.IsNullOrEmpty(ttdr.TestDisplayName))
                {
                    ttdr = new TheoryTestDataRow(ttdr, testMethod.Name);
                }
                
                deduplicatedRows.Add(ttdr);  // ← Identity-based deduplication
            }
        }
        
        return new(deduplicatedRows.CastOrToReadOnlyCollection());
    }
}

Behavior:

  1. Extends xUnit v3's MemberDataAttribute
  2. Reads data from static property/method
  3. Expects IEnumerable<ITheoryDataRow> or TheoryTestData<T>
  4. Deduplicates via HashSet<ITheoryTestDataRow> (identity-based)
  5. Sets TestDisplayName if not already set
  6. Returns deduplicated IReadOnlyCollection<ITheoryDataRow>

Test Explorer Output

Without PortamicalDataAttribute:

✓ Add_validInputs_returnsSum (testData: TestData<Int32, Int32>)
✓ Add_validInputs_returnsSum (testData: TestData<Int32, Int32>)

With PortamicalDataAttribute:

✓ Add_validInputs_returnsSum(testData: adding two positive numbers => returns their sum)
✓ Add_validInputs_returnsSum(testData: adding with zero => returns the other number)

Self-documenting test names via TestDisplayName!


PropsCode Options

Control which properties are included in test arguments:

PropsCode Includes Use Case
TrimTestCaseName All properties except TestCaseName Default — xUnit v3 provides test naming
All TestCaseName + all properties Custom scenarios needing explicit names
TrimReturnsExpected Also excludes Expected (for IReturns) Return-value tests (extract expected separately)
TrimThrowsExpected Also excludes Expected (for IThrows) Exception tests (extract exception separately)

Example:

// Default: Exclude TestCaseName
var theoryData = Convert(dataSource.GetArgs());  // ← TrimTestCaseName

// Include TestCaseName
var theoryData = Convert(dataSource.GetArgs(), AsProperties, WithTestCaseName);

// Exclude Expected for return-value tests
var theoryData = Convert(dataSource.GetReturnsArgs(), AsProperties, TrimReturnsExpected);

Complete Example: BirthDay Class Tests

Data Source (Framework-Agnostic)

using static Portamical.Core.Factories.TestDataFactory;

public class BirthDayDataSource
{
    public static IEnumerable<TestData<DateOnly>> GetConstructorValidArgs()
    {
        const string result = "creates BirthDay instance";
        
        string definition = "Valid name and dateOfBirth is today";
        DateOnly dateOfBirth = DateOnly.FromDateTime(DateTime.Now);
        yield return createTestData();
        
        definition = "Valid name and dateOfBirth is in the past";
        dateOfBirth = dateOfBirth.AddDays(-1);
        yield return createTestData();
        
        #region Local Methods
        TestData<DateOnly> createTestData()
        => CreateTestData(definition, result, dateOfBirth);
        #endregion
    }
    
    public static IEnumerable<TestDataThrows<ArgumentException, string>> GetConstructorInvalidArgs()
    {
        yield return CreateTestDataThrows(
            definition: "name is null",
            expected: new ArgumentNullException("name"),
            arg1: (string?)null);
        
        yield return CreateTestDataThrows(
            definition: "name is empty",
            expected: new ArgumentException("Value cannot be empty.", "name"),
            arg1: string.Empty);
    }
    
    public static IEnumerable<TestDataReturns<int, DateOnly, BirthDay>> GetCompareToArgs()
    {
        var today = DateOnly.FromDateTime(DateTime.Now);
        
        yield return CreateTestDataReturns(
            definition: "other is null",
            expected: -1,
            arg1: today,
            arg2: (BirthDay?)null);
        
        yield return CreateTestDataReturns(
            definition: "other has earlier dateOfBirth",
            expected: 1,
            arg1: today,
            arg2: new BirthDay("John", today.AddDays(-1)));
    }
}

Test Class (xUnit v3 Native Style)

using Portamical.xUnit_v3.Attributes;
using Portamical.xUnit_v3.Assertions;
using Portamical.xUnit_v3.TestBases;

public class BirthDayTests : TestBase
{
    private static readonly BirthDayDataSource _dataSource = new();
    
    // Constructor - Valid Args
    public static TheoryTestData<TestData<DateOnly>> ConstructorValidArgs
        => Convert(_dataSource.GetConstructorValidArgs());
    
    [Theory, PortamicalData(nameof(ConstructorValidArgs))]
    public void Constructor_validArgs_createsInstance(TestData<DateOnly> testData)
    {
        // Arrange
        const string name = "John Doe";
        var dateOfBirth = testData.Arg1;
        
        // Act
        var actual = new BirthDay(name, dateOfBirth);
        
        // Assert
        Assert.NotNull(actual);
        Assert.Equal(name, actual.Name);
        Assert.Equal(dateOfBirth, actual.DateOfBirth);
    }
    
    // Constructor - Invalid Args
    public static TheoryTestData<TestDataThrows<ArgumentException, string>> ConstructorInvalidArgs
        => Convert(_dataSource.GetConstructorInvalidArgs());
    
    [Theory, PortamicalData(nameof(ConstructorInvalidArgs))]
    public void Constructor_invalidArgs_throwsArgumentException(
        TestDataThrows<ArgumentException, string> testData)
    {
        // Arrange
        var name = testData.Arg1;
        var dateOfBirth = DateOnly.FromDateTime(DateTime.Now);
        var expected = testData.Expected;
        
        // Act & Assert
        PortamicalAssert.ThrowsDetails(
            attempt: () => new BirthDay(name!, dateOfBirth),
            expected: expected);
    }
    
    // CompareTo
    public static TheoryTestData<TestDataReturns<int, DateOnly, BirthDay>> CompareToArgs
        => Convert(_dataSource.GetCompareToArgs());
    
    [Theory, PortamicalData(nameof(CompareToArgs))]
    public void CompareTo_validArgs_returnsExpected(
        TestDataReturns<int, DateOnly, BirthDay> testData)
    {
        // Arrange
        const string name = "John Doe";
        var dateOfBirth = testData.Arg1;
        var other = testData.Arg2;
        var expected = testData.Expected;
        var sut = new BirthDay(name, dateOfBirth);
        
        // Act
        var actual = sut.CompareTo(other);
        
        // Assert
        Assert.Equal(expected, actual);
    }
}

Deduplication in Action

Problem: Duplicate Test Cases

public static IEnumerable<TestData<int>> GetArgs()
{
    // Accidentally added twice (same definition + result)
    yield return CreateTestData("Valid input", "succeeds", 42);
    yield return CreateTestData("Valid input", "succeeds", 99);
    //                           ↑ Same identity!
}

Without Portamical: Both test cases run (duplicate waste).

With Portamical xUnit v3: TheoryTestData<T> deduplicates automatically:

public override void Add(TTestData testData)
{
    if (_namedCases.Add(testData))  // ← HashSet.Add returns false for duplicate
    {
        base.Add(testData);  // ← Only adds if new identity
    }
}

Result: Only one test case runs, avoiding duplicate execution.


Alternative: TheoryDataRow Collection

xUnit v3 supports IEnumerable<ITheoryDataRow> directly:

using Portamical.xUnit_v3.Converters;

public static IEnumerable<ITheoryDataRow> Args
    => testDataCollection.ToTheoryDataRowCollection(
        ArgsCode.Instance,
        testMethodName: nameof(MyTest));

[Theory, MemberData(nameof(Args))]
public void MyTest(TestData<int> testData) { ... }

Benefits:

  • ✅ Direct xUnit v3 integration
  • ✅ Custom ITheoryDataRow implementations
  • ✅ More control over serialization

Note: Use TheoryTestData<T> for simplicity, ITheoryDataRow for advanced scenarios.


xUnit v3 Contract/Model Separation

Portamical.xUnit_v3 has the most complex architecture due to xUnit v3's contract/model separation:

xUnit v3 Architecture:
├── Contracts (Interfaces)
│   ├── ITheoryDataRow          ← xUnit v3 contract
│   └── ITheoryTestDataRow      ← Portamical extension
├── Models (Implementations)
│   ├── TheoryDataRow           ← xUnit v3 model
│   └── TheoryTestDataRow       ← Portamical model
└── Collections
    ├── TheoryData<T>           ← xUnit v3 native
    └── TheoryTestData<T>       ← Portamical extension

Why the complexity?

xUnit v3 separates contracts (interfaces) from models (implementations) for:

  • Extensibility — Custom ITheoryDataRow implementations
  • Testability — Mock interfaces in tests
  • Flexibility — Multiple implementations of same contract

Result: 9 namespaces (most complex adapter).


Use Cases

Install Portamical.xUnit_v3 if you are:

  • ✅ Using xUnit v3 (3.2.2+) for testing
  • ✅ Want strongly-typed, reusable test data
  • ✅ Need cross-framework test data portability
  • ✅ Want self-documenting test names ("definition => result")
  • ✅ Need automatic deduplication of test cases
  • ✅ Prefer xUnit v3's native TheoryData<T> over xUnit v2's wrappers
  • ✅ Want ITheoryTestDataRow identity-based equality

Architecture

Your xUnit v3 Tests
    ↓ depends on
Portamical.xUnit_v3 (Adapter)
    ├── Converters              → ToTheoryTestData(), ToTheoryDataRowCollection()
    ├── DataProviders/
    │   ├── Contracts           → ITheoryTestDataRow
    │   └── Model               → TheoryTestDataRow, TheoryTestData<T>
    ├── Attributes              → PortamicalDataAttribute
    ├── TestBases               → TestBase with Convert() methods
    └── Assertions              → PortamicalAssert
    ↓ depends on
Portamical (Shared Layer)
    ├── Converters              → ToDistinctArray()
    ├── Assertions              → PortamicalAssert base
    └── TestBases               → Framework-agnostic base classes
    ↓ depends on
Portamical.Core (Domain)
    ├── ITestData               → Core abstraction
    ├── TestData<T>             → Test data types
    └── TestDataFactory         → Factory methods

Key Principle: Portamical.Core has zero dependencies on xUnit v3.


Comparison: xUnit v3 vs. Other Frameworks

Feature xUnit v3 xUnit v2 MSTest NUnit
Attribute [MemberData] [MemberData] [DynamicData] [TestCaseSource]
Display Names ITheoryDataRow.TestDisplayName Manual DynamicDataDisplayName TestName
Return Type TheoryData<T> IEnumerable<object?[]> IEnumerable<object?[]> IEnumerable<TestCaseData>
Native Type TheoryTestData<T> TestDataProvider<T> object?[] TestCaseData
Deduplication TheoryTestData<T> TestDataProvider<T> Manual Manual
Type Safety ✅ Generic <T> ✅ Generic <T> ❌ Raw object?[] ⚠️ Wrapper
Contract/Model ✅ Separated ❌ Mixed ❌ N/A ❌ N/A
Complexity ⭐⭐⭐ High ⭐⭐ Moderate ⭐ Simple ⭐⭐ Moderate

Migration from xUnit v2

If you're migrating from xUnit v2:

xUnit v2 (Portamical.xUnit):

using Portamical.xUnit.TestBases;

public static TestDataProvider<TestData<int>> Args
    => Convert(dataSource.GetArgs());

[Theory, PortamicalData(nameof(Args))]
public void Test(TestData<int> testData) { ... }

xUnit v3 (Portamical.xUnit_v3):

using Portamical.xUnit_v3.TestBases;

public static TheoryTestData<TestData<int>> Args
    => Convert(dataSource.GetArgs());

[Theory, PortamicalData(nameof(Args))]
public void Test(TestData<int> testData) { ... }

Differences:

  • Namespace: Portamical.xUnitPortamical.xUnit_v3
  • Type: TestDataProvider<T>TheoryTestData<T>
  • Test method signature: Same
  • Attribute: Same ([PortamicalData])

Migration effort: ~5 minutes per test class (change namespace + data source type).



License and Project Lineage

This project is licensed under the MIT License.

Portamical.xUnit_v3 is the continuation and successor of CsabaDu.DynamicTestData.xUnit.v3 (also MIT-licensed).
CsabaDu.DynamicTestData.xUnit.v3 is considered legacy and is no longer supported; new development happens in Portamical.


Changelog

Version 2.0.0 (2026-03-16)

Breaking Changes (from Portamical 2.0.0 base)

Removed from TestBase:

  • ? IDisposable interface implementation
  • ? Dispose() and Dispose(bool) methods
  • ? ResetLogCounter() method ? use Resolver.ResetLogCounter()
  • ? ArgsCode property with setter

Migration:

- public class MyTests : TestBase
+ public class MyTests : TestBase
  {
-     protected override void Dispose(bool disposing)
-     {
-         ResetLogCounter();  // ? Removed
-         base.Dispose(disposing);
-     }

+     using Portamical.Core.Safety;  // ? Add namespace
+     public void Cleanup()
+     {
+         Resolver.ResetLogCounter();  // ? Direct call
+     }
  }
Non-Breaking Changes

Documentation Added (+1,768 lines)

  • TheoryTestDataRow constructors: 3-level hierarchy, visibility rationale
  • TheoryTestData.Add(): Runtime type validation, deduplication logic
  • TestBase: Stateless delegation patterns
  • CollectionConverter: Conversion pipeline documentation
  • PortamicalDataAttribute: xUnit v3 integration details
  • 20+ code examples, design patterns, migration guides

Code Enhancements

  • TheoryTestDataRow(ITestData, ArgsCode, string?): private protected ? protected (enables external inheritance)
  • TheoryTestData<T>.Add(): Runtime generic type validation with clear error messages
  • TheoryTestDataRow copy constructor: Shallow ? deep copy for Traits (prevents mutation bugs)
  • TestBase.Convert(): New convenience overload without testMethodName parameter

Changed

  • Type Safety

    // Add() now validates generic parameter types:
    data.Add(wrongTypeRow);  
    // v1: Silent bug
    // v2: ArgumentException with clear message
    
  • Memory Safety

    // Copy constructor now deep copies Traits:
    var copy = new TheoryTestDataRow(original, name);
    copy.Traits["Key"].Add("Value");  
    // v1: Modified original (bug)
    // v2: Only modifies copy (safe)
    
  • Extensibility

    // Now possible in external assemblies:
    public class CustomRow : TheoryTestDataRow
    {
        public CustomRow(ITestData data, ArgsCode code, string? name)
        : base(data, code, name)  // ? Now accessible
        { }
    }
    

Migration Required

  • If using IDisposable:

    - Remove: IDisposable inheritance
    - Remove: Dispose() overrides  
    - Remove: ResetLogCounter() calls
    + Add: using Portamical.Core.Safety;
    + Add: Resolver.ResetLogCounter() calls
    
  • If using ArgsCode property:

- ArgsCode = AsProperties;  // ❌ Removed
+ Convert(data, AsProperties);  // ✅ Pass explicitly
  • Stats
    • Documentation: +1,768 lines
    • Code: ~110 lines (60 xUnit_v3, 50 Portamical base)
    • Breaking Changes: 3 (TestBase members removed)
    • Enhancements: 4 (visibility, validation, deep copy, overload)

Version 1.0.0 (2026-03-06)

  • Initial release
  • xUnit v3 (3.2.2+) integration
  • TheoryTestData<T> extending xUnit v3's TheoryData<T>
  • ITheoryTestDataRow with identity-based equality
  • PortamicalDataAttribute with automatic deduplication
  • TestBase with Convert() methods
  • xUnit v3-specific PortamicalAssert
  • Contract/model separation (9 namespaces)

Made by CsabaDu

Portamical: Test data as a domain, not an afterthought.


Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 83 3/16/2026
1.0.0 84 3/9/2026

- Breaking Changes (from Portamical 2.0.0 base: TestBase members removed)
- TheoryTestDataRow(ITestData, ArgsCode, string?): private protected ? protected (enables external inheritance)
- TheoryTestData<T>.Add(): Runtime generic type validation with clear error messages
- TheoryTestDataRow copy constructor: Shallow ? deep copy for Traits (prevents mutation bugs)
- TestBase.Convert(): New convenience overload without testMethodName parameter
- Documentation: +1,768 lines