简体   繁体   中英

Best Practice for Unit Testing a Controller that depends on UserManager<TUser>?

I have a controller with the following signature:

[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private ILogger<UsersController> _logger;

    private readonly UserManager<IdentityUser> _usermanager;

    public UsersController(ILogger<UsersController> logger, UserManager<IdentityUser> usermanager)
    {
        _usermanager = usermanager;
        _logger = logger;
    }

    [HttpGet("{_uniqueid}")]
    public async Task<ObjectResult> GetUser(string _uniqueid)
    {
        //Retrieve the object
        try
        {
            var user = await _usermanager.FindByIdAsync(uniqueid);

            var model = JsonConvert.DeserializeObject<GetUserModel>(user.ToString());

            return new ObjectResult(JsonConvert.SerializeObject(model));
        }

        catch(CustomIdentityNotFoundException e)
        {
            return new BadRequestObjectResult(("User not found: {0}", e.Message));
        }
    }
}

Right now my unit test looks like this:

public class UsersUnitTests
{
    public UsersController _usersController;

    private UserManager<IdentityUser> _userManager;


    public UsersUnitTests()
    {
        _userManager = new MoqUserManager<IdentityUser>();

        _usersController = new UsersController((new Mock<ILogger<UsersController>>()).Object, _userManager);
    }

    [Fact]
    public async Task GetUser_ReturnsOkObjectResult_WhenModelStateIsValid()
    {
        //Setup

        //Test
        ObjectResult response = await _usersController.GetUser("realuser");

        //Assert
        //Should receive 200 and user data content body
        response.StatusCode.Should().Be((int)System.Net.HttpStatusCode.OK);
        response.Value.Should().NotBeNull();
    }
}

and the Moq'd classes:

public class MoqUserManager<T> : UserManager<IdentityUser>
{
    public MoqUserManager(IUserStore<IdentityUser> store, IOptions<IdentityOptions> optionsAccessor, 
        IPasswordHasher<IdentityUser> passwordHasher, IEnumerable<IUserValidator<IdentityUser>> userValidators, 
        IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer, 
        IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<IdentityUser>> logger) 
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
    }

    public MoqUserManager()
        : base((new MoqUserStore().Store), new Mock<IOptions<IdentityOptions>>().Object, 
            new Mock<IPasswordHasher<IdentityUser>>().Object, new Mock<IEnumerable<IUserValidator<IdentityUser>>>().Object, 
            new Mock<IEnumerable<IPasswordValidator<IdentityUser>>>().Object, new Mock<ILookupNormalizer>().Object, 
            new Mock<IdentityErrorDescriber>().Object, new Mock<IServiceProvider>().Object, new Mock<ILogger<UserManager<IdentityUser>>>().Object)
    {

    }
}

public class MoqUserStore : IdentityUserStore
{
    private Mock<IdentityUserStore> _store;

    public MoqUserStore()
        :base(new Mock<IdentityDbContext>().Object, new Mock<ILogger<IdentityUserStore>>().Object, null)
    {

        _store = new Mock<IdentityUserStore>(new Mock<IdentityDbContext>().Object, new Mock<ILogger<IdentityUserStore>>().Object, null);

        _store.Setup(x => x.FindByIdAsync("realuser", default(CancellationToken))).Returns(Task.Run(() => new IdentityUser("realuser")));
        _store.Setup(x => x.FindByIdAsync("notrealuser", default(CancellationToken))).Throws(new CustomIdentityNotFoundException());
        _store.Setup(x => x.CreateAsync(new IdentityUser("realuser"), default(CancellationToken))).Returns(Task.Run(() => IdentityResult.Success));
    }

    public IdentityUserStore Store { get => _store.Object; }

}

I get reference not set to an instance of an object errors when the MoqUserManager constructor is called.

My question is: What is the best practice (I will settle for works but stinks to high heaven ) for unit testing these types of controllers that depend on UserManager and/or SignInManager , and what is a easily repeatable way to mock the UserStore dependency?

I thought about the DI model and the dependencies of my controller. I only needed a handful of methods from UserManager , so I theorized about removing the dependency on UserManager from UsersController , and replacing it with some interface that implements the same signatures I needed from UserManager . Lets call that interface IMYUserManager :

public interface IMYUserManager
{
    Task<IdentityUser> FindByIdAsync(string uniqueid);
    Task<IdentityResult> CreateAsync(IdentityUser IdentityUser);
    Task<IdentityResult> UpdateAsync(IdentityUser IdentityUser);
    Task<IdentityResult> DeleteAsync(IdentityUser result);
}

Next, I needed to create a class that both is derived from UserManager and also implements IMYUserManager . The idea here is that implementing the methods from the interface will simply become overrides for the derived class, that way I get around FindByIdAsync (and the rest) being flagged as extension methods and requiring wrapping in a static class. Here is MyUserManager :

public class MYUserManager : UserManager<IdentityUser>, IMYUserManager
{
    public MYUserManager(IUserStore<IdentityUser> store, IOptions<IdentityOptions> optionsAccessor, 
        IPasswordHasher<IdentityUser> passwordHasher, IEnumerable<IUserValidator<IdentityUser>> userValidators, 
        IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, ILookupNormalizer keyNormalizer, 
        IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<IdentityUser>> logger) 
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
    }

    public override Task<IdentityUser> FindByIdAsync(string userId)
    {
        return base.FindByIdAsync(userId);
    }
    //Removed other overridden methods for brevity; They also call the base class method
}

Almost home. Next, I naturally updated UsersController to use the IMYUserManager interface:

[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private ILogger<UsersController> _logger;

    private readonly IMYUserManager _usermanager;

    public UsersController(ILogger<UsersController> logger, IMYUserManager 
        usermanager)
    {
        _usermanager = usermanager;
        _logger = logger;
    }
}

And, naturally after that I have to make this dependency available to the service container for all who desire to feast upon:

public void ConfigureServices(IServiceCollection services)
{

    services.AddScoped<IMYUserManager, MYUserManager>();


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

And, finally, after verifying that actually builds , I updated the test class:

public class UsersControllerTests
{
    public UsersController _usersController;

    private Mock<IMYUserManager> _userManager;


    public UsersControllerTests()
    {
        _userManager = new Mock<IMYUserManager>();

        _usersController = new UsersController((new Mock<ILogger<UsersController>> 
            ()).Object, _userManager.Object);
    }

    [Fact]
    public async Task GetUser_ReturnsOkObjectResult_WhenModelStateIsValid()
    {
        //Setup
        _userManager.Setup(x => x.FindByIdAsync("realuser"))
           .Returns(Task.Run(() => new IdentityUser("realuser","realuser1")));

        _usersController.ModelState.Clear();

        //Test
        ObjectResult response = await _usersController.GetUser("realuser");

        //Assert
        //Should receive 200 and user data content body
        response.StatusCode.Should().Be((int)System.Net.HttpStatusCode.OK);
        response.Value.Should().NotBeNull();
    }
}

What makes this a good solution?

Several things:

Removing the dependency on UserManager from UsersController was inline with the DI model. Abstracting away the dependencies (therefore abstracting away implementation details like extension methods) and making them available not only to be mocked, but available to the entire IServiceCollection means that I only have 3 very simple steps when I need to implement another method for the user manager:

  1. Add the method signature to IMYUserManager
  2. Override the method and call the base class implementation in MYUserManager
  3. Mock new dependency inside of unit tests

I may revisit the scope of the service, I chose AddScoped() just to prove the concept, but performance and business requirements will choose whether or not that stays the same.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM