Server
Las clases Server<TProgram>
y ServerBase<TProgram>
proporcionan un entorno de prueba integral para aplicaciones ASP.NET Core, especialmente microservicios. Estas clases simplifican la creación, configuración y gestión de un servidor web para pruebas unitarias y de integración, ofreciendo una base sólida para validar el comportamiento de tus aplicaciones de manera eficiente y confiable.
¿Cómo Funciona?
Server<TProgram>
extiende WebApplicationFactory<TProgram>
, proporcionando un punto de entrada para la configuración y el manejo del ciclo de vida del servidor de pruebas. ServerBase<TProgram>
, por otro lado, proporciona una clase base para las pruebas que se ejecutan sobre el servidor creado por Server<TProgram>
, facilitando el acceso al cliente HTTP, al proveedor de servicios y al canal gRPC.
El flujo general de funcionamiento es el siguiente:
- Configuración del Host:
Server<TProgram>
configura el host web de prueba, estableciendo las configuraciones de la aplicación, los servicios y el entorno. Utiliza unServerCompose
para la gestión de dependencias externas, como Redis, RabbitMQ, MongoDB y OpenTelemetry, a través de Docker Compose. - Inicialización de Dependencias: El
ServerCompose
inicia los contenedores de Docker necesarios para las pruebas, asegurando que las dependencias externas estén disponibles. - Creación del Cliente:
ServerBase<TProgram>
utiliza el métodoCreateClient
deServer<TProgram>
para crear unHttpClient
que puede utilizarse para enviar solicitudes al servidor de prueba. - Acceso al Servicio:
ServerBase<TProgram>
proporciona acceso alIServiceProvider
del servidor de prueba, permitiendo obtener instancias de servicios configurados en el mismo. - Acceso al Logger:
ServerBase<TProgram>
proporciona acceso alInMemoryLoggerProvider
, permitiendo inspeccionar los logs que genera el servidor de prueba. - Creación del Canal gRPC:
ServerBase<TProgram>
crea un canal gRPC utilizando elHttpHandler
del servidor, lo que permite realizar llamadas gRPC. - Ejecución de Pruebas: Las pruebas se ejecutan utilizando
ServerBase<TProgram>
, que proporciona una API conveniente para interactuar con el servidor de prueba. - Limpieza: Al finalizar las pruebas, los contenedores de Docker y los recursos utilizados se liberan, asegurando un entorno de pruebas limpio.
Server<TProgram>
La clase Server<TProgram>
extiende WebApplicationFactory<TProgram>
y proporciona un entorno de prueba para aplicaciones ASP.NET Core. Esta clase se encarga de configurar el host web de prueba, gestionar las dependencias externas a través de ServerCompose
, y proporcionar un HttpClient
para realizar solicitudes HTTP al servidor de pruebas.
ConfigureWebHost
Configura el IWebHostBuilder
para el servidor de pruebas. Establece las configuraciones, registra los servicios, y configura el entorno de desarrollo. Inyecta las dependencias levantadas por ServerCompose
.
Dispose
Detiene los contenedores de Docker y libera los recursos, como el InMemoryLoggerProvider
.
ConfigureServices
Configura los servicios para la aplicación de pruebas, como la autenticación de prueba y el logging en memoria.
ServerBase<TProgram>
La clase ServerBase<TProgram>
proporciona una clase base para las pruebas que se ejecutan sobre el servidor de pruebas creado por Server<TProgram>
. Esta clase facilita el acceso al HttpClient
, al IServiceProvider
y al canal gRPC, permitiendo interactuar con el servidor de pruebas de manera eficiente y confiable.
InitializeAsync
Inicializa los recursos necesarios para las pruebas, como el HttpClient
, el IServiceProvider
y el canal gRPC.
DisposeAsync()
No realiza ninguna acción de limpieza adicional (en esta implementación), dado que la limpieza principal se realiza en el método Dispose de Server<TProgram>
.
Implementación
Las clases Server<TProgram>
y ServerBase<TProgram>
se implementan como sigue:
using CodeDesignPlus.Net.xUnit.Microservice.Server.Authentication;using CodeDesignPlus.Net.xUnit.Microservice.Server.Logger;using CodeDesignPlus.Net.xUnit.Microservice.Server.Services;using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Mvc.Testing;
namespace CodeDesignPlus.Net.xUnit.Microservice.Server;
/// <summary>/// A server class for configuring and managing a web application for testing purposes./// </summary>/// <typeparam name="TProgram">The program class of the web application.</typeparam>public class Server<TProgram> : WebApplicationFactory<TProgram> where TProgram : class{ /// <summary> /// Gets the server compose instance for managing external dependencies. /// </summary> public ServerCompose Compose { get; } = new();
/// <summary> /// Gets the in-memory logger provider for capturing log messages. /// </summary> public InMemoryLoggerProvider LoggerProvider { get; } = new();
/// <summary> /// Gets or sets the action to configure the in-memory collection. /// </summary> public Action<Dictionary<string, string>> InMemoryCollection { get; set; }
/// <summary> /// Configures the web host for the application. /// </summary> /// <param name="builder">The web host builder.</param> protected override void ConfigureWebHost(IWebHostBuilder builder) { var configurationValues = new Dictionary<string, string>() { {"Redis:Instances:Core:ConnectionString", $"{Compose.Redis.Item1}:{Compose.Redis.Item2}"}, {"RabbitMQ:Host", Compose.RabbitMQ.Item1}, {"RabbitMQ:Port", Compose.RabbitMQ.Item2.ToString()}, {"Mongo:ConnectionString", $"mongodb://{Compose.Mongo.Item1}:{Compose.Mongo.Item2}"}, {"Observability:ServerOtel", $"http://{Compose.Otel.Item1}:{Compose.Otel.Item2}"}, {"Logger:OTelEndpoint", $"http://{Compose.Otel.Item1}:{Compose.Otel.Item2}" }, };
InMemoryCollection?.Invoke(configurationValues);
var contentRoot = Path.GetDirectoryName(Assembly.GetAssembly(typeof(TProgram))!.Location);
var configuration = new ConfigurationBuilder() .SetBasePath(contentRoot!) .AddEnvironmentVariables() .AddJsonFile("appsettings.json") .AddInMemoryCollection(configurationValues) .Build();
builder.UseConfiguration(configuration);
builder.ConfigureServices(ConfigureServices);
builder.UseEnvironment("Development"); }
/// <summary> /// Disposes the server and its resources. /// </summary> /// <param name="disposing">A boolean value indicating whether the object is being disposed.</param> protected override void Dispose(bool disposing) { Compose.StopInstance();
LoggerProvider.Dispose();
base.Dispose(disposing); }
/// <summary> /// Configures the services for the application. /// </summary> /// <param name="services">The service collection.</param> public virtual void ConfigureServices(IServiceCollection services) { services.AddAuthentication("TestAuth") .AddScheme<AuthenticationSchemeOptions, AuthHandler>("TestAuth", options => { });
services.AddSingleton(this.LoggerProvider); services.AddSingleton<ILoggerFactory, InMemoryLoggerFactory>(); }}
using CodeDesignPlus.Net.xUnit.Microservice.Server.Logger;using Grpc.Net.Client;
namespace CodeDesignPlus.Net.xUnit.Microservice.Server;/// <summary>/// A base server class for configuring and managing a web application for testing purposes./// </summary>/// <typeparam name="TProgram">The program class of the web application.</typeparam>/// <param name="server">The server instance.</param>public class ServerBase<TProgram>(Server<TProgram> server) : IAsyncLifetime where TProgram : class{ private AsyncServiceScope scope;
/// <summary> /// Gets the service provider for the application. /// </summary> protected IServiceProvider Services;
/// <summary> /// Gets the HTTP client for making requests to the server. /// </summary> protected HttpClient Client;
/// <summary> /// Gets the in-memory logger provider for capturing log messages. /// </summary> protected InMemoryLoggerProvider LoggerProvider => server.LoggerProvider;
/// <summary> /// Gets the gRPC channel for making gRPC calls to the server. /// </summary> protected GrpcChannel Channel;
/// <summary> /// Initializes the server and its resources asynchronously. /// </summary> /// <returns>A task that represents the asynchronous operation.</returns> public Task InitializeAsync() { Client = server.CreateClient(); scope = server.Services.CreateAsyncScope(); Services = scope.ServiceProvider;
var options = new GrpcChannelOptions { HttpHandler = server.Server.CreateHandler() };
Channel = GrpcChannel.ForAddress(server.Server.BaseAddress, options);
return Task.CompletedTask; }
/// <summary> /// Disposes the server and its resources asynchronously. /// </summary> /// <returns>A task that represents the asynchronous operation.</returns> public Task DisposeAsync() { return Task.CompletedTask; }}
Ejemplo de Uso
El siguiente ejemplo muestra cómo utilizar Server<TProgram>
y ServerBase<TProgram>
en un método de prueba unitaria:
using CodeDesignPlus.Net.xUnit.Microservice.Server;
namespace CodeDesignPlus.Net.xUnit.Microservice.Test.Server;
public class ServerTest : ServerBase<Program>, IClassFixture<Server<Program>>{ private readonly Server<Program> server;
private readonly Guid value = Guid.NewGuid();
public ServerTest(Server<Program> server) : base(server) { this.server = server;
server.InMemoryCollection = (x) => { x.Add("Custom:Item", value.ToString()); }; }
[Fact] public async Task GetWeatherForecast_ReturnsWeatherForecast() { //Act var response = await Client.GetAsync("/weatherforecast");
var value = server.Services.GetService<IConfiguration>()?.GetValue<string>("Custom:Item");
// Assert response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync();
var data = Serializers.JsonSerializer.Deserialize<IEnumerable<Api.WeatherForecast>>(responseString);
Assert.NotNull(data); Assert.NotEmpty(data); Assert.Equal(value, this.value.ToString()); }}
En este ejemplo:
ServerTest
hereda deServerBase<Program>
y recibe una instancia deServer<Program>
a través del constructor.- Se utiliza
IClassFixture<Server<Program>>
para indicar que la prueba necesita un servidor de prueba. - La prueba utiliza el
HttpClient
(Client
) para realizar una petición HTTP al endpoint/weatherforecast
. - Se realizan aserciones sobre la respuesta para verificar que fue exitosa y que se recibieron los datos esperados.
Conclusiones
Server<TProgram>
y ServerBase<TProgram>
son clases fundamentales para la creación de pruebas de integración y unitarias en aplicaciones .NET. Al proporcionar un entorno de prueba controlado, facilitar la gestión de dependencias externas y ofrecer una API conveniente para interactuar con el servidor de pruebas, estas clases mejoran la eficiencia y confiabilidad del proceso de pruebas.