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);
我正在做一个项目,我们必须使用 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);