Лучшая практика для модульного тестирования контроллера, который зависит от UserManager <TUser>? - PullRequest
0 голосов
/ 24 ноября 2018

У меня есть контроллер со следующей подписью:

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

Сейчас мой модульный тест выглядит следующим образом:

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

и классы Moq'd:

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

}

Я получаю reference not set to an instance of an object ошибок, когда вызывается конструктор MoqUserManager.

Мой вопрос: какова лучшая практика (я согласен на работы, но воняет на небеса ) для модульного тестирования этих типов контроллеров, которые зависят от UserManager и / или SignInManager, и каков легко повторяемый способ высмеивать зависимость UserStore?

1 Ответ

0 голосов
/ 24 ноября 2018

Я думал о модели DI и зависимостях моего контроллера.Мне нужно было всего несколько методов из UserManager, поэтому я теоретизировал об удалении зависимости от UserManager из UsersController и замене ее на какой-то интерфейс, который реализует те же сигнатуры, которые мне нужны от UserManager.Давайте назовем этот интерфейс IMYUserManager:

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

Далее мне нужно было создать класс, который является производным от UserManager и также реализует IMYUserManager.Идея здесь заключается в том, что реализация методов из интерфейса просто станет переопределением для производного класса, так что я получаю возможность пометить FindByIdAsync (и все остальное) как методы расширения и требовать переноса в статический класс.Вот 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
}

Почти дома.Затем я естественным образом обновил UsersController для использования интерфейса IMYUserManager:

[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;
    }
}

И, естественно, после этого я должен сделать эту зависимость доступной для сервисного контейнера для всех, кто желает наслаждаться:

public void ConfigureServices(IServiceCollection services)
{

    services.AddScoped<IMYUserManager, MYUserManager>();


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

И, наконец, после проверки того, что на самом деле строит , я обновил тестовый класс:

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

Что делает это хорошим решением?

Несколько вещей:

Удаление зависимости от UserManager из UsersController соответствовало модели DI.Абстрагирование зависимостей (следовательно, абстрагирование деталей реализации, таких как методы расширения) и обеспечение их доступности не только для моделирования, но и для всего IServiceCollection означает, что у меня есть только 3 очень простых шага, когда мне нужно реализовать другой метод дляuser manager:

  1. Добавить сигнатуру метода в IMYUserManager
  2. Переопределить метод и вызвать реализацию базового класса в MYUserManager
  3. Смоделировать новую зависимость внутримодульные тесты

Я могу вернуться к сфере обслуживания, я выбрал AddScoped() просто для подтверждения концепции, но требования к производительности и бизнесу будут определять, останется ли он неизменным.

...