web-dev-qa-db-ja.com

.Net CoreテストでUserManagerをモックする方法は?

次のコードがあります。 Create user.Followingのテストケースを実行しようとしています。

public class CreateUserCommandHandlerTest
{
    private Mock<UserManager<ApplicationUser>> _userManager;
    private CreateUserCommandHandler _systemUnderTest;

    public CreateUserCommandHandlerTest()
    {
        _userManager = MockUserManager.GetUserManager<ApplicationUser>();
        var user = new ApplicationUser() { UserName = "ancon1", Email = "[email protected]", RoleType = RoleTypes.Anonymous };
        _userManager
            .Setup(u => u.CreateAsync(user, "ancon2")).ReturnsAsync(IdentityResult.Success);
        _systemUnderTest = new CreateUserCommandHandler(_userManager.Object);
    }

    [Fact]
    public async void Handle_GivenValidInput_ReturnsCreatedResponse()
    {
        var command = new CreateUserCommand { Username = "ancon1", Email = "[email protected]", Password = "ancon2", RoleType = RoleTypes.Anonymous };
        var result = await _systemUnderTest.Handle(command, default(CancellationToken));
        Assert.NotNull(result);
        Assert.IsType<Application.Commands.CreatedResponse>(result);
    }
}

私のユーザーマネージャーはこちらです:

public static class MockUserManager
{
    public static Mock<UserManager<TUser>> GetUserManager<TUser>()
        where TUser : class
    {
        var store = new Mock<IUserStore<TUser>>();
        var passwordHasher = new Mock<IPasswordHasher<TUser>>();
        IList<IUserValidator<TUser>> userValidators = new List<IUserValidator<TUser>>
        {
            new UserValidator<TUser>()
        };
        IList<IPasswordValidator<TUser>> passwordValidators = new List<IPasswordValidator<TUser>>
        {
            new PasswordValidator<TUser>()
        };
        userValidators.Add(new UserValidator<TUser>());
        passwordValidators.Add(new PasswordValidator<TUser>());
        var userManager = new Mock<UserManager<TUser>>(store.Object, null, passwordHasher.Object, userValidators, passwordValidators, null, null, null, null);
        return userManager;
    }
}

そして私のコマンドハンドラはこれです:

 public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, BaseCommandResponse>
{
    private readonly UserManager<ApplicationUser> _userManager;

    public CreateUserCommandHandler(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task<BaseCommandResponse> Handle(CreateUserCommand createUserCommand, CancellationToken cancellationToken)
    {
        var user = new ApplicationUser { UserName = createUserCommand.Username, Email = createUserCommand.Email, RoleType = createUserCommand.RoleType };
        var result = await _userManager.CreateAsync(user, createUserCommand.Password);
        if (result.Succeeded)
        {
            return new CreatedResponse();
        }

        ErrorResponse errorResponse = new ErrorResponse(result.Errors.Select(e => e.Description).First());

        return errorResponse;
    }
}

テストを実行しているときに失敗し、オブジェクト参照がオブジェクトのインスタントに設定されていないと言っています。

ここで何が悪いのですか?

14

私はこれが数か月前であることを知っていますが、私はこのスレッドに戻り続けます。 HaokのGitHubの例を指すだけで「本を読んで」と言っているようなものなので、私はこのトピックについて自分の答えを拡張します。それは問題とあなたがする必要があることを正確に示しません。 Mockオブジェクトを分離する必要がありますが、それだけでなく、「CreateAsync」のメソッドを「セットアップ」する必要もあります。これを3つの部分に分けましょう。

  1. MOQまたは同様のフレームワークを使用して、UserManagerのモックアップされた作成を行う場合は、MOCKする必要があります。
  2. 結果を返すと予想されるUserManagerのメソッドを設定する必要があります。
  3. 必要に応じて、モックされたEntity Framework Core 2.1または同様のものからいくつかの一般的なリストを挿入して、IDentityユーザーのリストが実際に増減することを実際に確認できます。そのUserManagerだけが成功しただけではなく

それで、私がMocked UserManagerを返すためのヘルパーメソッドを持っているとしましょう。これはHaokコードからわずかに変更されています。

public static Mock<UserManager<TUser>> MockUserManager<TUser>(List<TUser> ls) where TUser : class
{
    var store = new Mock<IUserStore<TUser>>();
    var mgr = new Mock<UserManager<TUser>>(store.Object, null, null, null, null, null, null, null, null);
    mgr.Object.UserValidators.Add(new UserValidator<TUser>());
    mgr.Object.PasswordValidators.Add(new PasswordValidator<TUser>());

    mgr.Setup(x => x.DeleteAsync(It.IsAny<TUser>())).ReturnsAsync(IdentityResult.Success);
    mgr.Setup(x => x.CreateAsync(It.IsAny<TUser>(), It.IsAny<string>())).ReturnsAsync(IdentityResult.Success).Callback<TUser, string>((x, y) => ls.Add(x));
    mgr.Setup(x => x.UpdateAsync(It.IsAny<TUser>())).ReturnsAsync(IdentityResult.Success);

    return mgr;
}

これの鍵となるのは、私がテストするものである一般的な「TUser」を注入することと、これのリストを注入することです。私の例に似ています:

 private List<ApplicationUser> _users = new List<ApplicationUser>
 {
      new ApplicationUser("User1", "[email protected]") { Id = 1 },
      new ApplicationUser("User2", "[email protected]") { Id = 2 }
 };

 ...
 var userManager = IdentityMocking.MockUserManager<ApplicationUser>(_users); 

最後に、テストしたいこの実装に似たリポジトリを使用してパターンをテストしています。

 public async Task<int> CreateUser(ApplicationUser user, string password) => (await _userManager.CreateAsync(user, password)).Succeeded ? user.Id : -1;

私はそれを次のようにテストします:

 [Fact]
 public async Task CreateAUser()
 {
      var newUser = new ApplicationUser("NewUser", "[email protected]");
      var password = "P@ssw0rd!";

      var result = await _repo.CreateUser(newUser, password);

      Assert.Equal(3, _users.Count);
  }

私がしたことの要点は、CreateAsyncを「セットアップ」しただけでなく、コールバックを提供して、注入したリストが実際に増えていくのを確認できることです。これが誰かを助けることを願っています。

4
djangojazz

aspnet/Identity はオープンソースであるため、自分でモックを作成する方法を確認することができます。

以下に、その方法を示します。 MockHelpers.cs

TestUserManager

public static UserManager<TUser> TestUserManager<TUser>(IUserStore<TUser> store = null) where TUser : class
{
    store = store ?? new Mock<IUserStore<TUser>>().Object;
    var options = new Mock<IOptions<IdentityOptions>>();
    var idOptions = new IdentityOptions();
    idOptions.Lockout.AllowedForNewUsers = false;
    options.Setup(o => o.Value).Returns(idOptions);
    var userValidators = new List<IUserValidator<TUser>>();
    var validator = new Mock<IUserValidator<TUser>>();
    userValidators.Add(validator.Object);
    var pwdValidators = new List<PasswordValidator<TUser>>();
    pwdValidators.Add(new PasswordValidator<TUser>());
    var userManager = new UserManager<TUser>(store, options.Object, new PasswordHasher<TUser>(),
        userValidators, pwdValidators, new UpperInvariantLookupNormalizer(),
        new IdentityErrorDescriber(), null,
        new Mock<ILogger<UserManager<TUser>>>().Object);
    validator.Setup(v => v.ValidateAsync(userManager, It.IsAny<TUser>()))
        .Returns(Task.FromResult(IdentityResult.Success)).Verifiable();
    return userManager;
}
15
Nick Chapsas

.NetCore 2.2では、少し異なる方法で行う必要があります。 @Nick Chapsasの回答の更新のように扱います。

まず、IUserStoreの代わりにIUserPasswordStoreを使用する必要があります。 IUserPasswordStoreはIUserStoreを継承しますが、UserManagerはIUserPasswordStoreを取得しようとしています。他の方法では、いくつかはうまくいきません。

UserManagerの実際の動作(CreateUserAsyncなど)をテストする場合は、UserValidatorおよびPasswordValidatorの実際の実装を使用できます。メソッドがCreateUserエラーに対して想定したとおりに反応することを確認したいだけかもしれません。

これは私の更新された例です:

UserManager<TUser> CreateUserManager() where TUser : class
{
    Mock<IUserPasswordStore<TUser>> userPasswordStore = new Mock<IUserPasswordStore<TUser>>();
    userPasswordStore.Setup(s => s.CreateAsync(It.IsAny<TUser>(), It.IsAny<CancellationToken>()))
        .Returns(Task.FromResult(IdentityResult.Success));

    var options = new Mock<IOptions<IdentityOptions>>();
    var idOptions = new IdentityOptions();

    //this should be keep in sync with settings in ConfigureIdentity in WebApi -> Startup.cs
    idOptions.Lockout.AllowedForNewUsers = false;
    idOptions.Password.RequireDigit = true;
    idOptions.Password.RequireLowercase = true;
    idOptions.Password.RequireNonAlphanumeric = true;
    idOptions.Password.RequireUppercase = true;
    idOptions.Password.RequiredLength = 8;
    idOptions.Password.RequiredUniqueChars = 1;

    idOptions.SignIn.RequireConfirmedEmail = false;

    // Lockout settings.
    idOptions.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    idOptions.Lockout.MaxFailedAccessAttempts = 5;
    idOptions.Lockout.AllowedForNewUsers = true;


    options.Setup(o => o.Value).Returns(idOptions);
    var userValidators = new List<IUserValidator<TUser>>();
    UserValidator<TUser> validator = new UserValidator<TUser>();
    userValidators.Add(validator);

    var passValidator = new PasswordValidator<TUser>();
    var pwdValidators = new List<IPasswordValidator<TUser>>();
    pwdValidators.Add(passValidator);
    var userManager = new UserManager<TUser>(userPasswordStore.Object, options.Object, new PasswordHasher<TUser>(),
        userValidators, pwdValidators, new UpperInvariantLookupNormalizer(),
        new IdentityErrorDescriber(), null,
        new Mock<ILogger<UserManager<TUser>>>().Object);

    return userManager;
}

UserPasswordStoreにはメソッド(CreateAsync)があり、UserManagerからCreateAsyncをテストする場合はモックする必要があることに注意してください。

パスワードとロックアウトの設定は私のプロジェクトから取得されます。これらは、実際のテストを行えるように、設定と同期を保つ必要があります。

もちろん、たとえばPasswordValidatorはテストしませんが、次のようにメソッドをテストできます。

//Part of user service
public async Task<IdentityResult> Register(UserDto data)
{
    SystemUser user = ConvertDtoToUser(data);
    IdentityResult result = userManager.CreateAsync(user, data.Password);

    //some more code that is dependent on the result
}
1
Adam Jachocki