web-dev-qa-db-fra.com

Comment se moquer de UserManager dans les tests .Net Core?

J'ai le code suivant. Im essayant d'exécuter un cas de test pour créer un utilisateur. Voici ce que j'ai essayé jusqu'à présent.

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

Mon gestionnaire d'utilisateurs est ici:

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

et mon gestionnaire de commandes est le suivant:

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

lorsque j'exécute mon test, il échoue et dit que la référence d'objet n'est pas définie à un instant d'un objet.

Qu'est-ce que je fais mal ici ??

14
Anushka Madushan

Je sais que cela fait des mois mais je reviens toujours à ce fil. Je vais étendre ma propre réponse sur ce sujet, car le simple fait de pointer vers l'exemple GitHub de Haok revient à dire: "Lire un livre" car il est énorme. Il ne précise pas le problème et ce que vous devez faire. Vous devez isoler un objet Mock, mais non seulement cela, mais vous devez également "configurer" la méthode pour "CreateAsync". Mettons donc cela en trois parties:

  1. Vous devez MOCKER si vous utilisez MOQ ou un framework similaire pour créer une création maquette du UserManager.
  2. Vous devez configurer les méthodes de UserManager dont vous vous attendez pour obtenir des résultats.
  3. Vous pouvez éventuellement injecter une liste générique à partir d'un Entity Framework Core 2.1 simulé ou similaire afin que vous puissiez réellement voir qu'une liste d'utilisateurs IDentity augmente ou diminue réellement. Non seulement que UserManager a réussi et rien d'autre

Donc, disons que j'ai une méthode d'assistance pour retourner un Mocked UserManager. Ce qui est légèrement modifié par rapport au code 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;
}

Ce qui est essentiel, c'est que j'injecte un "TUser" générique, c'est ce que je vais également tester en injectant une liste de cela. Semblable à mon exemple de:

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

Enfin, je teste un modèle avec un référentiel similaire à cette implémentation que je veux tester:

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

Je le teste comme ceci:

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

La clé de ce que j'ai fait est que non seulement j'ai "configuré" le CreateAsync mais que j'ai fourni un rappel afin que je puisse réellement voir ma liste que j'injecte être incrémentée. J'espère que cela aide quelqu'un.

4
djangojazz

aspnet/Identity est opensource donc ce que vous pouvez faire est de voir comment ils se moquent d'eux-mêmes.

Voici comment ils le font: 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

Dans .NetCore 2.2, vous devez le faire légèrement différent. Traitez-le comme une mise à jour de la réponse de @Nick Chapsas.

Tout d'abord, vous devez utiliser IUserPasswordStore au lieu de IUserStore. IUserPasswordStore hérite de IUserStore, mais UserManager souhaite obtenir IUserPasswordStore. D'une autre manière, certaines choses ne fonctionneront pas.

Si vous souhaitez tester le comportement réel de UserManager (par exemple CreateUserAsync), vous pouvez utiliser des implémentations réelles de UserValidator et PasswordValidator. Vous voudrez peut-être simplement vous assurer que votre méthode réagit comme elle le supposait pour les erreurs CreateUser.

Voici mon exemple mis à jour:

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

Notez que UserPasswordStore a une méthode (CreateAsync) qui doit être simulée si vous souhaitez tester CreateAsync à partir de UserManager.

Les paramètres de mot de passe et de verrouillage sont extraits de mon projet. Ils doivent être synchronisés avec vos paramètres, afin que vous puissiez tester la réalité.

Bien sûr, vous ne testez pas par exemple PasswordValidator, mais vous pouvez tester vos méthodes, par exemple:

//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