How to unit test IValidatableObject Validate when it uses services for validation?

Viewed 1097

My Validate method is accessing various services for validation. These services are dependency injected and can be accessed using the validationContext.GetService method. During runtime, this works fine.

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    var logic = (ShipRegistrationLogic)validationContext.GetService(typeof(ShipRegistrationLogic));
    var userManager = (UserManager<IdentityUser>)validationContext.GetService(typeof(UserManager<IdentityUser>));
    var spaceTransitAuthority = (SpaceTransitAuthority)validationContext.GetService(typeof(ISpaceTransitAuthority));
    var hull = logic.GetHull(HullId);
    var user = userManager.FindByIdAsync(UserId).Result;

    if (spaceTransitAuthority.CheckActualHullCapacity(hull) > logic.GetMaxAuthorizedMass(user))
    {
        yield return new ValidationResult("You do not have the required pilot's license for this hull.", new[] { nameof(HullId) });
    }
}

I would like to unit test this complex validation. However, during the execution of the test, I am unable to figure out how to access the services needed for testing the evaluation. As GetService returns null during the test run.

I am using Moq and xUnit, and I have tried mocking the GetService method of the ValidationContext without success, as ValidationContext is a sealed class and cannot be mocked.

This is the test which shows what I have tried:

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void Validate_ShouldPassMassCheck(int hullId)
{
    // Arrange
    var userId = _users[0].Id;
    var viewModel = new ShipRegistrationViewModel()
    {
        UserId = userId,
        HullId = hullId
    };

    var hull = new Hull()
    {
        DefaultMaximumTakeOffMass = TakeOffMassEnum.Tank,
        Id = hullId,
        Name = "Test"
    };

    var shipRegistrationLogicMock = new Mock<ShipRegistrationLogic>();
    var spaceTransitAuthorityMock = new Mock<ISpaceTransitAuthority>();
    shipRegistrationLogicMock.Setup(x => x.GetHull(hullId)).Returns(hull);
    shipRegistrationLogicMock.Setup(x => x.GetMaxAuthorizedMass(_users[0])).Returns((int)TakeOffMassEnum.Tank);
    _userManager.Setup(x => x.FindByNameAsync(userId))
        .ReturnsAsync(new IdentityUser()
        {
            UserName = "user",
        });

    spaceTransitAuthorityMock.Setup(x => x.CheckActualHullCapacity(hull)).Returns((double)TakeOffMassEnum.Tank);

    var context = new Mock<ValidationContext>(viewModel);
    context.Setup(x => x.GetService(typeof(ShipRegistrationLogic))).Returns(shipRegistrationLogicMock);
    context.Setup(x => x.GetService(typeof(UserManager<IdentityUser>))).Returns(_userManager);
    context.Setup(x => x.GetService(typeof(ISpaceTransitAuthority))).Returns(spaceTransitAuthorityMock);

    // Act
    var results = viewModel.Validate(context.Object);

    // Assert
    Assert.Single(results);
    Assert.Equal(ValidationResult.Success, results.First());
}
2 Answers

You can mock a IServiceProvider and pass it in the constructor :

var services = new Mock<IServiceProvider>();
services.Setup(x => x.GetService(typeof(ShipRegistrationLogic)))
    .Returns(shipRegistrationLogicMock.Object);
services.Setup(x => x.GetService(typeof(UserManager<IdentityUser>)))
    .Returns(_userManager.Object);
services.Setup(x => x.GetService(typeof(ISpaceTransitAuthority)))
    .Returns(spaceTransitAuthorityMock.Object);

var context = new ValidationContext(viewModel, services.Object, null);

// Act
var results = viewModel.Validate(context);

You can also directly build a IServiceProvider without mock

As well as mocking IServiceProvider, like vernou says you can also build an IServiceProviver.

var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
services.AddSingleton(_ => shipRegistrationLogicMock.Object);
services.AddSingleton<UserManager<IdentityUser>>(_ => _userManager.Object);
services.AddSingleton(_ => spaceTransitAuthorityMock.Object);
var serviceProvider = services.BuidServiceProvider();

// And finally
var context = new ValidationContext(viewModel, serviceProvider, null);
Related