Как протестировать POST-метод контроллера, который не возвращает данных в содержимом ответа в .NET Core 3.1?

#c# #asp.net-core #testing #integration-testing #xunit

#c# #asp.net-core #тестирование #интеграция-тестирование #xunit

Вопрос:

я новичок в интеграционных тестах. У меня есть метод контроллера, который добавляет пользователя в базу данных, как показано ниже:

 [HttpPost]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserRequest request)
{
    try
    {
        var command = new CreateUserCommand
        {
            Login = request.Login,
            Password = request.Password,
            FirstName = request.FirstName,
            LastName = request.LastName,
            MailAddress = request.MailAddress,
            TokenOwnerInformation = User
        };

        await CommandBus.SendAsync(command);

        return Ok();
    }
    catch (Exception e)
    {
        await HandleExceptionAsync(e);
                
        return StatusCode(StatusCodes.Status500InternalServerError,
            new {e.Message});
    }
}
  

Как вы заметили, мой метод не возвращает никакой информации о пользователе, который был добавлен в базу данных — он информирует о результатах обработки определенного запроса с использованием кодов состояния. Я написал интеграционный тест, чтобы проверить, работает ли он правильно:

         [Fact]
        public async Task ShouldCreateUser()
        {
            // Arrange
            var createUserRequest = new CreateUserRequest
            {
                Login = "testowyLogin",
                Password = "testoweHaslo",
                FirstName = "Aleksander",
                LastName = "Kowalski",
                MailAddress = "akowalski@onet.poczta.pl"
            };
            var serializedCreateUserRequest = SerializeObject(createUserRequest);
            
            // Act
            var response = await HttpClient.PostAsync(ApiRoutes.CreateUserAsyncRoute,
                serializedCreateUserRequest);
            
            // Assert
            response
                .StatusCode
                .Should()
                .Be(HttpStatusCode.OK);
        }
  

Я не уверен, достаточно ли утверждать только код состояния ответа, возвращаемый с сервера. Я в замешательстве, потому что, я не знаю, должен ли я прикрепить к коду раздела assert, который получит всех пользователей и проверит, содержит ли он, например, созданного пользователя. У меня даже нет идентификатора такого пользователя, потому что мое приложение находит новый идентификатор для пользователя при добавлении его / ее в базу данных. Я также понятия не имею, как тестировать подобные методы:

         [HttpGet("{userId:int}")]
        public async Task<IActionResult> GetUserAsync([FromRoute] int userId)
        {
            try
            {
                var query = new GetUserQuery
                {
                    UserId = userId,
                    TokenOwnerInformation = User
                };
                
                var user = await QueryBus
                    .SendAsync<GetUserQuery, UserDto>(query);

                var result = user is null
                    ? (IActionResult) NotFound(new
                        {
                            Message = (string) _stringLocalizer[UserConstants.UserNotFoundMessageKey]
                        })
                    : Ok(user);

                return resu<
            }
            catch (Exception e)
            {
                await HandleExceptionAsync(e);
                
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new {e.Message});
            }
        }
  

Я считаю, что я должен каким-то образом сначала создать пользователя в разделе Arrange, получить его идентификатор, а затем использовать его в разделе Act с помощью метода GetUserAsync, вызываемого с запросом, отправленным HttpClient. Снова та же проблема — после создания не возвращается информация о пользователе (кстати, она не возвращается из-за моего дизайна CQRS во всем приложении — команды не возвращают никакой информации). Не могли бы вы объяснить мне, как правильно написать такие тесты? Я что-нибудь пропустил? Спасибо за любую помощь.

Комментарии:

1. Это просто: вы не можете написать интеграционный тест, который утверждает, что за пределами HTTP200, это природа асинхронного приложения. Если вам нужны дополнительные утверждения, вам понадобятся автоматические / сквозные тесты, в которых вы создаете случайного пользователя и ждете (повторяя попытку динамически), пока система не сможет создать пользователя в реальной базе данных. В качестве теста уровня интеграции, который можно запускать как часть сборки, ваш код выглядит нормально, просто утверждая, что ответ в порядке

Ответ №1:

Вот как я это делаю:

 var response = (CreatedResult) await _controller.Post(createUserRequest);
response.StatusCode.Should().Be(StatusCodes.Status201Created);
  

Вторая строка выше не нужна, просто для иллюстрации.

Кроме того, ваш ответ будет лучше, если вы вернете 201 (созданный) вместо 200 (OK) для Post-глаголов, например:

 return Created($"api/users/{user.id}", user);
  

Для тестирования notfound’s:

 var result = (NotFoundObjectResult) await _controller.Get(id);
result.StatusCode.Should().Be(StatusCodes.Status404NotFound);
  

NotFoundObjectResult Предполагается, что вы что-то возвращаете. Если вы просто отвечаете 404 и без объяснения причин, замените NotFoundObjectResult на NotFoundResult .

И, наконец, InternalServerErrors:

 var result = (ObjectResult) await _controller.Get(id);
result.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
  

Ответ №2:

Для этого вы можете использовать integrationFixture, используя этот пакет NuGet . Это альтернатива автоматической фиксации для интеграционных тестов.

В документированных примерах используются вызовы Get, но вы можете выполнять и другие вызовы. По логике вещей, вы должны проверить код состояния ( OkObjectResult означает 200) и ответ (который может быть пустой строкой, это вообще не проблема).

Вот документированный пример обычного вызова Get.

 [Fact]
public async Task GetTest()
{
    // arrange
    using (var fixture = new Fixture<Startup>())
    {
        using (var mockServer = fixture.FreezeServer("Google"))
        {
            SetupStableServer(mockServer, "Response");
            var controller = fixture.Create<SearchEngineController>();

            // act
            var response = await controller.GetNumberOfCharacters("Hoi");

            // assert
            var request = mockServer.LogEntries.Select(a => a.RequestMessage).Single();
            Assert.Contains("Hoi", request.RawQuery);
            Assert.Equal(8, ((OkObjectResult)response.Result).Value);
        }
    }
}

private void SetupStableServer(FluentMockServer fluentMockServer, string response)
{
    fluentMockServer.Given(Request.Create().UsingGet())
        .RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
            .WithStatusCode(HttpStatusCode.OK));
}
  

В приведенном выше примере контроллер решается с помощью DI, описанного в вашем Startup классе.

Вы также можете выполнить фактический вызов REST, используя using Refit . Приложение самостоятельно размещается внутри вашего теста.

 using (var fixture = new RefitFixture<Startup, ISearchEngine>(RestService.For<ISearchEngine>))
{
    using (var mockServer = fixture.FreezeServer("Google"))
    {
        SetupStableServer(mockServer, "Response");
        var refitClient = fixture.GetRefitClient();
        var response = await refitClient.GetNumberOfCharacters("Hoi");
        await response.EnsureSuccessStatusCodeAsync();
        var request = mockServer.LogEntries.Select(a => a.RequestMessage).Single();
        Assert.Contains("Hoi", request.RawQuery);
    }
}