对依赖于UserManager <tuser>的控制器进行单元测试的最佳实践?

时间:2018-11-23 21:43:44

标签: c# unit-testing moq xunit asp.net-core-2.1

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

[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和/或{的这些类型的控制器进行单元测试的最佳实践是什么(我会适应的工作,但会发臭到高高的天堂) {1}},模拟SignInManager依赖关系的简便方法是什么?

1 个答案:

答案 0 :(得分:1)

我考虑了DI模型和控制器的依赖关系。我只需要使用UserManager中的几种方法,因此我的理论是删除{{1}中对UserManager的依赖,并用实现我与{{ 1}}。让我们调用该接口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

最后,在确认实际上是 build 后,我更新了测试类:

[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(); } } 的依赖关系与DI模型保持一致。提取依赖关系(因此提取实现细节,例如扩展方法),并使它们不仅可被模拟,而且可用于整个UserManager,这意味着当我需要实现另一种方法时,我只有3个非常简单的步骤对于用户经理:

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

我可能会重新考虑服务范围,我选择IMYUserManager只是为了证明概念,但是性能和业务要求将决定是否保持不变。