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?
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);
}
}