SuperAPI AI Directory

dotnet-testing-fluentvalidation-testing

Testing & quality

View repository

Updated 10-15 23:45

About this skill

FluentValidation Validator Testing Guide

Why Test Validators?

Validators are the first line of defense for an application. Testing validators can:

  1. Ensure data integrity - Prevent invalid data from entering the system
  2. Document business rules - Tests are living documentation that clearly demonstrate business rules
  3. Security assurance - Prevent malicious or improper data input
  4. Refactoring safety net - Provide protection when business rules change
  5. Cross-field logic validation - Ensure complex logic works correctly

Prerequisites

Package Installation

<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.4.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="AwesomeAssertions" Version="9.4.0" />

Note:FluentValidation.TestHelper The namespace (TestValidate、ShouldHaveValidationErrorFor and other APIs) is already included in the FluentValidation main package, so there is no need to install a separate standalone package. You only need using FluentValidation.TestHelper; to use it.

FluentValidation 12.x Notes: FluentValidation 12.0 is a major version upgrade, with a minimum requirement of .NET 8. Removed APIs include Transform/TransformForEach(use Must + manual conversion instead),InjectValidator(use constructor injection + SetValidator)、CascadeMode.StopOnFirstFailure(use RuleLevelCascadeMode = CascadeMode.Stop)。ShouldHaveAnyValidationError has been renamed to ShouldHaveValidationErrors. For the complete migration guide, see FluentValidation 12.0 Upgrade Guide。

Basic using Directives

using FluentValidation;
using FluentValidation.TestHelper;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
using AwesomeAssertions;

Core Testing Patterns

This section covers 7 core testing patterns, each including a validator definition and a complete test example.

For complete code examples, see references/core-test-patterns.md

  • Pattern 1: Basic Field Validation — Use TestValidate + ShouldHaveValidationErrorFor / ShouldNotHaveValidationErrorFor to test a single field rule
  • Pattern 2: Parameterized Testing — Use [Theory] + [InlineData] to test multiple invalid/valid input combinations
  • Pattern 3: Cross-Field Validation — Multi-field related validation such as password confirmation and custom Must() rules
  • Pattern 4: Time-Dependent Validation — Inject TimeProvider, and use FakeTimeProvider to control time for testing
  • Pattern 5: Conditional Validation — Optional field validation using .When() , testing condition-triggered and skipped scenarios
  • Pattern 6: Asynchronous Validation — MustAsync + TestValidateAsync, with NSubstitute Mock external services
  • Pattern 7: Collection Validation — Validate that a collection is non-empty and that its elements are valid

Quick Example: Basic Field Validation

public class UserValidatorTests
{
    private readonly UserValidator _validator = new();

    [Fact]
    public void Validate_空白使用者名稱_應該驗證失敗()
    {
        var result = _validator.TestValidate(
            new UserRegistrationRequest { Username = "" });

        result.ShouldHaveValidationErrorFor(x => x.Username)
              .WithErrorMessage("使用者名稱不可為 null 或空白");
    }
}

FluentValidation.TestHelper Core API

Test Methods

Method Purpose Example
TestValidate(model) Run synchronous validation _validator.TestValidate(request)
TestValidateAsync(model) Run asynchronous validation await _validator.TestValidateAsync(request)

Assertion Methods

Method Purpose Example
ShouldHaveValidationErrorFor(x => x.Property) Assert that the property should have an error result.ShouldHaveValidationErrorFor(x => x.Username)
ShouldNotHaveValidationErrorFor(x => x.Property) Assert that the property should not have an error result.ShouldNotHaveValidationErrorFor(x => x.Email)
ShouldNotHaveAnyValidationErrors() Assert that the entire object has no errors result.ShouldNotHaveAnyValidationErrors()

Error Message Validation

Method Purpose Example
WithErrorMessage(string) Verify error message content .WithErrorMessage("使用者名稱不可為空")
WithErrorCode(string) Verify error code .WithErrorCode("NOT_EMPTY")

Testing Best Practices

Recommended Practices

  1. Use parameterized tests - Use Theory to test multiple input combinations
  2. Test boundary values - Pay special attention to boundary conditions
  3. Control time - Use FakeTimeProvider to handle time dependencies
  4. Mock external dependencies - Use NSubstitute to isolate external services
  5. Create helper methods - Manage test data in a unified way
  6. Clear test naming - Use the 方法_情境_預期結果 format
  7. Test error messages - Ensure users see the correct error messages

Practices to Avoid

  1. Avoid using DateTime.Now - It causes unstable tests
  2. Avoid over-coupling tests - Each test should verify only one rule
  3. Avoid hardcoding test data - Create it using helper methods
  4. Avoid ignoring boundary conditions - Boundary values are where errors are most likely
  5. Avoid skipping error message verification - Error messages are part of the user experience

Common Test Scenarios

Scenario 1: Email format validation

[Theory]
[InlineData("", "電子郵件不可為 null 或空白")]
[InlineData("invalid", "電子郵件格式不正確")]
[InlineData("@example.com", "電子郵件格式不正確")]
public void Validate_無效Email_應該驗證失敗(string email, string expectedError)
{
    var request = new UserRegistrationRequest { Email = email };
    var result = _validator.TestValidate(request);
    result.ShouldHaveValidationErrorFor(x => x.Email).WithErrorMessage(expectedError);
}

Scenario 2: Age range validation

[Theory]
[InlineData(17, "年齡必須大於或等於 18 歲")]
[InlineData(121, "年齡必須小於或等於 120 歲")]
public void Validate_無效年齡_應該驗證失敗(int age, string expectedError)
{
    var request = new UserRegistrationRequest { Age = age };
    var result = _validator.TestValidate(request);
    result.ShouldHaveValidationErrorFor(x => x.Age).WithErrorMessage(expectedError);
}

Scenario 3: Required field validation

[Fact]
public void Validate_未同意條款_應該驗證失敗()
{
    var request = new UserRegistrationRequest { AgreeToTerms = false };
    var result = _validator.TestValidate(request);
    result.ShouldHaveValidationErrorFor(x => x.AgreeToTerms)
          .WithErrorMessage("必須同意使用條款");
}

Test Helper Tools

Test data builder

public static class TestDataBuilder
{
    public static UserRegistrationRequest CreateValidRequest()
    {
        return new UserRegistrationRequest
        {
            Username = "testuser123",
            Email = "test@example.com",
            Password = "TestPass123",
            ConfirmPassword = "TestPass123",
            BirthDate = new DateTime(1990, 1, 1),
            Age = 34,
            PhoneNumber = "0912345678",
            Roles = new List<string> { "User" },
            AgreeToTerms = true
        };
    }

    public static UserRegistrationRequest WithUsername(this UserRegistrationRequest request, string username)
    {
        request.Username = username;
        return request;
    }

    public static UserRegistrationRequest WithEmail(this UserRegistrationRequest request, string email)
    {
        request.Email = email;
        return request;
    }
}

// 使用範例
var request = TestDataBuilder.CreateValidRequest()
                            .WithUsername("newuser")
                            .WithEmail("new@example.com");

Integration with Other Skills

This skill can be used in combination with the following skills:

  • unit-test-fundamentals: Unit testing fundamentals and the 3A pattern
  • test-naming-conventions: Test naming conventions
  • nsubstitute-mocking: Mocking external service dependencies
  • test-data-builder-pattern: Building complex test data
  • datetime-testing-timeprovider: Time-dependent testing

Troubleshooting

Q1: How do I test validation that requires a database query?

A: Use Mock to isolate the database dependency:

_mockUserService.IsUsernameAvailableAsync("username")
                .Returns(Task.FromResult(false));

Q2: How do I handle time-related validation?

A: Use FakeTimeProvider to control time:

_fakeTimeProvider.SetUtcNow(new DateTime(2024, 1, 1));

Q3: How do I test complex cross-field validation?

A: Test each condition separately to ensure complete coverage:

// 測試生日已過的情況
// 測試生日未到的情況
// 測試邊界日期

Q4: To what extent should I test?

A: Focus testing on:

  • At least one test per validation rule
  • Boundary values and special cases
  • Correctness of error messages
  • All combinations of cross-field logic

Template File Reference

This skill provides the following template files:

  • templates/validator-test-template.cs: Complete validator test example
  • templates/async-validator-examples.cs: Asynchronous validation example

Output Format

  • Generate a Validator test class (including TestHelper setup)
  • Use ShouldHaveValidationErrorFor/ShouldNotHaveValidationErrorFor assertions
  • Include examples of asynchronous validation and cross-field logic testing
  • Provide .csproj package references (FluentValidation — TestHelper API already included)

Reference Resources

Original Article

The content of this skill is distilled from the article series "Old-School Software Engineer's Testing Training - 30-Day Challenge":

Official Documentation

Related Skills

  • unit-test-fundamentals - Unit Testing Fundamentals
  • nsubstitute-mocking - Test Doubles and Mocking