Search code examples
c#xunitfluentvalidation

Is there a way to reuse tests in xUnit?


Let's say that I have a type called Widget.

Widget

public class Widget
{
    public int? Id { get; set; }
    public string? Name { get; set; }
    public List<Item>? Items { get; set; }
}

I have written a validator for this using FluentValidation called WidgetValidator, and a suite of tests for this called WidgetValidatorTests.

WidgetValidatorTests

public class WidgetValidatorTests
{
    // the tests
}

Widget instances are used in multiple places within the application. I also have a WidgetInventory class.

WidgetInventory

public class WidgetInventory
{
    public int? Id { get; set; }
    public DateTime? StartDate { get; set; }
    public List<Widget> Widgets { get; set; }

}

This also has a validator WidgetInventoryValidator.

WidgetInventoryValidator

public class WidgetInventoryValidator : AbstractValidator<WidgetInventory>
{
    public WidgetInventoryValidator()
    {
        RuleFor(x => x.Id)
            .NotNull()
                .WithMessage("Id cannot be null.")
            .GreaterThan(0)
                .WithMessage("Id cannot be zero.");

        RuleFor(x => x.Name)
            .NotNull()
                .WithMessage("Name cannot be null.");

        RuleForEach(x => x.Widgets)
            .SetValidator(new WidgetValidator());
    }
}

Now this is great and I can reuse the WidgetValidator here, but to properly test that the RuleforEach is correctly configured I need to replicate the tests I have in WidgetValidatorTests in my WidgetInventoryValidatorTests class and keep them in sync.

I don't see a good way of doing this with inheritance. So I was wondering, does xUnit have a solution for having a package of tests that you can reuse in multiple test suites?


Solution

  • I could suggest refactoring a little your validators to accept sub-vaidators through constructor parameters - this way you will more adhere to dependency injection principles :)

    And also gain a way to better test validators.

    First, refactored classes (I simplified a little your code):

    public class Widget
    {
        public int? Id { get; set; }
        public string? Name { get; set; }
    }
    
    public class WidgetInventory
    {
        public int? Id { get; set; }
        public string? Name { get; set; }
        public DateTime? StartDate { get; set; }
        public List<Widget> Widgets { get; set; }
    
    }
    
    public class WidgetInventoryValidator : AbstractValidator<WidgetInventory>
    {
        public WidgetInventoryValidator(IValidator<Widget> widgetValidator)
        {
            RuleFor(x => x.Id)
                .NotNull()
                    .WithMessage("Id cannot be null.")
                .GreaterThan(0)
                    .WithMessage("Id cannot be zero.");
    
            RuleFor(x => x.Name)
                .NotNull()
                    .WithMessage("Name cannot be null.");
    
            RuleForEach(x => x.Widgets)
                .SetValidator(widgetValidator);
        }
    }
    
    public class WidgetValidator : AbstractValidator<Widget>
    {
        public WidgetValidator()
        {
            RuleFor(x => x.Id)
                .NotNull()
                    .WithMessage("Id cannot be null.")
                .GreaterThan(0)
                    .WithMessage("Id cannot be zero.");
    
            RuleFor(x => x.Name)
                .NotNull()
                    .WithMessage("Name cannot be null.");
        }
    }
    

    And here are tests - with and without mocking to showcase differences:

    public class ValidationTests
    {
        [Fact]
        public void Test_WithoutMock_ReturnValidationError()
        {
            // Arrange
            var widgetValidator = new WidgetValidator();
            var inventoryValidator = new WidgetInventoryValidator(widgetValidator);
            var inventory = new WidgetInventory
            {
                Id = 1,
                Name = "Foo",
                Widgets = new List<Widget>() { new() },
            };
    
            // Act
            var result = inventoryValidator.Validate(inventory);
    
            // Assert
            Assert.False(result.IsValid);
        }
    
        [Fact]
        public void Test_WithMock_ReturnValidationResultAsMocked()
        {
            // Arrange
            var widgetValidatorMock = new Mock<IValidator<Widget>>();
            widgetValidatorMock
                .Setup(x => x.Validate(It.IsAny<FluentValidation.ValidationContext<Widget>>()))
                .Returns(new ValidationResult() { Errors = new List<ValidationFailure>() });
            var inventoryValidator = new WidgetInventoryValidator(widgetValidatorMock.Object);
            var inventory = new WidgetInventory
            {
                Id = 1,
                Name = "Foo",
                Widgets = new List<Widget>() { new() },
            };
    
            // Act
            var result = inventoryValidator.Validate(inventory);
    
            // Assert
            Assert.True(result.IsValid);
        }
    }