ASP .NET Core 集成测试因正文损坏而失败

ASP .NET Core Integration Tests fails with corrupted body

我正在做一个项目,我们必须使用 ASP .NET Core 3.x 开发 Web API。到目前为止,还不错,运行宁好。现在,我正在为这个网站编写一些集成测试 API,我很难让除 GET 请求之外的所有测试都正常工作。

我们正在使用 Jason Taylor 的 Clean Architecture。这意味着我们有一个包含所有请求处理程序的核心项目、一个包含所有数据库实体的域项目和一个用于 API 控制器的演示项目。我们使用 MediatR 和依赖注入来进行这些项目之间的通信。

现在,我们遇到请求的正文数据没有到达控制器的问题。

控制器中的 Update 方法如下所示:

[ApiController]
[Route("api/[controller]/[action]")]
public abstract class BaseController : ControllerBase
{
    private IMediator _mediator;
    protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
}

public class FavoriteController : BaseController
{
    [HttpPut("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Update(long id, UpdateFavoriteCommand command)
    {
        if (command == null || id != command.Id)
        {
            return BadRequest();
        }

        // Sends the request to the corresponding IRequestHandler
        await Mediator.Send(command);

        return NoContent();
    }
}

我们使用xUnit.net作为测试框架。 对于集成测试,我们使用了一个 InMemory SQLite 数据库,该数据库在固定装置 class.

中设置

测试如下所示:

public class UpdateFavoritesTestSqlite : IClassFixture<WebApplicationFactoryWithInMemorySqlite<Startup>>
{
    private readonly WebApplicationFactoryWithInMemorySqlite<Startup> _factory;
    private readonly string _endpoint;

    public UpdateFavoritesTestSqlite(WebApplicationFactoryWithInMemorySqlite<Startup> factory)
    {
        _factory = factory;
        _endpoint = "api/Favorite/Update/";
    }

    [Fact]
    public async Task UpdateFavoriteDetail_WithFullUpdate_ShouldUpdateCorrectly()
    {
        // Arange
        var client = _factory.CreateClient(); // WebApplicationFactory.CreateClient()
        var command = new UpdateFavoriteCommand
        {
            Id = 5,
            Caption = "caption new",
            FavoriteName = "a new name",
            Public = true
        };

        // Convert to JSON
        var jsonString = JsonConvert.SerializeObject(command);
        var httpContent = new StringContent(jsonString, Encoding.UTF8, "application/json");

        var stringUri = client.BaseAddress + _endpoint + command.Id;
        var uri = new Uri(stringUri);

        // Act
        var response = await client.PutAsync(uri, httpContent); 
        response.EnsureSuccessStatusCode();
        httpContent.Dispose();

        // Assert
        response.StatusCode.ShouldBe(HttpStatusCode.NoContent);

    }
}

如果我们 运行 测试,我们会收到 400 Bad Request 错误。 如果我们 运行 在调试模式下进行测试,我们可以看到代码由于模型状态错误而抛出自定义 ValidationException。这是在演示项目的DependencyInjection中配置的:

services
    .AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var failures = context.ModelState.Keys
                .Where(k => ModelValidationState.Invalid.Equals(context.ModelState[k].ValidationState))
                .ToDictionary(k => k, k => (IEnumerable<string>)context.ModelState[k].Errors.Select(e => e.ErrorMessage).ToList());

            throw new ValidationException(failures);
        };
    })
    .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<IWebApiDbContext>());

失败对象包含一个错误:

The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.

这是截图:Visual Studio in Debugging mode with json error.

在我读过的 Whosebug 中的一篇文章中,删除 [ApiController] class 属性会产生更详细的错误描述。在再次调试期间,在 await Mediator.Send(command); 行的 FavoriteController 的 Update 方法中测试和设置断点,我能够看到,到达 Update 方法的命令对象仅包含 null 或默认值,id 除外, 这是 5.

command 
    Caption         null    string
    FavoriteName    null    string
    Id              5       long
    Public          false   bool

最令人困惑(和令人沮丧)的部分是,使用 swagger 或邮递员进行的手动测试都成功了。按照我的理解,肯定是集成测试的时候出了问题。

我希望,有人可以帮助我,看看我缺少什么。难不成是Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory的HttpClient有问题?

我们在 Web api 演示项目的 LoggingMiddleware 中发现了问题。 在写这个问题之前,我们已经看过另一篇关于Whosebug的文章: 但是我们的代码中已经有了 request.Body.Seek(0, SeekOrigin.Begin); 。所以,我们认为就是这样,这不是问题所在。

但是现在,我们找到了这篇文章: .net core 3.0 logging middleware by pipereader

而不是像这样阅读请求正文:

await request.Body.ReadAsync(buffer, 0, buffer.Length);

...在读取后关闭流的地方,我们现在将 BodyReader 用作流并保持流打开:

var stream = request.BodyReader.AsStream(true); // AsStream(true) to let stream open
await stream.ReadAsync(buffer, 0, buffer.Length);
request.Body.Seek(0, SeekOrigin.Begin);