单元测试依赖于UserManager的控制器的最佳实践 ?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单元测试依赖于UserManager的控制器的最佳实践 ?相关的知识,希望对你有一定的参考价值。

我有一个带有以下签名的控制器:

[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的课程:

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依赖的简单可重复的方法?

答案

我想到了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个非常简单的步骤:

  1. 将方法签名添加到IMYUserManager
  2. 重写该方法并在MYUserManager中调用基类实现
  3. 在单元测试中模拟新的依赖

我可能会重新审视服务的范围,我选择AddScoped()只是为了证明这个概念,但性能和业务要求将选择是否保持不变。

以上是关于单元测试依赖于UserManager的控制器的最佳实践 ?的主要内容,如果未能解决你的问题,请参考以下文章

在AngularJS单元测试中模拟模块依赖性

在 AngularJS 单元测试中模拟模块依赖

如何测试依赖于具有关系的 Eloquent 模型的类?

在 Laravel 中对控制器进行单元测试而不测试路由的最佳方法是啥

在 Go 中分离单元测试和集成测试

如何在需要 UserManager 但使用内存数据库的 XUnit 中测试方法