TestFabric 0.3.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package TestFabric --version 0.3.1
                    
NuGet\Install-Package TestFabric -Version 0.3.1
                    
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="TestFabric" Version="0.3.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TestFabric" Version="0.3.1" />
                    
Directory.Packages.props
<PackageReference Include="TestFabric" />
                    
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 TestFabric --version 0.3.1
                    
#r "nuget: TestFabric, 0.3.1"
                    
#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 TestFabric@0.3.1
                    
#: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=TestFabric&version=0.3.1
                    
Install as a Cake Addin
#tool nuget:?package=TestFabric&version=0.3.1
                    
Install as a Cake Tool

Test Fabric

Introduction

Testing toolkit leveraging Moq and AutoFixture to simplify creating unit and integration tests in .NET projects.

Benefits

  • Fluent API: Chain method calls for readable test setup
  • Type Safety: Compile-time checking of mock configurations
  • Reduced Boilerplate: Less repetitive mock setup code
  • Built-in Patterns: Common interfaces already implemented
  • Moq Integration: Full access to underlying Moq functionality when needed

Requirements

  • .NET Standard 2.0 or higher, .NET Framework 4.6.2 or higher, .NET 5 or higher
  • Moq 4.20.72 or higher
  • AutoFixture 4.18.1 or higher
  • Microsoft.Extensions.Logging.Abstractions 8.0.0 or higher

Installation

dotnet add package TestFabric

License

This project is licensed under the MIT License.

MockBuilder<T>

The MockBuilder<T> is a base class that provides helper methods to provide a fluent interface for configuring mock objects. It supports:

  • Properties: Get-only and read/write property configuration
  • Methods: Action and function method configuration with various parameter counts
  • Async Methods: Async action and function method configuration
  • Try-Get Patterns: Methods that return bool and have out parameters

Basic Usage

You can create a mock builder for an interface.

public interface IUserRepository
{
    User GetById(int id);
    Save(User user);
}

By calling helper methods in configuration methods. Returning the instance provides a fluent interface to the mock builder.

using TestFabric;

public class IUserRepositoryMockBuilder : MockBuilder<IUserRepository>
{
    public IUserRepositoryMockBuilder WithGetById(User user) 
    {
        WithFunction(x => x.GetById(It.IsAny<int>()), user); 
        return this;
    }
    
    public IUserRepositoryMockBuilder WithSave()
    {
        WithAction(x => x.Save(It.IsAny<User>()));
        return this;
    }
}

You can then easily configure your mock in your tests.

[Fact]
public void ShouldSave()
{
    // Arrange
    var user = new User { Id = 1, Name = "John" }; 
    var mockRepository = newUserRepositoryMockBuilder()
        .WithGetById(user)
        .WithSave()
        .Create();
    var service = new UserService(mockRepository.Object);

    // Act
    service.UpdateUser(1, "Jane");

    // Assert
    mockRepository.Verify(x => x.Save(It.IsAny<User>()), Times.Once);
}

MockBuilder Methods

Properties
    // Read/Write Property
    WithProperty(x => x.Name, "Initial Value");
    // Get-only Property with fixed value
    WithPropertyGet(x => x.Id, 42);
    // Get-only Property with delegate
    WithPropertyGet(x => x.Timestamp, () => DateTime.Now);  
Actions (Void Methods)
    // Simple action 
    WithAction(x => x.DoSomething());
    // Action with callback 
    WithAction(x => x.Process(It.IsAny<string>()), (value) => Console.WriteLine(value)); 
    // Async action (Task)
    WithActionAsync(x => x.ProcessAsync(It.IsAny<int>()), (id) => Console.WriteLine($"Processing {id}"));
Functions (Methods with Return Values)
    // Function returning fixed value 
    WithFunction(x => x.Calculate(), 100);
    // Function returning sequence of values 
    WithFunction(x => x.GetNext(), 1, 2, 3, 4);
    // Function with delegate 
    WithFunction(x => x.Transform(It.IsAny<string>()), (input) => input.ToUpper()); 
    // Async function (Task<int>)
    WithFunctionAsync(x => x.ComputeAsync(), () => 42);
Try-Get Pattern
    // Method that returns bool and has out parameter 
    WithTryGet(x => x.TryGetValue(out outValue), true, outValue); 
    // With callback for dynamic out value
    WithTryGet<string, int>(
        x => x.TryParse(It.IsAny<string>(), out outValue), 
        true,
        outValue);

Advanced Examples

Complex Service Testing
public class OrderServiceTests 
{ 
    [Fact] 
    public async Task Should_Process_Order_Successfully() 
    {
        // Arrange 
        var order = new Order { Id = 1, Total = 100.00m }; 
        var logMessages = new List<string>(); 
        var progressValues = new List<string>();  
        var mockRepository = new OrderRepositoryMockBuilder()
            .WithGetById(order)
            .WithSave()
            .Create();
        var mockLogger = new LoggerMockBuilder<OrderService>()
            .WithLog(logMessages)
            .Create();
        var mockProgress = new ProgressMockBuilder<string>()
            .WithReport(msg => progressValues.Add(msg))
            .Create();
    
        var sut = new OrderService(mockRepository.Object, mockLogger.Object);
    
        // Act
        await sut.ProcessOrderAsync(1, mockProgress.Object);
    
        // Assert
        Assert.Contains(logMessages, msg => msg.Contains("Processing order 1"));
        Assert.Contains(progressValues, "Order validated");
        Assert.Contains(progressValues, "Order processed");
        
        mockRepository.Verify(x => x.Save(It.IsAny<Order>()), Times.Once);
    }
}
Testing Error Scenarios
[Fact]
public void Should_Handle_Repository_Exception() 
{ 
    // Arrange 
    var mockRepository = new UserRepositoryMockBuilder()
        .WithGetById(_ => throw new DatabaseException("Connection failed"))
        .Create();
    var sut = new UserService(mockRepository.Object);

    // Act & Assert
    Assert.Throws<DatabaseException>(() => service.GetUser(1));
}

Provided Mock Builders

Test Fabric includes pre-built mock builders for common .NET interfaces:

LoggerMockBuilder<T>

Simplifies mocking ILogger<T> for testing logging behavior.

[Fact]
public void Should_Log_Error_Messages() 
{ 
    // Arrange 
    var logMessages = new List (); 
    var mockLogger = new LoggerMockBuilder<MyService>()
        .WithLog(logMessages)
        .Create();  
    var service = new MyService(mockLogger.Object);

    // Act
    service.ProcessWithError();

    // Assert
    Assert.Contains(logMessages, msg => msg.Contains("[Error]"));
}
ProgressMockBuilder<T>

Mocks IProgress<T> for testing progress reporting functionality.

[Fact]
public async Task Should_Report_Progress() 
{ 
    // Arrange 
    var progressValues = new List<int>(); 
    var mockProgress = new ProgressMockBuilder<int>()
        .WithReport(value => progressValues.Add(value))
        .Create();  
    var processor = new DataProcessor();

    // Act
    await processor.ProcessDataAsync(mockProgress.Object);

    // Assert
    Assert.Equal(new[] { 0, 25, 50, 75, 100 }, progressValues);
}
EqualityComparerMockBuilder<T>

Mocks IEqualityComparer<T> for testing custom equality logic.

[Fact]
public void Should_Use_Custom_Equality() 
{ 
    // Arrange 
    var mockComparer = new EqualityComparerMockBuilder<User>()
        .WithEquals((u1, u2) => u1.Id == u2.Id)
        .WithGetHashCode(u => u.Id.GetHashCode())
        .Create(); 
    var set = new HashSet<User>(mockComparer);
    var user1 = new User { Id = 1, Name = "John" };
    var user2 = new User { Id = 1, Name = "Jane" };

    // Act
    set.Add(user1);
    set.Add(user2);

    // Assert
    Assert.Single(set); // Only one user because they have the same ID
}

Equality Comparers

Test Fabric provides a comprehensive set of equality comparers designed to simplify testing scenarios where precise equality comparisons are needed. These are particularly useful with assertion methods like Assert.Equal that accept custom equality comparers.

Object Equality Comparer

Base class for creating custom object comparers with specific equality rules.

// Custom object comparer implementation
public class UserIdComparer : ObjectEqualityComparer<User>
{
    protected override bool EqualsImpl(User x, User y)
    {
        return x.Id == y.Id;
    }
}

// Usage in assertions
var comparer = new UserIdComparer();
var user1 = new User { Id = 1, Name = "John", Email = "john@test.com" };
var user2 = new User { Id = 1, Name = "Jane", Email = "jane@test.com" };

Assert.Equal(user1, user2, comparer); // true - same ID despite different names/emails

// Use cases:
[Fact]
public void Should_Find_User_By_Id_Regardless_Of_Other_Properties()
{
    // Arrange
    var repository = new UserRepository();    
    var expectedUser = new User { Id = 42, Name = "John Doe", Email = "john.doe@example.com" };
    
    // Act
    var foundUser = repository.FindById(42);
    
    // Assert - Compare only by ID, ignoring other properties
    Assert.Equal(expectedUser, foundUser, new UserIdComparer());
}

Compare Factory

The Compare class provides a factory for creating commonly used equality comparers:

Double Comparers
Absolute Double Comparer

Compares double values within an absolute tolerance. Perfect for testing floating-point calculations where you need to account for precision errors.

// Factory method
IEqualityComparer<double> comparer = Compare.DoubleAbsolute(0.01);

// Usage in assertions
Assert.Equal(1.001, 0.999, Compare.DoubleAbsolute(0.01)); // true - difference is 0.002, within tolerance
Assert.Equal(3.14159, 3.14, Compare.DoubleAbsolute(0.002)); // true - difference is 0.00159, within tolerance
Assert.Equal(10.0, 9.5, Compare.DoubleAbsolute(0.1)); // false - difference is 0.5, exceeds tolerance

// Use cases:
[Fact]
public void Should_Calculate_Area_With_Precision()
{
    // Arrange
    var calculator = new CircleAreaCalculator();
    
    // Act
    double result = calculator.CalculateArea(radius: 5.0);
    
    // Assert - Account for floating-point precision errors
    Assert.Equal(78.54, result, Compare.DoubleAbsolute(0.01));
}

When to use:

  • Testing mathematical calculations with known precision requirements
  • Comparing results from different calculation methods that should be equivalent
  • Unit testing scientific or engineering calculations
Relative Double Comparer

Compares double values based on relative percentage difference. Ideal when the tolerance should scale with the size of the values being compared.

// Factory method
IEqualityComparer<double> comparer = Compare.DoubleRelative(0.05); // 5% tolerance

// Usage in assertions
Assert.Equal(100.0, 102.0, Compare.DoubleRelative(0.05)); // true - 2% difference, within 5% tolerance
Assert.Equal(1000.0, 1030.0, Compare.DoubleRelative(0.05)); // true - 3% difference, within 5% tolerance
Assert.Equal(10.0, 12.0, Compare.DoubleRelative(0.05)); // false - 20% difference, exceeds 5% tolerance

// Use cases:
[Fact]
public void Should_Calculate_Growth_Rate_Within_Expected_Range()
{
    // Arrange
    var analyzer = new PerformanceAnalyzer();
    var baselineMetric = 1000.0;
    
    // Act
    double actualGrowth = analyzer.CalculateGrowthRate(baselineMetric);
    double expectedGrowth = 1050.0; // Expected 5% growth
    
    // Assert - Allow 2% variance in growth calculation
    Assert.Equal(expectedGrowth, actualGrowth, Compare.DoubleRelative(0.02));
}

When to use:

  • Testing percentage-based calculations (growth rates, discounts, interest)
  • Comparing scaled values where absolute differences would be misleading
  • Performance metrics that should be proportionally similar
Collection Comparers
Array Equality Comparer

Compares arrays element-by-element, supporting custom element comparers.

// Basic array comparison
var comparer = new ArrayEqualityComparer<int>();
Assert.Equal(new[] { 1, 2, 3 }, new[] { 1, 2, 3 }, comparer); // true

// Array comparison with custom element comparer
var doubleArrayComparer = new ArrayEqualityComparer<double>(Compare.DoubleAbsolute(0.01));
Assert.Equal(
    new[] { 1.001, 2.002, 3.003 }, 
    new[] { 1.000, 2.000, 3.000 }, 
    doubleArrayComparer); // true

// Use cases:
[Fact]
public void Should_Process_Data_Arrays_Correctly()
{
    // Arrange
    var processor = new DataProcessor();
    var input = new[] { 1.1, 2.2, 3.3 };
    var expected = new[] { 2.2, 4.4, 6.6 }; // Input doubled
    
    // Act
    double[] result = processor.DoubleValues(input);
    
    // Assert - Account for floating-point precision in array comparison
    Assert.Equal(expected, result, new ArrayEqualityComparer<double>(Compare.DoubleAbsolute(0.001)));
}
List Equality Comparer

Similar to array comparer but for IList<T> collections.

// Basic list comparison
var comparer = new ListEqualityComparer<string>();
Assert.Equal(
    new List<string> { "a", "b", "c" }, 
    new List<string> { "a", "b", "c" }, 
    comparer); // true

// List comparison with custom element comparer
var doubleListComparer = new ListEqualityComparer<double>(Compare.DoubleRelative(0.01));
Assert.Equal(
    new List<double> { 100.0, 200.0 },
    new List<double> { 101.0, 198.0 },
    doubleListComparer); // true (within 1% relative tolerance)

// Use cases:
[Fact]
public void Should_Filter_And_Transform_Lists_Correctly()
{
    // Arrange
    var service = new DataTransformationService();
    var input = new List<decimal> { 10.5m, 20.7m, 30.1m };
    var expected = new List<decimal> { 21.0m, 41.4m, 60.2m };
    
    // Act
    List<decimal> result = service.DoubleAndRound(input);
    
    // Assert
    Assert.Equal(expected, result, new ListEqualityComparer<decimal>());
}

Equality Comparer Builder

The EqualityComparerBuilder<T> provides a fluent API for creating custom equality comparers with fine-grained control over how objects are compared. It's particularly useful for testing scenarios where you need to compare complex objects with specific equality rules.

The builder allows you to:

  • Configure custom comparers for specific types
  • Ignore specific members during comparison
  • Set custom comparison logic for individual properties/fields
  • Enable detailed tracing for debugging comparison failures
  • Handle collections (arrays, lists) automatically

The Equality Comparer Builder provides the flexibility to create precise comparison logic that matches your testing needs, making assertions more reliable and maintainable.

Simple Object Comparison
[Fact]
public void Should_Compare_Users_By_Custom_Logic()
{
    // Arrange
    var comparer = new EqualityComparerBuilder<User>()
        .Create();
    
    var user1 = new User { Id = 1, Name = "John", Email = "john@test.com" };
    var user2 = new User { Id = 1, Name = "John", Email = "john@test.com" };
    
    // Act & Assert
    Assert.Equal(user1, user2, comparer);
}
Ignoring Specific Members
[Fact]
public void Should_Ignore_Timestamp_During_Comparison()
{
    // Arrange
    var comparer = new EqualityComparerBuilder<LogEntry>()
        .IgnoreMember(x => x.Timestamp)
        .IgnoreMember(x => x.Id)
        .Create();
    
    var entry1 = new LogEntry 
    { 
        Id = 1, 
        Message = "Error occurred", 
        Level = LogLevel.Error,
        Timestamp = DateTime.Now 
    };
    var entry2 = new LogEntry 
    { 
        Id = 2, 
        Message = "Error occurred", 
        Level = LogLevel.Error,
        Timestamp = DateTime.Now.AddMinutes(5) 
    };
    
    // Act & Assert - Same despite different IDs and timestamps
    Assert.Equal(entry1, entry2, comparer);
}
Custom Member Comparers

Configure specific comparison logic for individual properties or fields:

[Fact]
public void Should_Use_Custom_Member_Comparers()
{
    // Arrange
    var comparer = new EqualityComparerBuilder<Product>()
        .ConfigureMember(x => x.Price, Compare.DoubleAbsolute(0.01))
        .ConfigureMember(x => x.Name, (n1, n2) => string.Equals(n1, n2, StringComparison.OrdinalIgnoreCase))
        .Create();
    
    var product1 = new Product 
    { 
        Name = "Widget", 
        Price = 19.99,
        Category = "Tools" 
    };
    var product2 = new Product 
    { 
        Name = "WIDGET", 
        Price = 19.991,
        Category = "Tools" 
    };
    
    // Act & Assert - Equal despite case difference and minor price variance
    Assert.Equal(product1, product2, comparer);
}
Type-Level Configuration

Configure comparison logic for entire types:

[Fact]
public void Should_Use_Custom_Type_Comparers()
{
    // Arrange
    var comparer = new EqualityComparerBuilder<Order>()
        .ConfigureType<DateTime>((d1, d2) => d1.Date == d2.Date) // Compare dates only, ignore time
        .ConfigureType<decimal>(Compare.DoubleAbsolute(0.01))     // Custom decimal precision
        .Create();
    
    var order1 = new Order 
    { 
        OrderDate = new DateTime(2023, 1, 15, 10, 30, 0),
        Total = 99.99m 
    };
    var order2 = new Order 
    { 
        OrderDate = new DateTime(2023, 1, 15, 16, 45, 0),
        Total = 99.991m 
    };
    
    // Act & Assert - Equal despite different times and minor amount difference
    Assert.Equal(order1, order2, comparer);
}
Ignoring Types Completely
[Fact]
public void Should_Ignore_Audit_Information()
{
    // Arrange
    var comparer = new EqualityComparerBuilder<Document>()
        .IgnoreType<AuditInfo>()  // Ignore all audit info objects
        .Create();
    
    var doc1 = new Document 
    { 
        Title = "Report",
        Content = "Important data",
        Audit = new AuditInfo { CreatedBy = "user1", CreatedAt = DateTime.Now }
    };
    var doc2 = new Document 
    { 
        Title = "Report",
        Content = "Important data", 
        Audit = new AuditInfo { CreatedBy = "user2", CreatedAt = DateTime.Now.AddDays(1) }
    };
    
    // Act & Assert - Equal despite different audit info
    Assert.Equal(doc1, doc2, comparer);
}
Collection Handling

The builder automatically handles arrays and lists:

[Fact]
public void Should_Compare_Collections_With_Custom_Element_Logic()
{
    // Arrange
    var comparer = new EqualityComparerBuilder<OrderSummary>()
        .ConfigureType<decimal>(Compare.DoubleAbsolute(0.01))
        .Create();
    
    var summary1 = new OrderSummary
    {
        ItemPrices = new[] { 10.99m, 25.00m, 5.99m },
        Categories = new List<string> { "Electronics", "Books", "Clothing" }
    };
    var summary2 = new OrderSummary
    {
        ItemPrices = new[] { 10.991m, 24.999m, 5.991m },
        Categories = new List<string> { "Electronics", "Books", "Clothing" }
    };
    
    // Act & Assert - Arrays and lists compared with custom decimal precision
    Assert.Equal(summary1, summary2, comparer);
}
Debugging with Tracing

Enable tracing to understand why comparisons fail:

[Fact]
public void Should_Trace_Comparison_Process()
{
    // Arrange
    var traceOutput = new List<string>();
    var comparer = new EqualityComparerBuilder<ComplexObject>()
        .ConfigureMember(x => x.Value, Compare.DoubleAbsolute(0.001))
        .EnableTracing(line => traceOutput.Add(line), detailed: true)
        .Create();
    
    var obj1 = new ComplexObject { Id = 1, Value = 1.001, Name = "Test" };
    var obj2 = new ComplexObject { Id = 2, Value = 1.002, Name = "Test" };
    
    // Act
    bool areEqual = comparer.Equals(obj1, obj2);
    
    // Assert
    Assert.False(areEqual);
    
    // Examine trace output to understand the failure
    Assert.Contains(traceOutput, line => line.Contains("Check equality of ComplexObject"));
    Assert.Contains(traceOutput, line => line.Contains("member comparison failed"));
}

Advanced Scenarios

Combining Multiple Comparers
[Fact]
public void Should_Compare_Complex_Calculation_Results()
{
    // Arrange
    var calculator = new ScientificCalculator();
    var inputValues = new[] { 1.0, 2.0, 3.0 };
    
    // Expected results with some floating-point tolerance
    var expectedResults = new[] { 2.718, 7.389, 20.086 }; // e^x calculations
    
    // Act
    double[] results = calculator.CalculateExponentials(inputValues);
    
    // Assert - Use array comparer with absolute tolerance for exponential calculations
    var comparer = new ArrayEqualityComparer<double>(Compare.DoubleAbsolute(0.001));
    Assert.Equal(expectedResults, results, comparer);
}

[Fact]
public void Should_Handle_Mixed_Data_Types_In_Collections()
{
    // Arrange
    var processor = new MixedDataProcessor();
    var expectedAmounts = new List<decimal>() { 1.3m, 3.4m, 2.7m };
    var expectedRates = new[] { 0.1, 0.07, 0.12 };
    var expectedAccounts = new List<Account>() { new Account(123), new Account(2453) };
    
    // Act
    var results = processor.ProcessFinancialData();
    
    // Assert different aspects with appropriate comparers
    Assert.Equal(
        expectedAmounts,
        results.Amounts, 
        new ListEqualityComparer<decimal>()); // Exact decimal comparison
    
    Assert.Equal(
        expectedRates,
        results.InterestRates, 
        new ArrayEqualityComparer<double>(Compare.DoubleRelative(0.001))); // Relative comparison for rates
    
    Assert.Equal(
        expectedAccounts, 
        results.Accounts, 
        new ListEqualityComparer<Account>(new AccountNumberComparer())); // Custom object comparison
}

Best Practices

  1. Choose the Right Tolerance:

    • Use absolute comparers for small numbers or when you know the expected precision
    • Use relative comparers for large numbers or percentage-based calculations
  2. Combine with Collection Comparers:

    • Wrap element comparers in array/list comparers for collection assertions
  3. Create Custom Object Comparers:

    • Extend ObjectEqualityComparer<T> for domain-specific equality rules
  4. Document Your Tolerances:

    • Always comment why you chose specific tolerance values in your tests

Test Suites

Test Fabric provides test suite base classes that give you access to powerful data generation capabilities through built-in data factories. These test suites simplify creating test data and provide a consistent approach to randomized testing.

TestSuite.Normal

The most important test suite to use is TestSuite.Normal, which provides access to a built-in data factory configured with standard settings. This is the recommended base class for most testing scenarios.

public class PersonServiceTests : TestSuite.Normal
{
    [Fact]
    public void Should_Create_Person_With_Valid_Data()
    {
        // Arrange - Use built-in data factory to create test data
        var expectedPerson = Factory.Create<Person>();
        var service = new PersonService();

        // Act
        var result = service.CreatePerson(expectedPerson.Name, expectedPerson.Age);

        // Assert
        Assert.Equal(expectedPerson.Name, result.Name);
        Assert.Equal(expectedPerson.Age, result.Age);
    }

    [Fact] 
    public void Should_Handle_Multiple_People()
    {
        // Arrange - Create multiple test objects
        var people = Factory.CreateMany<Person>(5).ToList();
        var service = new PersonService();

        // Act
        var results = service.ProcessPeople(people);

        // Assert
        Assert.Equal(people.Count, results.Count);
    }
}

Randomized Data Configuration

The data factory in TestSuite classes generates randomized data that helps discover edge cases and makes your tests more robust. You can configure constraints on the generated data to match your testing needs.

public class ProductTests : TestSuite.Normal
{
    [Fact]
    public void Should_Calculate_Discount_For_Various_Prices()
    {
        // Arrange - Generate products with constrained price ranges
        var expensiveProduct = Factory.Build<Product>()
            .With(p => p.Price, Factory.CreateFromRange(100d, 1000d))
            .Create();
            
        var cheapProduct = Factory.Build<Product>()
            .With(p => p.Price, Factory.CreateFromRange(5d, 15d))
            .Create();
            
        var calculator = new DiscountCalculator();

        // Act & Assert
        var expensiveDiscount = calculator.CalculateDiscount(expensiveProduct);
        var cheapDiscount = calculator.CalculateDiscount(cheapProduct);
        
        Assert.True(expensiveDiscount > cheapDiscount);
    }
}

Constrained Building

Use the BuildConstrained<T>() method to create objects that should be selected from a range or list of valid values.

public class UserValidationTests : TestSuite.Normal
{
    [Fact]
    public void Should_Validate_User_Age()
    {
        // Arrange - Create user with constrained email format
        var validAge = Factory.BuildConstrained<int>()
            .AddOptions(2,7,13)
            .AddRange(new NumberRange<int>(20, 30))
            .Create();            
        var validator = new UserValidator();

        // Act
        var result = validator.ValidateAge(validAge);

        // Assert
        Assert.True(result.IsValid);
    }
}

Test data generation methods

The TestSuite<TDataFactoryBuilder> class provides three convenient methods for generating test data in your unit tests:

Random<T>()

Generates a completely random object of the specified type with all properties populated with random values.

// Generate random objects
 var user = Random<User>(); 
 var number = Random<int>(); 
 var date = Random<DateTimeOffset>();

Use this when: You need test data but don't care about specific values, just that all properties are populated.

InRange<T>(T minInclusive, T maxExclusive)

Generates a random value within a specific numeric range. The minimum value is included, but the maximum value is excluded.

// Generate values within ranges
var age = InRange(18, 65);           // Age between 18-64
var price = InRange(10.0, 100.0);    // Price between 10.00-99.99
var year = InRange(2020, 2025);      // Year between 2020-2024

Use this when: You need random data that falls within realistic or valid bounds for your business logic.

InRange<T>(IEnumerable<T> items)

Randomly selects one item from a predefined collection of valid options.

// Pick from predefined options
var colors = new[] { "Red", "Green", "Blue", "Yellow" };
var randomColor = InRange(colors);

var validEmails = new[] { "test@example.com", "user@domain.org" };
var email = InRange(validEmails);

Use this when: You have a specific set of valid values and need to randomly pick one for testing.

Available TestSuite Types

Test Fabric provides several pre-configured test suite types:

  • TestSuite.Normal: Standard configuration with randomization
  • TestSuite.WithRecursion: Configured to handle recursive object creation
// For most scenarios
public class StandardTests : TestSuite.Normal { }

// For complex object graphs with potential recursion
public class ComplexObjectTests : TestSuite.WithRecursion { }

Creating Custom TestSuite Classes

You can create your own custom test suite by implementing a custom data factory builder and configuring the AutoFixture settings to meet your specific needs.

// Step 1: Create a custom data factory builder
public class CustomDataFactoryBuilder : IFactoryBuilder
{
    public IFactory Create()
    {
        var fixture = new Fixture();
        
        // Configure custom behaviors
        fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
            .ForEach(b => fixture.Behaviors.Remove(b));
        fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2));
        
        // Add custom specimen builders
        fixture.Customizations.Add(new EmailAddressGenerator());
        fixture.Customizations.Add(new CustomDateTimeGenerator());
        
        // Configure specific types
        fixture.Customize<User>(composer =>
            composer
                .With(u => u.IsActive, true)
                .With(u => u.CreatedDate, DateTime.Now.AddDays(-30))
                .Without(u => u.Password) // Don't generate passwords
        );
        
        return new Factory(fixture, null, new DoublePicker(), new IntPicker());
    }
}

// Step 2: Create your custom test suite
public class CustomTestSuite : TestSuite<CustomDataFactoryBuilder>
{
    // Add any additional helper methods specific to your testing needs
    protected User CreateValidUser()
    {
        return Factory.BuildConstrained<User>()
            .With(u => u.Email, Random<string>() + "@business.com")
            .With(u => u.Age, InRange(18, 120))
            .Create();
    }
    
    protected Product CreateProductInCategory(string category)
    {
        return Factory.BuildConstrained<Product>()
            .With(p => p.Category, category)
            .With(p => p.Price, Factory.CreateFromRange(1d, 1000d))
            .Create();
    }
}

// Step 3: Use your custom test suite
public class AdvancedUserTests : CustomTestSuite
{
    [Fact]
    public void Should_Process_Valid_Users()
    {
        // Arrange - Use your custom helper methods
        var user = CreateValidUser();
        var service = new UserService();

        // Act
        var result = service.ProcessUser(user);

        // Assert
        Assert.True(result.Success);
        Assert.True(user.IsActive);
        Assert.True(user.CreatedDate < DateTime.Now);
    }
    
    [Fact]
    public void Should_Handle_Electronics_Category()
    {
        // Arrange
        var product = CreateProductInCategory("Electronics");
        var service = new ProductService();

        // Act
        var result = service.CategorizeProduct(product);

        // Assert
        Assert.Equal("Electronics", result.Category);
        Assert.True(1d <= product.Price);
        Assert.True(product.Price < 1000d);
    }
}

Best Practices

  1. Use TestSuite.Normal for most scenarios: It provides a good balance of randomization and performance.
  2. Constrain your data appropriately: Use BuildConstrained<T>() to ensure generated data meets your business rules.
  3. Leverage randomization: Let the data factory generate varied test data to help discover edge cases.
  4. Create domain-specific test suites: For complex domains, create custom test suites with domain-specific helper methods.
  5. Configure recursion handling: Use TestSuite.WithRecursion or custom builders when dealing with complex object graphs.
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
0.3.3 131 8/11/2025
0.3.2 130 8/10/2025
0.3.1 115 8/10/2025
0.3.0 136 8/8/2025
0.2.0 196 8/7/2025
0.1.1 196 8/6/2025
0.1.0 197 8/6/2025