2
votes

I'm working in ServiceStack and using FluentValidation to handle incoming DTOs on requests. I've broken these out as follows, but my unit tests don't seem to be able to target specific rule sets. My code is as follows:

DTO / REQUEST CLASSES

public class VendorDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class DvrDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public VendorDto Vendor { get; set; }
}

public class DvrRequest : IReturn<DvrResponse>
{
    public DvrDto Dvr { get; set; }
}

VALIDATOR CLASSES

public class VendorValidator : AbstractValidator<VendorDto>
{
    public VendorValidator()
    {
        RuleFor(v => v.Name).NotEmpty();
    }
}

public class DvrValidator : AbstractValidator<DvrDto>
{
    public DvrValidator()
    {
        RuleFor(dvr => dvr.Name).NotEmpty();
        RuleFor(dvr => dvr.Vendor).NotNull().SetValidator(new VendorValidator());
    }
}

public class DvrRequestValidator : AbstractValidator<DvrRequest>
{
    public DvrRequestValidator()
    {
        RuleSet(HttpMethods.Post, () =>
        {
            RuleFor(req => req.Dvr).SetValidator(new DvrValidator());
        });

        RuleSet(HttpMethods.Patch, () =>
        {
            RuleFor(req => req.Dvr).SetValidator(new DvrValidator());
            RuleFor(req => req.Dvr.Id).GreaterThan(0);
        });
    }
}

UNIT TEST

[TestMethod]
public void FailWithNullDtoInRequest()
{
    // Arrange
    var dto = new DvrRequest();
    var validator = new DvrRequestValidator();

    // Act
    var result = validator.Validate(msg, ruleSet: HttpMethods.Post);

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

I would prefer to be able to control what gets called depending on what the HttpMethod is that's being called. My thought here was, I want to validate all fields on the DvrDto (and child VendorDto) for both POST and PATCH, but only require a valid Id be set on PATCH. I am setting up my DvrRequestValidator to handle this. However, my unit test as written above (targeting the RuleSet for the POST verb) always finds the request to be valid, even though the validator should fail the request.

In fiddling with it, if I make the following changes:

VALIDATOR

public class DvrRequestValidator : AbstractValidator<DvrRequest>
{
    public DvrRequestValidator()
    {
        RuleFor(req => req.Dvr).SetValidator(new DvrValidator());

        RuleSet(HttpMethods.Patch, () =>
        {
            RuleFor(req => req.Dvr.Id).GreaterThan(0);
        });
    }
}

TEST CALL (removing the targeted verb)

    // Act
    var result = validator.Validate(msg); // , ruleSet: HttpMethods.Post);

The validator then works as I expect for a POST, but the PATCH rule set doesn't get executed. As a result, I seem to lose the granularity of what I want validated on a particular verb. It would appear to me that this is supported in examples I've seen both on StackOverflow and in the FluentValidation docs. Am I doing something wrong here? Or is this not possible?

1

1 Answers

0
votes

The Validators are registered in ServiceStack's Global Request Filters so you'd typically use an Integration Test with a Service Client to test validation errors.

If you want to test the validator independently in a Unit Test you can execute a HTTP Method Result set with something like:

var req = new BasicRequest(requestDto);
var validationResult = validator.Validate(new ValidationContext(requestDto, null, 
    new MultiRuleSetValidatorSelector(HttpMethods.Patch)) {
        Request = req
    });

Unit Testing ServiceStack Features

Although note a lot of ServiceStack functionality assumes there's an AppHost is available, but in most cases you can just use an In Memory AppHost, e.g:

[Test]
public void My_unit_test()
{
    using (new BasicAppHost().Init())
    {
        //test ServiceStack classes
    }
}

Of if you prefer you can set it up once per test fixture with something like:

public class MyUnitTests
{
    ServiceStackHost appHost;
    public MyUnitTests() => appHost = new BasicAppHost().Init();

    [OneTimeTearDown]
    public void OneTimeTearDown() => appHost.Dispose();

    [Test]
    public void My_unit_test()
    {
        //test ServiceStack classes
   }
}