Ir al contenido
wannacode

Pruebas de componentes

En resumen: Un escenario es para los ejemplos que el negocio acordó y quiere leer. Una prueba de componente llama directamente al código: diez importes en milisegundos, sin pasos y sin Gherkin. Las dos comprueban una regla para lectores distintos. Los escenarios se quedan pocos, y las pruebas de componente se encargan de los casos técnicos.

Problema

Quince escenarios para un límite

El programador decidió comprobar el envío como es debido: 1 céntimo, 10 €, 49,49 €, 49,50 €, 49,99 €, 50 €, 50,01 €, 100 €, 250 €, un importe negativo. Lo escribió todo como escenarios.

La dueña de la tienda abrió el archivo de escenarios para ver la regla del envío. Vio quince escenarios casi idénticos y cerró el archivo. La documentación viva ya no se leía.

La idea

La prueba llama directamente al código

Del curso de programación conoces las pruebas de componente: un método que llama al código y compara el resultado. La regla del envío no necesita ni Gherkin ni pasos. La prueba llama directamente al método:

src/test/java/shop/DeliveryTest.java
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}

Esto es una prueba parametrizada: una comprobación y una tabla de casos. Se parece a un esquema del escenario con «Ejemplos», pero es para el programador. Un caso nuevo es una fila nueva en la tabla.

El rechazo se comprueba con una prueba aparte. La regla lanza una excepción, y la prueba comprueba que lo hace:

src/test/java/shop/DeliveryTest.java
@Test
@DisplayName("un carrito vacío se rechaza")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
Más precisamente, para curiosos

Las herramientas son las estándar de cada lenguaje: JUnit 5 (las pruebas parametrizadas viven en el módulo junit-jupiter-params), xUnit con [Theory] y [InlineData], pytest, y node:test, incorporado en Node. En el curso de programación las pruebas de Python usaban unittest. pytest está más extendido: una prueba es una función normal con assert, y parametrize construye la tabla.

La idea

Qué regla en qué nivel

La regla es una. Las comprobaciones están en dos niveles, y cada uno tiene su lector:

  • Un escenario es para los ejemplos que el negocio acordó: exactamente 50,00 €, 49,99 €, un carrito vacío. Uno o dos ejemplos por regla. Es la documentación viva, y el negocio la lee.
  • Una prueba de componente es para los casos técnicos: 1 céntimo, un importe grande, un importe negativo, el redondeo. Protegen el código, pero no dicen nada nuevo sobre la regla.

Comprobar el límite en los dos niveles no es un desperdicio. El escenario dice lo que se acordó. La prueba de componente falla primero, y exactamente donde está el error.

La regla para elegir es corta: ¿querría el negocio leer este ejemplo? Si sí, un escenario. Si no, una prueba de componente.

Pruébalo

¿Escenario o prueba de componente?

1. «Un carrito de exactamente 50,00 €: el envío es gratis». El negocio lo acordó en la reunión de la historia.
2. Un importe de 1 céntimo, un importe de 250 € y un importe de 99 999,99 €.
3. Un importe negativo, digamos -5,00 €, que ningún cliente puede introducir.

La idea

La ejecución

El archivo de pruebas entero, ejecutado contra el código correcto. Todo pasa:

src/test/java/shop/DeliveryTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeliveryTest {
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}
@Test
@DisplayName("un carrito vacío se rechaza")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
}
Salida
OK feeFor(1) = 490
OK feeFor(25000) = 0
OK feeFor(4999) = 490
OK feeFor(5000) = 0
OK un carrito vacío se rechaza
Pruebas: 5, pasan: 5, fallan: 0

Ahora el programador «simplifica» la comparación y redondea el importe a euros enteros antes de ella. 49,99 € se convierte en 50 €, y el envío pasa a ser gratis:

src/main/java/shop/Delivery.java
package shop;
public class Delivery {
public static final int FREE_FROM = 5000;
public static final int FEE = 490;
// La tarifa de envío en céntimos. total es el total del carrito, también en céntimos.
public static int feeFor(int total) {
if (total <= 0) {
throw new IllegalArgumentException("Un carrito vacío no se puede pedir");
}
return Math.round(total / 100.0) * 100 >= FREE_FROM ? 0 : FEE;
}
}

Las mismas pruebas, contra el código nuevo:

src/test/java/shop/DeliveryTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeliveryTest {
@ParameterizedTest(name = "feeFor({0}) = {1}")
@CsvSource({ "4999, 490", "5000, 0", "1, 490", "25000, 0" })
void feeForTotal(int total, int fee) {
assertEquals(fee, Delivery.feeFor(total));
}
@Test
@DisplayName("un carrito vacío se rechaza")
void emptyCartIsRefused() {
assertThrows(IllegalArgumentException.class, () -> Delivery.feeFor(0));
}
}
Salida
FALLA feeFor(4999) = 490: expected: <490> but was: <0>
OK feeFor(1) = 490
OK feeFor(25000) = 0
OK feeFor(5000) = 0
OK un carrito vacío se rechaza
Pruebas: 5, pasan: 4, fallan: 1

Falla exactamente un caso: 49,99 €. El nombre de la prueba te dice el importe, y el mensaje te dice el valor esperado y el real. No tienes que buscar qué paso de qué escenario.

Más precisamente, para curiosos

La salida aquí es un informe corto, ordenado por nombre: las herramientas se ejecutan de verdad, pero dentro de un envoltorio que imprime solo la línea de cada prueba y el recuento. Los nombres de las pruebas difieren entre lenguajes porque cada herramienta los construye a su manera: JUnit a partir del name de la prueba parametrizada, xUnit a partir del nombre del método y los argumentos, pytest a partir del nombre de la función y los valores, node:test a partir del texto que le das.

La idea

Por qué no todo en Gherkin

Gherkin es para la conversación con el negocio: los ejemplos acordados por los tres amigos (three amigos), escritos para que todos puedan leerlos. Como herramienta de pruebas es cara:

  • cada línea nueva de un escenario necesita un paso, y los pasos hay que mantenerlos;
  • un escenario pasa por más código, así que es más lento;
  • cuando falla, la causa está más lejos del mensaje.

Y lo más importante: quince escenarios técnicos esconden los tres que enuncian la regla. La documentación viva sigue viva solo mientras alguien la lee.

Así que los escenarios son pocos y enuncian la regla. Las pruebas de componente son muchas y protegen el código. Esto es también la pirámide: una base ancha de pruebas rápidas, y encima menos pruebas, más lentas y más cercanas al cliente.

Más precisamente, para curiosos

Aslak Hellesøy, que creó Cucumber, ha escrito más de una vez que Cucumber es ante todo una herramienta para un lenguaje compartido y la colaboración, y que las pruebas son un subproducto. Si los escenarios no se escriben y se leen junto con el negocio, lo que queda es una manera cara de escribir pruebas.

Ejercicio

Los dos niveles

1. Qué prueba protege qué. Las preguntas son sobre los escenarios de la lección anterior y las pruebas de esta.

1. Quitas el caso de 250,00 € (25000 céntimos) de la prueba parametrizada. ¿Qué error ya no atrapará ninguna prueba de la lección?
2. ¿Qué prueba atrapa el error «un carrito vacío no se rechaza sino que paga 4,90 €»?
3. El programador quiere añadir 49,50 € tanto a los escenarios como a la prueba de componente. ¿Qué le dices?

Test

Ponte a prueba

1. ¿Cuál es la diferencia entre un escenario y una prueba de componente para una regla?
2. ¿Por qué una prueba de componente es más rápida que un escenario?
3. ¿Qué hace una prueba parametrizada?
4. ¿Por qué no todo se escribe en Gherkin?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·