У меня есть класс ErrorHandlingMiddleware, который используется в нескольких проектах и поэтому находится в общей библиотеке (а не в API), и я хочу написать для него несколько тестов. Я написал проходной тест с использованием TestServer (как описано в этой статье), который доказывает, что мое промежуточное ПО работает, когда конечная точка не выдает никаких исключений, но у меня возникли проблемы с тем, чтобы понять, как намеренно генерировать исключение, чтобы вызвать часть обработки ошибок.
Единственное, что мне удалось найти по этому поводу, это этот вопрос, который буквально противоположен тому, что я хочу.
Вот мой промежуточный код - ErrorHandlingService - это то, что просто хорошо обрабатывает и форматирует все мои ошибки.
public ErrorHandlingMiddleware(RequestDelegate next, IErrorHandlingService errHandler)
{
_next = next;
_errHandler = errHandler;
}
public async Task Invoke(HttpContext context, IHostEnvironment env)
{
try
{
_errHandler.AddRequestInformation(Guid.NewGuid(), DateTime.Now, $"{context.Request.Method} {context.Request.Path}");
await _next(context);
}
catch (Exception ex)
{
_errHandler.AddError(new MyError()
{
Title = "An unknown error occurred during execution",
ErrorID = "---",
ErrorMessage = env.IsProduction() ? "" : ex.ToString(),
HttpCode = HttpStatusCode.InternalServerError
});
context.Response.StatusCode = (int)_errHandler.ErrorResponse.StatusCode;
await context.Response.WriteAsJsonAsync(_errHandler.ErrorResponse);
}
}
И мой проходной тест, который проверяет отсутствие исключений.
public class ErrorHandlingMiddlewareTests
{
private async Task<IHost> GetHost()
{
return await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.Add(ServiceDescriptor.Scoped<IErrorHandlingService, ErrorHandlingService>());
})
.Configure(app =>
{
app.UseMiddleware<ErrorHandlingMiddleware>();
});
})
.StartAsync();
}
[Fact]
public async Task NewRequest_AddsRequestInformation()
{
// Arrange
using var host = await GetHost();
var requestRoute = "/route/to/endpoint";
var server = host.GetTestServer();
server.BaseAddress = new Uri("https://example.com/");
// Act
var context = await server.SendAsync(c => //throws a 404, I'm not testing for that
{
c.Request.Method = HttpMethods.Get;
c.Request.Path = requestRoute;
});
// Assert
var errorResponse = host.Services.GetService<IErrorHandlingService>().ErrorResponse;
errorResponse.ErrorContent.Request.Should().Be("GET " + requestRoute);
errorResponse.ErrorContent.TraceID.Should().NotBeEmpty();
}
}
Итак, как мне теперь заставить тестовый сервер генерировать исключение, которое может быть перехвачено моим промежуточным программным обеспечением?
Вместо этого вы можете написать модульные тесты. Просто сделайте так, чтобы макет выдал исключение.
Один из способов — перегрузить обработчик флагом и обернуть его в DEBUG, чтобы его не было в производственных сборках. В тестах, где вы хотите получить ответ об ошибке, отправьте true в качестве третьего параметра.
#if DEBUG
public async Task Invoke(HttpContext context, IHostEnvironment env, isErrorTest)
{
if (isErrorTest)
{
// Either build and respond with your error here, or possibly set something up on the server that would cause the normal handler to throw the error you want
_errHandler.AddError(new MyError()
{
// ...
});
// ... error response
}
// if not error test, call your normal handler
}
#endif
Это яркий пример «повреждения, вызванного тестами»: код промежуточного программного обеспечения и API не нужно загрязнять знаниями о тестах, которые на нем выполняются.
Итак, менее чем через 24 часа я решил, что, как обычно, мне просто нужно было поспать на нем. Комментарий StriplingWarrior указал мне на руководство, которое, хотя и не говорило ничего, чего я еще не знал, все же побудило меня задуматься о том, как на самом деле строится приложение в .NET и где обычный API будет искать контроллеры/маршруты.
Короче говоря, после некоторых проб и ошибок и некоторых исследований того, как работает Startup.cs, и некоторых основ маршрутизации вот как я решил это:
private async Task<IHost> GetHost()
{
return await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRouting(); // <- new line
services.Add(ServiceDescriptor.Scoped<IErrorHandlingService, ErrorHandlingService>());
})
.Configure(app =>
{
app.UseRouting(); // <- new line
app.UseMiddleware<ErrorHandlingMiddleware>();
app.UseEndpoints(endpoints => // <- new section
{
endpoints.MapGet("/hello", () => "Hello World!");
endpoints.MapGet("/err", () => { throw new Exception(); });
});
});
})
.StartAsync();
}
Использование некоторых встроенных определений новых конечных точек означает, что я могу легко контролировать то, что входит и выходит, и потенциально могу сделать ответы более сложными, если мне нужно. Я предполагаю, что довольно редко вы тестируете материал в библиотеке классов с локальным TestServer, но я надеюсь, что это будет полезно для следующего человека, у которого нет класса Startup для использования.
Мне кажется, вы можете создать образец веб-приложения специально для тестирования и использовать тестовый сервер для этого приложения. У вас может быть конечная точка, специально предназначенная для создания исключения, или у вас может быть конечная точка общего назначения, которая вызывает службу, которую вы можете имитировать, чтобы создавать разные поведения для каждого из ваших тестов. См. scotthannen.org/blog/2021/11/18/…