Действия контроллера модульного тестирования Asp.net Core Identity

У меня проблема с определением, как и что тестировать.

У меня есть контроллер, который вводит UserManager и вызывает метод CreateAsync для создания нового пользователя.

Я не хочу тестировать диспетчер пользователей Identity, поскольку он уже был тщательно протестирован. Что я хотел бы сделать, так это проверить, что контроллер работает по правильным путям (в моем случае есть 3 пути, отправляющих ответы либо с ошибками состояния модели, либо с ошибками ответа идентичности, либо с простой строкой)

Должен ли я пытаться создать имитацию диспетчера пользователей, чтобы создать свой тест (я не уверен, как настроить диспетчер пользователей как фиктивную зависимость) Во-вторых, как я могу установить условия для проверки того, что контроллер принял заданный путь.

Я использую xUnit и Moq.

[Route("api/[controller]")]
public class MembershipController : BaseApiController
{
    private UserManager<ApplicationUser> _userManager;

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

    [HttpGet("RegisterNewUser")]
    public HttpResponseMessage RegisterNewUser([FromBody] NewUserRegistration user)
    {
        if (ModelState.IsValid)
        {
            ApplicationUser newUser = new ApplicationUser();
            newUser.UserName = user.username;
            newUser.Email = user.password;
            IdentityResult result = _userManager.CreateAsync(newUser, user.password).Result;

            if (result.Errors.Count() > 0)
            {
                var errors = new IdentityResultErrorResponse().returnResponseErrors(result.Errors);
                return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
            }
        }
        else
        {
            var errors = new ViewModelResultErrorResponse().returnResponseErrors(ModelState);
            return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
        }

        return this.WebApiResponse(
                    "We have sent a valifation email to you, please click on the verify email account link.",
                    HttpStatusCode.OK);
    }
}

В моем модульном тесте у меня есть следующее, чтобы проверить сценарий счастливого пути

    [Fact]
    public void RegisterNewUser_ReturnsHttpStatusOK_WhenValidModelPosted()
    {
        var mockStore = new Mock<IUserStore<ApplicationUser>>();
        var mockUserManager = new Mock<UserManager<ApplicationUser>>(mockStore.Object, null, null, null, null, null, null, null, null);

        ApplicationUser testUser = new ApplicationUser { UserName = "[email protected]" };

        mockStore.Setup(x => x.CreateAsync(testUser, It.IsAny<CancellationToken>()))
           .Returns(Task.FromResult(IdentityResult.Success));

        mockStore.Setup(x => x.FindByNameAsync(testUser.UserName, It.IsAny<CancellationToken>()))
                    .Returns(Task.FromResult(testUser));


        mockUserManager.Setup(x => x.CreateAsync(testUser).Result).Returns(new IdentityResult());

        MembershipController sut = new MembershipController(mockUserManager.Object);
        var input = new NewUserInputBuilder().Build();
        sut.RegisterNewUser(input);

    }

Где «вход» в sut.RegisterNewUser (вход); относится к вспомогательному классу, который создает модель представления, которая требуется для действия контроллера:

public class NewUserInputBuilder
{
    private string username { get; set; }
    private string password { get; set; }
    private string passwordConfirmation { get; set; }
    private string firstname { get; set; }
    private string lastname { get; set; }

    internal NewUserInputBuilder()
    {
        this.username = "[email protected]";
        this.password = "password";
        this.passwordConfirmation = "password";
        this.firstname = "user";
        this.lastname = "name";
    }

    internal NewUserInputBuilder WithNoUsername()
    {
        this.username = "";
        return this;
    }

    internal NewUserInputBuilder WithMisMatchedPasswordConfirmation()
    {
        this.passwordConfirmation = "MismatchedPassword";
        return this;
    }

    internal NewUserRegistration Build()
    {
        return new NewUserRegistration
        { username = this.username, password = this.password,
            passwordConfirmation = this.passwordConfirmation,
            firstname = this.firstname, lastname = this.lastname
        };
    }
} 

Моя цель здесь - вызвать 3 условия с помощью тестов:

  1. Создайте действительную модель просмотра и верните сообщение об успешном завершении.
  2. Создайте допустимую модель просмотра, но вернет ошибку IdentityResponse (например, пользователь существует), которая преобразуется в
  3. Создать недопустимую модель просмотра и вернуть ошибки Modelstate

Ошибки обрабатываются с помощью абстрактного класса, который возвращает объект json. Базовый класс для контроллера просто создает HttpResponseMessage для возврата.

В основном я хочу проверить, что правильный класс ответа на ошибку вызывается путем принудительного выполнения теста по пути ошибки modelstate, пути identityresult.errors и что удачный путь может быть достигнут.

Затем я планирую изолированно протестировать классы ответов на ошибки.

Надеюсь, этого достаточно.


person BWG    schedule 17.11.2017    source источник
comment
Хотя это потребует некоторого рефакторинга, взгляните на ответ, который я дал здесь stackoverflow.com/questions/41883635/. Несколько быстрых советов, когда дело доходит до издевательства. Не смейтесь над тем, чем вам не принадлежит. Классы должны зависеть от абстракций, а не от конкреции.   -  person Nkosi    schedule 17.11.2017
comment
Спасибо за Ваш ответ. Я все еще не совсем уверен, как тестировать свой код. Искал чуть более полный ответ.   -  person BWG    schedule 17.11.2017
comment
Что ж, тогда я предлагаю вам сначала начать с чего-нибудь пробовать. Если вы застряли, покажите, что вы пробовали, и тогда мы увидим, где мы можем помочь. Судя по тому, что вы уже предоставили, это было бы слишком широко для полного ответа. Сообщество постарается помочь, как может, но не ожидайте, что оно сделает все за вас.   -  person Nkosi    schedule 17.11.2017
comment
Спасибо за предложение, я обновил сообщение, включив в него свой тестовый код, некоторые связанные классы и особенности того, что мне нужно протестировать.   -  person BWG    schedule 17.11.2017
comment
Хороший. Я посмотрю на это.   -  person Nkosi    schedule 17.11.2017
comment
Первоначальная оценка удачного пути показала мне, что нет необходимости настраивать UserStore, поскольку вы будете напрямую переопределять диспетчер пользователей. Тестируемый метод должен быть асинхронным и не использовать блокирующие вызовы, т.е. .Result   -  person Nkosi    schedule 17.11.2017


Ответы (2)


Тестируемый метод должен быть асинхронным и не использовать блокирующие вызовы, т.е. .Result

[HttpGet("RegisterNewUser")]
public async Task<HttpResponseMessage> RegisterNewUser([FromBody] NewUserRegistration user) {
    if (ModelState.IsValid) {
        var newUser = new ApplicationUser() {
            UserName = user.username,
            Email = user.password
        };
        var result = await _userManager.CreateAsync(newUser, user.password);
        if (result.Errors.Count() > 0) {
            var errors = new IdentityResultErrorResponse().returnResponseErrors(result.Errors);
            return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
        }
    } else {
        var errors = new ViewModelResultErrorResponse().returnResponseErrors(ModelState);
        return this.WebApiResponse(errors, HttpStatusCode.BadRequest);
    }

    return this.WebApiResponse(
                "We have sent a valifation email to you, please click on the verify email account link.",
                HttpStatusCode.OK);
}

Обзор тестируемого сценария и метода «Счастливый путь» показывает, что нет необходимости настраивать UserStore, поскольку тест будет напрямую замещать виртуальных членов диспетчера пользователей.

Обратите внимание, что тест также был сделан асинхронным.

  1. Создайте допустимую модель просмотра и верните сообщение об успешном завершении.
[Fact]
public async Task RegisterNewUser_ReturnsHttpStatusOK_WhenValidModelPosted() {
    //Arrange
    var mockStore = Mock.Of<IUserStore<ApplicationUser>>();
    var mockUserManager = new Mock<UserManager<ApplicationUser>>(mockStore, null, null, null, null, null, null, null, null);

    mockUserManager
        .Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
        .ReturnsAsync(IdentityResult.Success);

    var sut = new MembershipController(mockUserManager.Object);
    var input = new NewUserInputBuilder().Build();

    //Act
    var actual = await sut.RegisterNewUser(input);

    //Assert
    actual
        .Should().NotBeNull()
        .And.Match<HttpResponseMessage>(_ => _.IsSuccessStatusCode == true);        
}
  1. Создайте допустимую модель просмотра, но возвращает ошибку IdentityResponse (например, пользователь существует), которая преобразуется

Для этого вам просто нужно настроить макет, чтобы он возвращал результат с ошибками.

[Fact]
public async Task RegisterNewUser_ReturnsHttpStatusBadRequest_WhenViewModelPosted() {
    //Arrange

    //...code removed for brevity

    mockUserManager
        .Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
        .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "test"}));

    //...code removed for brevity

    //Assert
    actual
        .Should().NotBeNull()
        .And.Match<HttpResponseMessage>(_ => _.StatusCode == HttpStatusCode.BadRequest);
}

И для

  1. Создать недопустимую модель просмотра и вернуть ошибки Modelstate

Вам просто нужно установить состояние модели контроллера так, чтобы оно было недействительным.

[Fact]
public async Task RegisterNewUser_ReturnsHttpStatusBadRequest_WhenInvalidModelState() {
    //Arrange
    var mockStore = Mock.Of<IUserStore<ApplicationUser>>();
    var mockUserManager = new Mock<UserManager<ApplicationUser>>(mockStore, null, null, null, null, null, null, null, null);

    var sut = new MembershipController(mockUserManager.Object);
    sut.ModelState.AddModelError("", "invalid data");
    var input = new NewUserInputBuilder().Build();

    //Act
    var actual = await sut.RegisterNewUser(input);

    //Assert
    actual
        .Should().NotBeNull()
        .And.Match<HttpResponseMessage>(_ => _.StatusCode == HttpStatusCode.BadRequest);    
}

FluentAssertions использовались для выполнения всех утверждений. Вы могли бы так же легко использовать Assert.* API.

Этого должно быть достаточно, чтобы ответить на вышеуказанный вопрос.

person Nkosi    schedule 17.11.2017
comment
Я почти потерял слова. Спасибо за самый замечательный ответ, это действительно заполнило пробелы и действительно улучшило мое понимание. - person BWG; 17.11.2017
comment
@BenWhite с модульным тестированием вы хотите изолировать то, что тестируется. В случаях, когда есть зависимости, вы хотите иметь возможность имитировать зависимости, чтобы тесты могли быть выполнены, как задумано. Когда вы пройдете по потоку вашего тестируемого метода, он покажет вам, чем нужно манипулировать для достижения желаемых результатов. - person Nkosi; 17.11.2017

Вот простой способ использования NUnit (вы можете сделать что-то подобное с xUnit), если вы не хотите тестировать диспетчер пользователей. (Я также показал, как DbContext может быть передан тому же контроллеру, используя базу данных в памяти, которую можно использовать для настройки фиктивных данных)

    private DbContextOptions<MyContextName> options;

    [OneTimeSetUp]
    public void SetUp()
    {
        options = new DbContextOptionsBuilder<MyContextName>()
            .UseInMemoryDatabase(databaseName: "MyDatabase")
            .Options;

        // Insert seed data into the in-memory mock database using one instance of the context
        using (var context = new MyContextName(options))
        {
            var testWibble = new Wibble { MyProperty = 1, MyOtherProperty = 2 ... };
            context.wibbles.Add(testWibble);

            context.SaveChanges();
        }
    }


    [Test]
    public void Some_TestMethod()
    {
        // Use a clean instance of the context to run the test
        using (var context = new MyDbContext(options))
        {
            var store = new UserStore<MyUserType>(context);
            var userManager = new UserManager<MyUserType>(store, null, null, null, null, null, null, null, null);

            MyController MyController = new MyController(userManager, context);

            ... test the controller
        }
    }
person Chris Halcrow    schedule 24.08.2020