模拟的UserManager不能真正按预期工作

时间:2019-07-01 12:17:03

标签: c# testing asp.net-core mocking

我有一些服务,该服务负责在使用UserManager时更改用户密码,并且一切正常,但是当我想编写一些测试时,它在方法CheckPassword处开始失败,该方法基本上检查是否current (old password)是正确的

myService:
public async bool TryChangePassword(User user, string OldPassword, string NewPassword)
{
    (...)

    // it returns false
    var checkOldPassword = await _userManager.CheckPasswordAsync(user, OldPassword);

    if (!checkOldPassword)
    {
        return false;
    }

    var token = await _userManager.GeneratePasswordResetTokenAsync(user);

    var result = await _userManager.ResetPasswordAsync(user, token, NewPassword);

    return result.Succeeded;
}

myTests:

private readonly UserManager<User> _userManager;

[Fact]
public void password_change_attempt_1()
{
    var service = new myService(_context, _userManager);

    var user = new User("john");

    var register = _userManager.CreateAsync(user, "123456");

    _context.SaveChanges();

    Assert.True(_context.Users.Any());

    var result = service.TryChangePassword(user, "123456", "newPassword");
}

但是由于某种原因它失败了:

  

var checkOldPassword =等待_userManager.CheckPasswordAsync(user,OldPassword);

它返回false,但是如您所见,只要密码正确,嘲笑用户管理器可能有问题

这是如何在UserManager构造函数中创建Tests'的模拟物

public Tests()
{
    var o = new DbContextOptionsBuilder<Context>();
    o.UseInMemoryDatabase(Guid.NewGuid().ToString());
    _context = new Context(o.Options);
    _context.Database.EnsureCreated();

    var userStore = new MockUserStore(_context);
    _userManager = new MockUserManager(userStore,
                        new Mock<IOptions<IdentityOptions>>().Object,
                        new Mock<IPasswordHasher<User>>().Object,
                        new IUserValidator<User>[0],
                        new IPasswordValidator<User>[0],
                        new Mock<ILookupNormalizer>().Object,
                        new Mock<IdentityErrorDescriber>().Object,
                        new Mock<IServiceProvider>().Object,
                        new Mock<ILogger<UserManager<User>>>().Object);
}

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

    public override Task<IdentityResult> CreateAsync(User user)
    {
        this.Store.CreateAsync(user, new CancellationToken());
        return Task.FromResult(IdentityResult.Success);
    }
}
public class MockUserStore : IUserStore<User>, IUserPasswordStore<User>
{
    public readonly Context _ctx;

    public MockUserStore(Context ctx)
    {
        _ctx = ctx;
    }
    public Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
    {
        _ctx.Users.Add(user);

        return Task.FromResult(IdentityResult.Success);
    }

    public Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        throw new NotImplementedException();
    }

    public Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetPasswordHashAsync(User user, CancellationToken cancellationToken)
    {
        return Task.FromResult<string>(user.PasswordHash);
    }

    public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken)
    {
        return Task.FromResult<string>(user.Id);
    }

    public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<bool> HasPasswordAsync(User user, CancellationToken cancellationToken)
    {
        return Task.FromResult<bool>(!String.IsNullOrEmpty(user.PasswordHash));
    }

    public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken)
    {
        user.PasswordHash = passwordHash;
        return Task.FromResult(0);
    }

    public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

我想可以在这里修复它

public Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
{
    _ctx.Users.Add(user);

    return Task.FromResult(IdentityResult.Success);
}

通过添加类似内容

user.PasswordHash = generateHash(password)

但是我怎么知道ASP.NET Core Identity使用了多少次迭代?

https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing?view=aspnetcore-2.2

string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
    password: password,
    salt: salt,
    prf: KeyDerivationPrf.HMACSHA1,
    iterationCount: 10000,
    numBytesRequested: 256 / 8));

1 个答案:

答案 0 :(得分:0)

您应该始终避免使用如此深的模拟。 用自己的界面隐藏具体的实现。

public interface IMyUserManager
{
    Task<bool> CheckPasswordAsync(User user, string oldPassword);
    Task<string> GeneratePasswordResetTokenAsync(User user);        
    Task<IdentityResult> ResetPasswordAsync(User user, string token, string 
        newPassword);
    }
}

第二,使用内置的UserManager实现此接口

public class MyUserManager : IMyUserManager 
{
    private readonly UserManager<User> _userManager;

    public MyUserManager(UserManager<User> userManager)
    {
        if (userManager is null)
        {
           throw new ArgumentNullException(nameof(userManager));
        }
        _userManager = userManager;
    }

    public Task<bool> CheckPasswordAsync(User user, string oldPassword)
    {
        return _userManager.CheckPasswordAsync(user, oldPassword);
    }

    public Task<string> GeneratePasswordResetTokenAsync(User user)
    {
        return _userManager.GeneratePasswordResetTokenAsync(user);
    }

    public Task<IdentityResult> ResetPasswordAsync(User user, string token, 
        string newPassword)
    {
        return _userManager.ResetPasswordAsync(user, token, newPassword);
    }
}

然后,重写您的服务

        private readonly IMyUserManager _userManager;

        public MyService(IMyUserManager userManager)
        {
            _userManager = userManager;
        }
        public async Task<bool> TryChangePassword(User user, string OldPassword, string NewPassword)
        {
            // it returns false
            var checkOldPassword = await _userManager.CheckPasswordAsync(user, OldPassword);

            if (!checkOldPassword)
            {
                return false;
            }

            var token = await _userManager.GeneratePasswordResetTokenAsync(user);

            var result = await _userManager.ResetPasswordAsync(user, token, NewPassword);

            return result.Succeeded;

最后,编写尽可能简单的测试用例。修复测试时,周围的人永远不想去实现子类

[Fact]
        public async Task password_change_attempt_1()
        {
            var mock = new Mock<IMyUserManager>();
            MyService myService = new MyService(mock.Object);
            mock.Setup(x => x.CheckPasswordAsync(It.IsAny<User>(), It.IsAny<string>()))
                .Returns(Task.FromResult(true));
            mock.Setup(x => x.ResetPasswordAsync(It.IsAny<User>(), It.IsAny<string>(),
                It.IsAny<string>()))
                .Returns(Task.FromResult(IdentityResult.Success));

            var result =
                await myService
                    .TryChangePassword(new User { Name = "Name", }, "OldPassword", "NewPassword");

            Assert.Equal(result, true);
        }