Saltar a contenido

A4.A Qué es una prueba y cómo se escribe

Para quién es esto

Es material avanzado: da por sabidas las unidades 1 a 5. La unidad 1 ya te enseñó una primera prueba; aquí aprendes a escribirlas con método.

Una prueba automática es un programa corto que ejecuta tu código con unos datos y comprueba el resultado por ti. Lo que compruebas a mano una vez, la prueba lo comprueba todas las veces, en segundos, cada vez que cambias algo. Si rompes algo sin querer, lo sabes al momento y no dentro de una semana.

La receta: preparar, actuar, comprobar

Casi todas las pruebas siguen tres pasos (Arrange, Act, Assert):

Paso Qué haces En el ejemplo
Preparar Creas los objetos y los datos Un carrito nuevo
Actuar Llamas al código que se prueba agregar('tarta', 1800, 2)
Comprobar Dices qué esperas El total es 3600

Una buena prueba comprueba una sola cosa, tiene un nombre que dice lo que debe ocurrir y no depende de otra prueba.

El marco de pruebas de Kotlin

Kotlin usa kotlin.test, que se apoya en el marco de la plataforma. Cada función con @Test es una prueba y se comprueba con assertEquals, assertTrue... Las funciones pueden tener nombres con espacios entre acentos graves, que se leen como una frase. Se ejecutan desde el IDE o con gradle test. Las salidas de abajo vienen de ejecutarlas con JUnit 4 (kotlin-test-junit).

Un ejemplo: un carrito de la compra

Primero, el código que se prueba. Los precios van en céntimos (enteros) para evitar errores de decimales:

carrito.kt

// El código que se prueba: un carrito de la compra.
// Los precios van en céntimos (números enteros) para evitar errores de redondeo con decimales.
class Carrito {
    private data class Linea(val producto: String, val precio: Int, val cantidad: Int)

    private val lineas = mutableListOf<Linea>()

    fun agregar(producto: String, precio: Int, cantidad: Int) {
        require(cantidad > 0) { "la cantidad debe ser mayor que cero" }
        require(precio >= 0) { "el precio no puede ser negativo" }
        lineas.add(Linea(producto, precio, cantidad))
    }

    val total: Int get() = lineas.sumOf { it.precio * it.cantidad }
    val unidades: Int get() = lineas.sumOf { it.cantidad }
    val vacio: Boolean get() = lineas.isEmpty()

    /** El total después de aplicar un descuento de 0 a 100 por ciento. */
    fun conDescuento(porcentaje: Int): Int {
        require(porcentaje in 0..100) { "el porcentaje debe estar entre 0 y 100" }
        return total - total * porcentaje / 100
    }
}

Y sus tres primeras pruebas:

CarritoTest.kt

import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

// kotlin.test: las mismas anotaciones valen sobre JUnit 4 o JUnit 5; solo cambia la dependencia del proyecto.
class CarritoTest {
    private lateinit var carrito: Carrito

    @BeforeTest
    fun preparar() {
        carrito = Carrito() // un carrito nuevo antes de CADA prueba: así no dependen unas de otras
    }

    @Test
    fun `un carrito nuevo está vacío y su total es 0`() {
        assertTrue(carrito.vacio)
        assertEquals(0, carrito.total)
    }

    @Test
    fun `agregar un producto suma su importe`() {
        carrito.agregar("tarta", 1800, 2)
        assertEquals(3600, carrito.total)
        assertEquals(2, carrito.unidades)
    }

    @Test
    fun `con varios productos, el total es la suma de los importes`() {
        carrito.agregar("tarta", 1800, 2)
        carrito.agregar("galleta", 100, 12)
        assertEquals(4800, carrito.total)
    }
}

El método con @BeforeTest se ejecuta antes de cada prueba. Por eso el carrito se vuelve a crear: ninguna prueba hereda lo que hizo la anterior.

Al ejecutar las pruebas:

...
OK (3 tests)

Cada punto es una prueba que pasó. OK (3 tests) significa que se ejecutaron tres y las tres pasaron.

Cuando una prueba falla

Para ver un fallo de verdad, rompí el código a propósito: cambié precio × cantidad por precio + cantidad en el cálculo del total. Esto es lo que se ve:

1) agregar un producto suma su importe(CarritoTest)
java.lang.AssertionError: expected:<3600> but was:<1802>
    at CarritoTest.agregar un producto suma su importe(CarritoTest.kt:24)

Un buen mensaje de fallo dice qué prueba falló, qué se esperaba (3600) y qué se obtuvo (1802). Con eso suele bastar para encontrar el error sin depurar.

Una prueba que nunca ha fallado no demuestra nada

Cuando escribas una prueba, rómpela una vez a propósito (cambia el resultado esperado o el código) y comprueba que falla. Así sabes que de verdad está mirando algo.

Errores frecuentes

Error Cómo evitarlo
Pruebas que dependen unas de otras (el orden importa) Prepara de cero antes de cada prueba
Nombres como test1, test2 Describe el comportamiento: «agregar un producto suma su importe»
Comprobar muchas cosas distintas en una sola prueba Una idea por prueba: si falla, sabes qué falló
Copiar en la prueba el mismo cálculo que hace el código Escribe el resultado esperado a mano (3600), no precio * cantidad

Para practicar

Los ejercicios A4.1 y A4.4 piden escribir una función con sus pruebas y una preparación previa. Para ver la misma idea en los otros lenguajes: Pasar de uno a otro.