Testing Validation Rules in FluentValidation

Data validation in .NET applications is crucial for ensuring software quality and security. FluentValidation, combined with XUnit, offers efficient tools for defining and testing validation rules. In this article, we will discuss how to test validation rules using these technologies, focusing on specific examples.

Let's start with a simple validator for the User model, which requires the user to provide their name and email address. Validation rules such as NotEmpty for the name and EmailAddress for the email field help ensure that the input data is both complete and correct.

A sample validator might look like this:

public class UserValidator : AbstractValidator<User> {
    public UserValidator() {
        RuleFor(user => user.Name).NotEmpty().WithMessage("Name is required.");
        RuleFor(user => user.Email).EmailAddress().WithMessage("Invalid email address.");
    }
}

Testing Validation Rules

Testing a validator requires a thoughtful approach, where each unit test focuses on a specific aspect of validation. For instance, we can check if the Name field is not empty by initializing a User object with an empty name, running the validator, and checking if the validator correctly identified the error. The following test checks if the validator identifies an empty Name as an error:

[Fact]
public void Should_have_error_when_Name_is_empty() {
    // Arrange
    var validator = new UserValidator();
    var user = new User { Name = string.Empty, Email = "test@example.com" };

    // Act
    var result = validator.TestValidate(user);

    // Assert
    result.ShouldHaveValidationErrorFor(user => user.Name).WithErrorMessage("Name is required.");
}

In this test, we create an instance of the validator, define a user with an empty name, run the validator, and use the ShouldHaveValidationErrorFor method to check if the validator returned the expected validation error.

The TestValidate method in FluentValidation is used to run the validator for testing purposes, allowing assertions on the validation result. It is a useful tool for testing that allows treating validators as "black boxes"—we provide input data and check if the validation results are correct. The TestValidate method also has an asynchronous counterpart, TestValidateAsync, which works similarly but returns a Task, which is useful in asynchronous tests. With TestValidate, developers can easily and effectively verify the behavior of validation rules in their applications.

It is equally important to ensure that the validator does not generate errors for valid data. The example below shows how to ensure that a valid email address does not cause validation errors:

[Fact]
public void Should_not_have_error_when_Email_is_valid() {
    // Arrange
    var validator = new UserValidator();
    var user = new User { Name = "Jan Kowalski", Email = "valid@example.com" };

    // Act
    var result = validator.TestValidate(user);

    // Assert
    result.ShouldNotHaveValidationErrorFor(user => user.Email);
}

This test initializes the validator and a user with a valid email address, then uses the ShouldNotHaveValidationErrorFor method to ensure that no validation error appears for the Email field.

The Only Method

The Only method in FluentValidation is used to ensure that no other validation errors occur besides those specified in the conditions. For example:

[Fact]
public void Should_have_error_when_Name_is_empty() {
    // Arrange
    var validator = new UserValidator();
    var user = new User { Name = string.Empty, Email = "test@example.com" };

    // Act
    var result = validator.TestValidate(user);

    // Assert
    result.ShouldHaveValidationErrorFor(user => user.Name).WithErrorMessage("Name is required.").Only();
}

The Only method checks that the only validation errors are those we specified in the conditions. If other errors appear, the test will fail. This is useful when we want to ensure that our validation returns exactly the errors we expect, without any additional surprises.

Conclusion

Testing validation rules using FluentValidation and XUnit allows developers to precisely check and ensure the quality of input data in an application. Methods like ShouldHaveValidationErrorFor and ShouldNotHaveValidationErrorFor are key to ensuring that validators work correctly, detect errors where they should, and do not generate false positives. This helps ensure the reliability and security of our applications.

See you in the next posts.