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?
Section titled “¿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 unServerComposepara la gestión de dependencias externas, como Redis, RabbitMQ, MongoDB y OpenTelemetry, a través de Docker Compose. - Inicialización de Dependencias: El
ServerComposeinicia 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étodoCreateClientdeServer<TProgram>para crear unHttpClientque puede utilizarse para enviar solicitudes al servidor de prueba. - Acceso al Servicio:
ServerBase<TProgram>proporciona acceso alIServiceProviderdel 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 elHttpHandlerdel 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>
Section titled “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
Section titled “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
Section titled “Dispose”Detiene los contenedores de Docker y libera los recursos, como el InMemoryLoggerProvider.
ConfigureServices
Section titled “ConfigureServices”Configura los servicios para la aplicación de pruebas, como la autenticación de prueba y el logging en memoria.
ServerBase<TProgram>
Section titled “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
Section titled “InitializeAsync”Inicializa los recursos necesarios para las pruebas, como el HttpClient, el IServiceProvider y el canal gRPC.
DisposeAsync()
Section titled “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
Section titled “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 Ductus.FluentDocker.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> /// Gets or sets the action to configure the services for the application. /// </summary> public Action<IServiceCollection> ConfigureServices { 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(ConfigureServicesInternal);
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 ConfigureServicesInternal(IServiceCollection services) { services.AddAuthentication("TestAuth") .AddScheme<AuthenticationSchemeOptions, AuthHandler>("TestAuth", options => { });
services.AddSingleton(this.LoggerProvider); services.AddSingleton<ILoggerFactory, InMemoryLoggerFactory>();
this.ConfigureServices?.Invoke(services); }}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
Section titled “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 item = 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(item, this.value.ToString()); }}En este ejemplo:
ServerTesthereda 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
Section titled “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.