Procedimientos recomendados de pruebas unitarias con .NET Core y .NET Standard
Artikulua
La escritura de pruebas unitarias reporta muchos beneficios; las pruebas ayudan con la regresión, proporcionan documentación y facilitan un buen diseño. Pero también son difíciles de leer y, si son frágiles, pueden causar estragos en el código base. En este artículo se describen algunos procedimientos recomendados sobre el diseño de pruebas unitarias para proyectos de .NET Core y .NET Standard.
En esta guía, aprenderá algunos procedimientos recomendados para escribir pruebas unitarias resistentes y fáciles de entender.
Las pruebas funcionales son costosas. Normalmente implican la apertura de la aplicación y la realización de una serie de pasos que usted (u otro usuario) debe seguir para validar el comportamiento esperado. Es posible que estos pasos no siempre sean conocidos para el evaluador. Tendrán que ponerse en contacto con alguien más experto en el área con el fin de llevar a cabo la prueba. Las pruebas en sí mismas podrían llevar segundos en el caso de cambios triviales, o minutos en el de cambios más importantes. Por último, este proceso debe repetirse para cada cambio que se realice en el sistema.
Por otra parte, las pruebas unitarias duran milisegundos, se pueden ejecutar con solo presionar un botón y no exigen necesariamente ningún conocimiento del sistema en general. El que la prueba se supere o no depende del ejecutor de pruebas, no del usuario.
Protección frente a regresión
Los defectos de regresión son aquellos que se presentan cuando se realiza un cambio en la aplicación. Es habitual que los evaluadores no solo prueben una nueva característica, sino también las ya existentes con el fin de comprobar que las características implementadas anteriormente siguen funcionando según lo previsto.
Con las pruebas unitarias, es posible volver a ejecutar el conjunto completo de pruebas después de cada compilación o incluso después de cambiar una línea de código. Eso da confianza en que el nuevo código no interrumpa la funcionalidad existente.
Documentación ejecutable
Es posible que no siempre sea evidente lo que hace un método determinado o cómo se comporta ante una acción determinada. Es posible que se pregunte: ¿cómo se comporta este método si se le pasa una cadena en blanco? ¿Null?
Si tiene un conjunto de pruebas unitarias con un nombre adecuado, cada prueba debe ser capaz de explicar con claridad el resultado esperado de una acción determinada. Además, debe ser capaz de comprobar que funciona.
Menos código acoplado
Cuando el código está estrechamente acoplado, puede resultar difícil realizar pruebas unitarias. Sin crear pruebas unitarias para el código que se está escribiendo, el acoplamiento puede ser menos evidente.
Al escribir pruebas para el código, este se desacopla naturalmente, ya que, de otra forma, sería más difícil de probar.
Características de una buena prueba unitaria
Rápida: no es poco frecuente que los proyectos maduros tengan miles de pruebas unitarias. Las pruebas unitarias deberían tardar poco tiempo en ejecutarse. Milisegundos.
Aislada: las pruebas unitarias son independientes, se pueden ejecutar de forma aislada y no tienen ninguna dependencia de ningún factor externo, como un sistema de archivos o una base de datos.
Repetible: la ejecución de una prueba unitaria debe ser coherente con sus resultados, es decir, devolver siempre el mismo resultado si no cambia nada entre ejecuciones.
Autocomprobación: la prueba debe ser capaz de detectar automáticamente si el resultado ha sido correcto o incorrecto sin necesidad de intervención humana.
Puntual: una prueba unitaria no debe tardar un tiempo desproporcionado en escribirse en comparación con el código que se va a probar. Si observa que la prueba del código tarda mucho en comparación con su escritura, considere un diseño más fácil de probar.
Cobertura de código
Un alto porcentaje de cobertura de código suele ir asociado a una mayor calidad del código. Sin embargo, la propia medida no puede determinar la calidad del código. La configuración de un objetivo de porcentaje de cobertura de código excesivamente ambicioso puede ser contraproducente. Imagine un proyecto complejo con miles de ramas condicionales e imagine que establece un objetivo de cobertura de código del 95 %. Actualmente, el proyecto mantiene una cobertura de código del 90 %. La cantidad de tiempo que lleva cubrir todos los casos del 5 % restante puede ser un esfuerzo enorme y la propuesta de valor disminuye rápidamente.
Un alto porcentaje de cobertura de código no es un indicador de éxito, ni implica una alta calidad del código. Simplemente representa la cantidad de código cubierta por las pruebas unitarias. Para obtener más información, vea Cobertura de código de pruebas unitarias.
Vamos a hablar el mismo idioma
Desafortunadamente, el término ficticio se emplea de forma incorrecta al referirse a las pruebas. Los puntos siguientes definen los tipos más comunes de emulaciones al escribir pruebas unitarias:
Emulación: una emulación es un término genérico que se puede usar para describir un stub o un objeto ficticio. Si es un stub o un objeto ficticio depende del contexto en el que se use. Es decir, una emulación puede ser un stub o un objeto ficticio.
Objeto ficticio: un objeto ficticio es una emulación del sistema que decide si una prueba unitaria se ha superado o no. Un objeto ficticio comienza como una emulación hasta que se declara una instrucción Assert en ella.
Stub: un stub es un reemplazo controlable para una dependencia existente (o colaborador) en el sistema. Con un stub, puede probar el código sin tratar directamente con la dependencia. De forma predeterminada, un stub empieza como una emulación.
Tenga en cuenta el fragmento de código siguiente:
C#
var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
El ejemplo anterior sería de un código auxiliar al que se hace referencia como simulación. En este caso, es un código auxiliar. Simplemente está pasando el pedido para poder crear una instancia de Purchase (el sistema sometido a prueba). El nombre MockOrder también es confuso porque, una vez más, el pedido no es un objeto ficticio.
Un mejor enfoque sería:
C#
var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Al cambiar el nombre de la clase a FakeOrder, ha hecho que la clase sea mucho más genérica. La clase se puede usar como simulación o código auxiliar, lo que sea mejor para el caso de prueba. En el ejemplo anterior, FakeOrder se usa como código auxiliar. No usa FakeOrder de ninguna forma durante la aserción. FakeOrder se ha pasado a la clase Purchase para satisfacer los requisitos del constructor.
Para usarla como un objeto ficticio, podría hacer algo parecido al código siguiente:
C#
var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Validated);
En este caso, va a comprobar una propiedad en la emulación (afirmando sobre ella), por lo que en el fragmento de código anterior, mockOrder es un objeto simulado.
Garrantzitsua
Es importante tener clara esta terminología. Si se llama a los stubs "objetos ficticios", los demás desarrolladores van a hacer suposiciones falsas sobre su intención.
Lo principal que debe recordar sobre los objetos ficticios frente a los stubs es que los objetos ficticios son como los stubs, pero se afirma sobre el objeto ficticio, y no sobre un stub.
procedimientos recomendados
Estos son algunos de los procedimientos recomendados más importantes para escribir pruebas unitarias.
Evitar dependencias de infraestructura
No intente incluir dependencias en la infraestructura al escribir pruebas unitarias. Las dependencias vuelven las pruebas lentas y frágiles, y se deben reservar para las pruebas de integración. Puede evitar esas dependencias en su aplicación si sigue el Explicit Dependencies Principle (Principio de dependencias explícitas) y usando la Inserción de dependencias. También puede mantener las pruebas unitarias en un proyecto separado de las pruebas de integración. Este enfoque garantiza que el proyecto de pruebas unitarias no tenga referencias a paquetes de infraestructura ni dependencias de estos.
Asignar nombre a las pruebas
El nombre de la prueba debe constar de tres partes:
Nombre del método que se va a probar.
Escenario en el que se está probando.
Comportamiento esperado al invocar al escenario.
¿Por qué?
Los estándares de nomenclatura son importantes porque expresan de forma explícita la intención de la prueba. Las pruebas van más allá de garantizar que el código funciona, también proporcionan documentación. Con solo mirar el conjunto de pruebas unitarias, debe ser capaz de deducir el comportamiento del código sin ni siquiera mirar el propio código. Además, cuando no se superan las pruebas, puede ver exactamente qué escenarios no cumplen las expectativas.
Malo:
C#
[Fact]
publicvoidTest_Single()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Mejor:
C#
[Fact]
publicvoidAdd_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Organizar las pruebas
Organizar, actuar, declarar es un patrón común al realizar pruebas unitarias. Como el propio nombre implica, consta de tres acciones principales:
Organizar los objetos, crearlos y configurarlos según sea necesario.
Actuar en un objeto.
Declarar que algo es como se espera.
¿Por qué?
Separa claramente lo que se está probando de los pasos organizar y declarar.
Menos posibilidad de mezclar aserciones con el código para "actuar".
La legibilidad es uno de los aspectos más importantes a la hora de escribir una prueba. Al separar cada una de estas acciones dentro de la prueba, se resaltan claramente las dependencias necesarias para llamar al código, la forma de llamarlo y lo que se intenta afirmar. Aunque es posible combinar algunos pasos y reducir el tamaño de la prueba, el objetivo principal es que la prueba sea lo más legible posible.
[Fact]
publicvoidAdd_EmptyString_ReturnsZero()
{
// Arrangevar stringCalculator = new StringCalculator();
// Actvar actual = stringCalculator.Add("");
// Assert
Assert.Equal(0, actual);
}
Escribir pruebas correctas lo más sencillas posible
La entrada que se use en una prueba unitaria debe ser lo más sencilla posible para comprobar el comportamiento que se está probando actualmente.
¿Por qué?
Las pruebas se hacen más resistentes a los cambios futuros en el código base.
Más cercano al comportamiento de prueba que a la implementación.
Las pruebas que incluyen más información de la necesaria para superarse tienen una mayor posibilidad de incorporar errores en la prueba y pueden hacer confusa su intención. Al escribir pruebas, queremos centrarnos en el comportamiento. El establecimiento de propiedades adicionales en los modelos o el empleo de valores distintos de cero cuando no es necesario solo resta de lo que se quiere probar.
Malo:
C#
[Fact]
publicvoidAdd_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("42");
Assert.Equal(42, actual);
}
Mejor:
C#
[Fact]
publicvoidAdd_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Evitar cadenas mágicas
La asignación de nombres a las variables de las pruebas unitarias es tan importante, si no más, que la asignación de nombres a las variables del código de producción. Las pruebas unitarias no deben contener cadenas mágicas.
¿Por qué?
Evita la necesidad de que el lector de la prueba inspeccione el código de producción con el fin de averiguar lo que hace que el valor sea especial.
Muestra explícitamente lo que se intenta probar, en lugar de lo que se intenta lograr.
Las cadenas mágicas pueden provocar confusión al lector de las pruebas. Si una cadena tiene un aspecto fuera de lo normal, puede preguntarse por qué se ha elegido un determinado valor para un parámetro o valor devuelto. Este tipo de valor de cadena puede dar lugar a un vistazo más detallado a los detalles de implementación, en lugar de centrarse en la prueba.
Eskupekoa
Al escribir pruebas, su objetivo debe ser expresar tanta intención como sea posible. En el caso de las cadenas mágicas, un buen enfoque es asignar estos valores a constantes.
Malo:
C#
[Fact]
publicvoidAdd_BigNumber_ThrowsException()
{
var stringCalculator = new StringCalculator();
Action actual = () => stringCalculator.Add("1001");
Assert.Throws<OverflowException>(actual);
}
Mejor:
C#
[Fact]
voidAdd_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
conststring MAXIMUM_RESULT = "1001";
Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
Assert.Throws<OverflowException>(actual);
}
Evitar la lógica en las pruebas
Al escribir las pruebas unitarias, evite la concatenación manual de cadenas y las condiciones lógicas como if, while, for, switch y otras condiciones.
¿Por qué?
Menos posibilidad de incorporar un error a las pruebas.
El foco está en el resultado final, en lugar de en los detalles de implementación.
Al incorporar lógica al conjunto de pruebas, aumenta considerablemente la posibilidad de agregar un error. El último lugar en el que se quiere encontrar un error es el conjunto de pruebas. Debe tener un alto nivel de confianza de que las pruebas funcionen; de lo contrario, no confiará en ellas. Las pruebas en las que no se confía no proporcionan ningún valor. Cuando se produce un error en una prueba, quiere saber que algo va mal con el código y que no se puede omitir.
Eskupekoa
Si la lógica en la prueba parece inevitable, considere la posibilidad de dividirla en dos o más pruebas diferentes.
Malo:
C#
[Fact]
publicvoidAdd_MultipleNumbers_ReturnsCorrectResults()
{
var stringCalculator = new StringCalculator();
var expected = 0;
var testCases = new[]
{
"0,0,0",
"0,1,2",
"1,2,3"
};
foreach (var test in testCases)
{
Assert.Equal(expected, stringCalculator.Add(test));
expected += 3;
}
}
Mejor:
C#
[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
publicvoidAdd_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add(input);
Assert.Equal(expected, actual);
}
Se prefieren métodos auxiliares a la instalación y el desmontaje
Si necesita un objeto o un estado similares para las pruebas, se prefiere un método auxiliar al uso de los atributos Setup y Teardown, si existen.
¿Por qué?
Menos confusión al leer las pruebas, puesto que todo el código es visible desde dentro de cada prueba.
Menor posibilidad de configurar demasiado o muy poco para la prueba.
Menor posibilidad de compartir el estado entre las pruebas, lo que crea dependencias no deseadas entre ellas.
En los marcos de trabajo de pruebas unitarias, se llama a Setup antes de cada prueba unitaria del conjunto de pruebas. Aunque algunos puedan verlo como una herramienta útil, por lo general termina por dar lugar a pruebas recargadas y difíciles de leer. Cada prueba normalmente tendrá requisitos diferentes para funcionar y ejecutarse. Por desgracia, Setup obliga a usar los mismos requisitos para cada prueba.
Oharra
xUnit ha quitado la instalación y el desmontaje a partir de la versión 2.x
Malo:
C#
privatereadonly StringCalculator stringCalculator;
publicStringCalculatorTests()
{
stringCalculator = new StringCalculator();
}
C#
// more tests...
C#
[Fact]
publicvoidAdd_TwoNumbers_ReturnsSumOfNumbers()
{
var result = stringCalculator.Add("0,1");
Assert.Equal(1, result);
}
Mejor:
C#
[Fact]
publicvoidAdd_TwoNumbers_ReturnsSumOfNumbers()
{
var stringCalculator = CreateDefaultStringCalculator();
var actual = stringCalculator.Add("0,1");
Assert.Equal(1, actual);
}
Al escribir las pruebas, intente incluir solo una actuación por prueba. Entre los enfoques comunes para usar solo una actuación se incluyen los siguientes:
Crear una prueba independiente para cada actuación.
Usar pruebas con parámetros.
¿Por qué?
Cuando se produce un error en la prueba, está claro qué actuación produce el error.
Garantiza que la prueba se centre en un solo caso.
Proporciona la imagen completa de por qué se producen errores en las pruebas.
Varias actuaciones se deben afirmar individualmente y no se garantiza que se ejecuten todas las afirmaciones. En la mayoría de los marcos de pruebas unitarias, cuando se produce un error de una declaración en una prueba unitaria, se considera de forma automática que las pruebas siguientes tienen errores. Este tipo de proceso puede ser confuso, ya que una funcionalidad que realmente está funcionando se muestra como errónea.
[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
publicvoidAdd_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
// Arrangevar stringCalculator = new StringCalculator();
// Actvar actual = stringCalculator.Add(input);
// Assert
Assert.Equal(expected, actual);
}
Validar métodos privados mediante la prueba unitaria de métodos públicos
En la mayoría de los casos, no es necesario probar un método privado. Los métodos privados son un detalle de implementación y nunca existen de forma aislada. En algún momento, va a haber un método público que llame al método privado como parte de su implementación. Lo que debería importarle es el resultado final del método público que llama al privado.
Su primera reacción puede ser empezar a escribir una prueba para TrimInput porque quiere asegurarse de que el método funcione según lo previsto. Sin embargo, es muy posible que ParseLogLine manipule a sanitizedInput de una forma inesperada, con lo que una prueba de TrimInput sería inútil.
La prueba real debe realizarse en el método público ParseLogLine, porque eso es lo debe importarle en última instancia.
C#
publicvoidParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
var parser = new Parser();
var result = parser.ParseLogLine(" a ");
Assert.Equals("a", result);
}
Con este punto de vista, si ve un método privado, busque el método público y escriba las pruebas en ese método. Simplemente porque un método privado devuelva el resultado esperado, no significa que el sistema que finalmente llama al método privado use el resultado correctamente.
Referencias estáticas de stub
Uno de los principios de una prueba unitaria es que debe tener control total del sistema sometido a prueba. Este principio puede ser problemático cuando el código de producción incluye llamadas a referencias estáticas (por ejemplo, DateTime.Now). Observe el código siguiente:
¿Cómo se podrían realizar pruebas unitarias de este código? Puede probar un enfoque como:
C#
publicvoidGetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(2, actual)
}
publicvoidGetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(1, actual);
}
Desafortunadamente, se dará cuenta rápidamente de que hay un par de problemas con las pruebas.
Si el conjunto de pruebas se ejecuta un martes, se superará la segunda prueba, pero se producirá un error en la primera.
Si el conjunto de pruebas se ejecuta otro día, se superará la primera prueba, pero se producirá un error en la segunda.
Para solucionar estos problemas, debe incorporar un arreglo en el código de producción. Un enfoque consiste en encapsular el código que necesita controlar en una interfaz y hacer que el código de producción dependa de esa interfaz.
El conjunto de pruebas ahora se convierte en el siguiente:
C#
publicvoidGetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(2, actual);
}
publicvoidGetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(1, actual);
}
Ahora el conjunto de pruebas tiene control total sobre DateTime.Now y puede convertir en stub cualquier valor al llamar al método.
Jardun lankidetzan gurekin GitHub-en
Eduki honen iturburua GitHub-en aurki daiteke, bertan arazoak eta aldaketak egiteko eskaerak sortu eta berrikus ditzakezu. Informazio gehiagorako, ikusi gure kolaboratzaileen gida.
.NET oharrak
.NET iturburu irekiko proiektu bat da. Hautatu esteka bat oharrak bidaltzeko:
Bat egin IAren soluzio eskalagarrien soluzioak sortzeko topaketa sortarekin, mundu errealaren erabilera-kasuetan oinarrituak, beste garatzaile eta aditu batzuekin.
Comience a probar las aplicaciones de C# mediante las herramientas de prueba de Visual Studio. Aprenda a escribir pruebas, usar el explorador de pruebas, crear conjuntos de pruebas y aplicar el patrón rojo, verde y de refactorización para escribir código.
Aprenda los conceptos de pruebas unitarias en C# y .NET mediante una experiencia interactiva centrada en la creación de una solución de ejemplo paso a paso con pruebas de dotnet y xUnit.
Esta es la tercera de una serie de cuatro partes donde Robert se une a Phil Japikse para analizar las pruebas unitarias. Esta serie se expande en el episodio de 2017 Unit Testing. En este episodio, Robert y Phil cubren el marco moq. La simulación proporciona la capacidad de simular un objeto. Por ejemplo, puede probar una llamada a una base de datos sin tener que comunicarse realmente con ella. El marco Moq es un marco de pruebas unitarias de código abierto que funciona muy bien con código .NET y Phil
Aprenda los conceptos de pruebas unitarias en C# y .NET mediante una experiencia interactiva centrada en la creación de una solución de ejemplo paso a paso con pruebas de dotnet y MSTest.
Aprenda a usar el marco de pruebas unitarias de Microsoft para código administrado para configurar un método de prueba unitaria para recuperar valores de un origen de datos.