Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Sugerencia
Este contenido es un extracto del libro electrónico, Arquitecto de aplicaciones web modernas con ASP.NET Core y Azure, disponible en .NET Docs o como un PDF descargable gratuito que se puede leer sin conexión.
"Si no le gustan las pruebas unitarias del producto, lo más probable es que a los clientes no les guste probarlo, tampoco". _-Anónimo-
El software de cualquier complejidad puede producir errores de maneras inesperadas en respuesta a los cambios. Por lo tanto, las pruebas después de realizar cambios son necesarias para todas las aplicaciones más triviales (o menos críticas). Las pruebas manuales son la manera más lenta, menos confiable y costosa de probar software. Desafortunadamente, si las aplicaciones no están diseñadas para ser testeables, puede ser el único medio de prueba disponible. Las aplicaciones escritas para seguir los principios arquitectónicos establecidos en el capítulo 4 deben ser en gran medida susceptibles de pruebas unitarias. ASP.NET aplicaciones core admiten la integración automatizada y las pruebas funcionales.
Tipos de pruebas automatizadas
Hay muchos tipos de pruebas automatizadas para aplicaciones de software. La prueba de nivel más simple y inferior es la prueba unitaria. En un nivel ligeramente superior, hay pruebas de integración y pruebas funcionales. Otros tipos de pruebas, como pruebas de IU, pruebas de carga, pruebas de esfuerzo y pruebas de humo, están fuera del ámbito de este documento.
Pruebas unitarias
Una prueba unitaria prueba una sola parte de la lógica de la aplicación. Uno puede describirlo más enumerando algunas de las cosas que no es. Una prueba unitaria no prueba cómo funciona el código con dependencias o infraestructura, es decir, qué son las pruebas de integración. Una prueba unitaria no prueba el marco en el que se escribe el código: debe suponer que funciona o, si lo encuentra, registre un error y codifique una solución alternativa. Una prueba unitaria se ejecuta completamente en memoria y en proceso. No se comunica con el sistema de archivos, la red ni una base de datos. Las pruebas unitarias solo deben probar el código.
Las pruebas unitarias, en virtud del hecho de que solo prueban una sola unidad del código, sin dependencias externas, deben ejecutarse muy rápido. Por lo tanto, debería poder ejecutar conjuntos de pruebas de cientos de pruebas unitarias en unos segundos. Ejecútelos con frecuencia, idealmente antes de cada inserción en un repositorio de control de código fuente compartido y, ciertamente, con cada compilación automatizada en el servidor de compilación.
Pruebas de integración
Aunque es una buena idea encapsular el código que interactúa con infraestructura como bases de datos y sistemas de archivos, seguirá teniendo parte de ese código y probablemente quiera probarlo. Además, debe comprobar que las capas del código interactúan según lo esperado cuando las dependencias de la aplicación se resuelven por completo. Esta funcionalidad es responsabilidad de las pruebas de integración. Las pruebas de integración tienden a ser más lentas y más difíciles de configurar que las pruebas unitarias, ya que a menudo dependen de dependencias externas e infraestructura. Por lo tanto, debe evitar probar las cosas que se podrían probar con pruebas unitarias en las pruebas de integración. Si puede probar un escenario determinado con una prueba unitaria, debe probarlo con una prueba unitaria. Si no es posible, considere la posibilidad de usar una prueba de integración.
Las pruebas de integración a menudo tendrán procedimientos de configuración y desmontaje más complejos que las pruebas unitarias. Por ejemplo, una prueba de integración que va en contra de una base de datos real necesitará una manera de devolver la base de datos a un estado conocido antes de cada ejecución de prueba. A medida que se agregan nuevas pruebas y el esquema de base de datos de producción evoluciona, estos scripts de prueba tienden a crecer en tamaño y complejidad. En muchos sistemas grandes, no es práctico ejecutar conjuntos completos de pruebas de integración en estaciones de trabajo de desarrollador antes de comprobar los cambios en el control de código fuente compartido. En estos casos, las pruebas de integración se pueden ejecutar en un servidor de compilación.
Pruebas funcionales
Las pruebas de integración se escriben desde la perspectiva del desarrollador para comprobar que algunos componentes del sistema funcionan correctamente juntos. Las pruebas funcionales se escriben desde la perspectiva del usuario y comprueban la exactitud del sistema en función de sus requisitos. El siguiente extracto ofrece una analogía útil para pensar en las pruebas funcionales, en comparación con las pruebas unitarias:
"Muchas veces el desarrollo de un sistema es similar al edificio de una casa. Aunque esta analogía no es bastante correcta, podemos ampliarla para comprender la diferencia entre las pruebas unitarias y funcionales. Las pruebas unitarias son análogas a un inspector de edificio que visita el sitio de construcción de una casa. Está enfocado en los diversos sistemas internos de la casa: los cimientos, la estructura, la electricidad, la fontanería, etc. Asegura (pruebas) que las partes de la casa funcionarán correctamente y de forma segura, es decir, cumplan con el código de construcción. Las pruebas funcionales en este escenario son análogas al propietario que visita este mismo sitio de construcción. Asume que los sistemas internos se comportarán adecuadamente, que el inspector de edificio está realizando su tarea. El propietario se centra en lo que será vivir en esta casa. Está preocupado por cómo se ve la casa, si las distintas habitaciones tienen un tamaño cómodo, si la casa se ajusta a las necesidades de la familia, y si las ventanas están en un buen lugar para atrapar el sol de la mañana. El propietario está realizando pruebas funcionales en la casa. Tiene la perspectiva del usuario. El inspector municipal está realizando pruebas unitarias en la casa. Tiene la perspectiva del constructor".
Origen: Pruebas unitarias frente a pruebas funcionales
Me encanta decir "Como desarrolladores, fallamos de dos maneras: creamos lo malo o creamos lo incorrecto". Las pruebas unitarias garantizan que está construyendo lo correcto; Las pruebas funcionales garantizan que está creando lo correcto.
Dado que las pruebas funcionales funcionan en el nivel del sistema, pueden requerir cierto grado de automatización de la interfaz de usuario. Al igual que las pruebas de integración, normalmente funcionan con algún tipo de infraestructura de prueba. Esta actividad hace que sean más lentas y frágiles que las pruebas unitarias y de integración. Solo debe tener tantas pruebas funcionales como necesite para estar seguro de que el sistema se comporta según lo previsto por los usuarios.
Pirámide de pruebas
Martin Fowler escribió sobre la pirámide de pruebas, un ejemplo del cual se muestra en la figura 9-1.
Figura 9-1. Pirámide de pruebas
Las diferentes capas de la pirámide, y sus tamaños relativos, representan diferentes tipos de pruebas y cuántos debe escribir para la aplicación. Como puede ver, la recomendación es tener una gran base de pruebas unitarias, compatible con una capa más pequeña de pruebas de integración, con una capa aún más pequeña de pruebas funcionales. Idealmente, cada capa solo debe tener pruebas en ella que no se puedan realizar adecuadamente en una capa inferior. Tenga en cuenta la pirámide de pruebas cuando intente decidir qué tipo de prueba necesita para un escenario determinado.
Qué probar
Un problema común para los desarrolladores que no tienen experiencia al escribir pruebas automatizadas es decidir qué probar. Un buen punto de partida es probar la lógica condicional. En cualquier lugar que tenga un método con comportamiento que cambie en función de una instrucción condicional (if-else, switch, etc.), debería poder presentar al menos un par de pruebas que confirmen el comportamiento correcto para determinadas condiciones. Si el código tiene condiciones de error, es conveniente escribir al menos una prueba para la "ruta de acceso feliz" a través del código (sin errores) y al menos una prueba para la "ruta de acceso triste" (con errores o resultados inusuales) para confirmar que la aplicación se comporta como se esperaba en el caso de errores. Por último, intente centrarse en las pruebas que pueden producir errores, en lugar de centrarse en métricas como la cobertura de código. Por lo general, es mejor tener más cobertura de código que menos. Sin embargo, escribir algunas pruebas más de un método complejo y crítico para la empresa suele ser un mejor uso del tiempo que escribir pruebas para propiedades automáticas solo para mejorar las métricas de cobertura de código de prueba.
Organización de proyectos de prueba
Los proyectos de prueba se pueden organizar de la manera que mejor funcione para usted. Es recomendable separar las pruebas por tipo (prueba unitaria, prueba de integración) y por lo que están probando (por proyecto, por espacio de nombres). Si esta separación consta de carpetas dentro de un único proyecto de prueba o de varios proyectos de prueba, es una decisión de diseño. Un proyecto es más sencillo, pero para proyectos grandes con muchas pruebas o para ejecutar más fácilmente diferentes conjuntos de pruebas, es posible que desee tener varios proyectos de prueba diferentes. Muchos equipos organizan proyectos de prueba basados en el proyecto que están probando, que para las aplicaciones con más de algunos proyectos pueden dar lugar a un gran número de proyectos de prueba, especialmente si sigue desglosando estos de acuerdo con qué tipo de pruebas se encuentran en cada proyecto. Un enfoque de compromiso es tener un proyecto por tipo de prueba, por aplicación, con carpetas dentro de los proyectos de prueba para indicar el proyecto (y la clase) que se está probando.
Un enfoque común consiste en organizar los proyectos de aplicación en una carpeta "src" y los proyectos de prueba de la aplicación en una carpeta "tests" paralela. Puede crear carpetas de soluciones coincidentes en Visual Studio si encuentra útil esta organización.
Figura 9-2. Prueba de la organización en la solución
Puede usar el marco de pruebas que prefiera. El marco de trabajo xUnit funciona bien y es en el que están escritas todas las pruebas de ASP.NET Core y EF Core. Puede agregar un proyecto de prueba de xUnit en Visual Studio mediante la plantilla que se muestra en la figura 9-3 o desde la CLI mediante dotnet new xunit
.
Figura 9-3. Agregar un proyecto de prueba de xUnit en Visual Studio
Nombres de pruebas
Asigne un nombre a las pruebas de forma coherente, con nombres que indiquen lo que hace cada prueba. Un enfoque con el que he tenido gran éxito es asignar un nombre a las clases de prueba según la clase y el método que están probando. Este enfoque resulta en muchas clases de prueba pequeñas, pero hace que quede extremadamente claro de qué es responsable cada prueba. Con el nombre de clase de prueba configurado, para identificar la clase y el método que se va a probar, se puede usar el nombre del método de prueba para especificar el comportamiento que se está probando. Este nombre debe incluir el comportamiento esperado y las entradas o suposiciones que deben producir este comportamiento. Algunos nombres de prueba de ejemplo:
CatalogControllerGetImage.CallsImageServiceWithId
CatalogControllerGetImage.LogsWarningGivenImageMissingException
CatalogControllerGetImage.ReturnsFileResultWithBytesGivenSuccess
CatalogControllerGetImage.ReturnsNotFoundResultGivenImageMissingException
Una variante de este enfoque finaliza cada nombre de clase de prueba con "Should" (Debería) y modifica ligeramente el tiempo:
CatalogControllerGetImage
Deber.
LlamarImageServiceWithId
CatalogControllerGetImage
Debería.
RegistrarWarningGivenImageMissingException
Para algunos equipos el segundo método de nomenclatura es más claro, aunque un poco más detallado. En cualquier caso, intente usar una convención de nomenclatura que proporcione información sobre el comportamiento de las pruebas, de modo que cuando se produzca un error en una o varias pruebas, es obvio de sus nombres qué casos han fallado. Evite asignar nombres a las pruebas vagamente, como ControllerTests.Test1, ya que estos nombres no ofrecen ningún valor cuando los ve en los resultados de la prueba.
Si sigue una convención de nomenclatura como la anterior que genera muchas clases de prueba pequeñas, es recomendable organizar aún más las pruebas mediante carpetas y espacios de nombres. En la figura 9-4 se muestra un enfoque para organizar las pruebas por carpeta dentro de varios proyectos de prueba.
Figura 9-4. Organización de clases de prueba por carpeta en función de la clase que se está probando.
Si una clase de aplicación determinada tiene muchos métodos que se están probando (y, por tanto, muchas clases de prueba), puede tener sentido colocar estas clases en una carpeta correspondiente a la clase de aplicación. Esta organización no es diferente de cómo puede organizar archivos en carpetas en otro lugar. Si tiene más de tres o cuatro archivos relacionados en una carpeta que contiene muchos otros archivos, a menudo resulta útil moverlos a su propia subcarpeta.
Pruebas unitarias de aplicaciones de ASP.NET Core
En una aplicación ASP.NET Core bien diseñada, la mayor parte de la complejidad y la lógica de negocios se encapsularán en entidades empresariales y una variedad de servicios. La aplicación ASP.NET Core MVC, con sus controladores, filtros, modelos de vista y vistas, debe requerir pocas pruebas unitarias. Gran parte de la funcionalidad de una acción determinada se encuentra fuera del propio método de acción. Las pruebas de si el enrutamiento o el control global de errores funcionan correctamente no se pueden realizar de forma eficaz con una prueba unitaria. Del mismo modo, los filtros, incluidos los filtros de validación del modelo y de autenticación y autorización, no se pueden probar de manera unitaria con una prueba dirigida al método de acción de un controlador. Sin estos orígenes de comportamiento, la mayoría de los métodos de acción deben ser trivialmente pequeños y delegar la mayor parte de su trabajo en los servicios que se pueden probar independientemente del controlador que los use.
En ocasiones tendrá que refactorizar el código para poder realizar pruebas unitarias en él. Con frecuencia, esta actividad implica identificar abstracciones y usar la inserción de dependencias para acceder a la abstracción en el código que desea probar, en lugar de codificar directamente en la infraestructura. Por ejemplo, considere este método de acción fácil para mostrar imágenes:
[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
var contentRoot = _env.ContentRootPath + "//Pics";
var path = Path.Combine(contentRoot, id + ".png");
Byte[] b = System.IO.File.ReadAllBytes(path);
return File(b, "image/png");
}
La prueba unitaria de este método es difícil por su dependencia directa de System.IO.File
, que usa para leer desde el sistema de archivos. Puede probar este comportamiento para asegurarse de que funciona según lo previsto, pero hacerlo con archivos reales es una prueba de integración. Vale la pena tener en cuenta que no se puede probar unitariamente la ruta de este método; verá cómo realizar esta prueba con una prueba funcional en breve.
Si no puede probar directamente el comportamiento del sistema de archivos y no puede probar la ruta, ¿qué hay que probar? Bueno, después de refactorizar para que las pruebas unitarias sean posibles, puede detectar algunos casos de prueba y el comportamiento que falta, como el control de errores. ¿Qué hace el método cuando no se encuentra un archivo? ¿Qué debería hacer? En este ejemplo, el método refactorizado tiene este aspecto:
[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
byte[] imageBytes;
try
{
imageBytes = _imageService.GetImageBytesById(id);
}
catch (CatalogImageMissingException ex)
{
_logger.LogWarning($"No image found for id: {id}");
return NotFound();
}
return File(imageBytes, "image/png");
}
_logger
y _imageService
se insertan como dependencias. Ahora puede probar que el mismo identificador que se pasa al método de acción se pasa a _imageService
y que los bytes resultantes se devuelven como parte de FileResult. También puede probar que el registro de errores se está produciendo según lo previsto y que se devuelve un NotFound
resultado si falta la imagen, suponiendo que este comportamiento es importante para la aplicación (es decir, no solo código temporal que el desarrollador agregó para diagnosticar un problema). La lógica real del archivo se ha movido a un servicio de implementación independiente y se ha mejorado para devolver una excepción específica de la aplicación en el caso de que falte un archivo. Puede probar esta implementación de forma independiente mediante una prueba de integración.
En la mayoría de los casos, querrá usar controladores de excepciones globales en los controladores, por lo que la cantidad de lógica en ellos debe ser mínima y probablemente no valga la pena realizar pruebas unitarias. Realice la mayoría de las pruebas de acciones del controlador mediante pruebas funcionales y la TestServer
clase que se describe a continuación.
Pruebas de integración de aplicaciones ASP.NET Core
La mayoría de las pruebas de integración de las aplicaciones de ASP.NET Core deben ser servicios de prueba y otros tipos de implementación definidos en el proyecto de infraestructura. Por ejemplo, podría probar que EF Core estaba actualizando y recuperando correctamente los datos que espera de las clases de acceso a datos que residen en el proyecto de infraestructura. La mejor manera de probar que el proyecto de ASP.NET Core MVC se comporta correctamente es con pruebas funcionales que se ejecutan en la aplicación que se ejecuta en un host de prueba.
Pruebas funcionales de aplicaciones ASP.NET Core
Para las aplicaciones ASP.NET Core, la clase hace que las TestServer
pruebas funcionales sean bastante fáciles de escribir. Configura un TestServer
usando un WebHostBuilder
(o HostBuilder
) directamente (como es habitual para su aplicación) o con el tipo WebApplicationFactory
(disponible desde la versión 2.1). Intente hacer coincidir su host de prueba con el host de producción lo más estrechamente posible, de modo que sus pruebas ejecuten un comportamiento similar al que mostrará la aplicación en producción. La clase WebApplicationFactory
es útil para configurar el ContentRoot de TestServer, que es utilizado por ASP.NET Core para localizar recursos estáticos como las vistas.
Puede crear pruebas funcionales sencillas mediante la creación de una clase de prueba que implemente IClassFixture<WebApplicationFactory<TEntryPoint>>
, donde TEntryPoint
es la clase Startup
de su aplicación web. Después de incorporar esta interfaz, el accesorio de prueba puede crear un cliente con el método CreateClient
de la fábrica:
public class BasicWebTests : IClassFixture<WebApplicationFactory<Program>>
{
protected readonly HttpClient _client;
public BasicWebTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
// write tests that use _client
}
Sugerencia
Si usa la configuración mínima de API en el archivo de Program.cs , de forma predeterminada, la clase se declarará interna y no será accesible desde el proyecto de prueba. En su lugar, puede elegir cualquier otra clase de instancia en el proyecto web o agregarla al archivo Program.cs :
// Make the implicit Program class public so test projects can access it
public partial class Program { }
Con frecuencia, querrá realizar alguna configuración adicional del sitio antes de que se ejecute cada prueba, como configurar la aplicación para usar un almacén de datos en memoria y, a continuación, inicializar la aplicación con datos de prueba. Para lograr esta funcionalidad, cree su propia subclase de WebApplicationFactory<TEntryPoint>
e invalide su ConfigureWebHost
método. El ejemplo siguiente es del proyecto eShopOnWeb FunctionalTests y se usa como parte de las pruebas en la aplicación web principal.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace Microsoft.eShopWeb.FunctionalTests.Web;
public class WebTestFixture : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.AddEntityFrameworkInMemoryDatabase();
// Create a new service provider.
var provider = services
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<CatalogContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(provider);
});
services.AddDbContext<AppIdentityDbContext>(options =>
{
options.UseInMemoryDatabase("Identity");
options.UseInternalServiceProvider(provider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CatalogContext>();
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
var logger = scopedServices
.GetRequiredService<ILogger<WebTestFixture>>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
// Seed the database with test data.
CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();
// seed sample user data
var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}
Las pruebas pueden hacer uso de este WebApplicationFactory personalizado mediante él para crear un cliente y, a continuación, realizar solicitudes a la aplicación mediante esta instancia de cliente. La aplicación tendrá datos propagados que se pueden usar como parte de las aserciones de la prueba. La siguiente prueba comprueba que la página principal de la aplicación eShopOnWeb se carga correctamente e incluye una lista de productos que se agregó a la aplicación como parte de los datos de inicialización.
using Microsoft.eShopWeb.FunctionalTests.Web;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages;
[Collection("Sequential")]
public class HomePageOnGet : IClassFixture<WebTestFixture>
{
public HomePageOnGet(WebTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsHomePageWithProductListing()
{
// Arrange & Act
var response = await Client.GetAsync("/");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
// Assert
Assert.Contains(".NET Bot Black Sweatshirt", stringResponse);
}
}
Esta prueba funcional evalúa el conjunto completo de aplicaciones ASP.NET Core MVC / Razor Pages, incluyendo todos los middleware, filtros y enlazadores que puedan estar presentes. Comprueba que una ruta determinada ("/") devuelve el código de estado correcto esperado y la salida HTML. Lo hace sin configurar un servidor web real y evita gran parte de la frágilidad que puede experimentar el uso de un servidor web real para las pruebas (por ejemplo, problemas con la configuración del firewall). Las pruebas funcionales que se ejecutan en TestServer suelen ser más lentas que las pruebas unitarias y de integración, pero son mucho más rápidas que las pruebas que se ejecutarían a través de la red en un servidor web de prueba. Utilice pruebas funcionales para garantizar que la interfaz de la aplicación funcione según lo previsto. Estas pruebas son especialmente útiles cuando se encuentra duplicación en los controladores o páginas y se aborda la duplicación mediante la adición de filtros. Lo ideal es que esta refactorización no cambie el comportamiento de la aplicación y un conjunto de pruebas funcionales comprobará que este es el caso.
Referencias: prueba de aplicaciones ASP.NET Core MVC
- Pruebas en ASP.NET Core
https://learn.microsoft.com/aspnet/core/testing/- Convención de nomenclatura de pruebas unitarias
https://ardalis.com/unit-test-naming-convention- Prueba de EF Core
https://learn.microsoft.com/ef/core/miscellaneous/testing/- Pruebas de integración en ASP.NET Core
https://learn.microsoft.com/aspnet/core/test/integration-tests