Saltar al contenido
K
Kotlin: el lenguajePaso a paso35 min de lectura · 8 min de máquina Básico

Bucles y tu primer test

Al terminar: Repetir con rangos y bucles, y escribir tu primer test con kotlin.test.

Al terminar sabrás repetir instrucciones con bucles, sin copiarlas, y tendrás tu primer test: un trozo de código que comprueba que puntosPor sigue dando los puntos que debe, en menos de un segundo y sin abrir la app. También tendrás un main() que recorre jornadas y marcadores con bucles.

Importa porque casi todo programa repite algo (cada jornada, cada jugador, cada pantalla de una lista). Y porque un test te avisa cuando rompes una regla sin que tengas que ejecutar el programa y mirar el resultado.

Temas: rangos, for y while; += y *=; tu primer test con kotlin.test.

Rangos y bucles

Un bucle repite unas instrucciones sin que las copies: escribes el bloque una vez y se ejecuta varias veces. Cada repetición se llama vuelta. Sirve para recorrer cosas («cada jornada», «cada asiento») o para repetir hasta que algo ocurra.

Hay dos bucles. for (i in 1..3) repite una vez por cada valor de un conjunto: la variable i vale 1 en la primera vuelta, 2 en la segunda y 3 en la tercera. while (condición) repite mientras la condición (la pregunta de sí o no de la lección anterior) sea verdadera; la comprueba antes de cada vuelta.

Para decir «del 1 al 3» se usa un rango: una secuencia de números seguidos con un principio y un final.

Se escribeRecorre
1..31, 2, 3 (incluye el final)
1 until 41, 2, 3 (excluye el final)
10 downTo 0 step 510, 5, 0
in 0..45pregunta «¿está entre 0 y 45?»

downTo cuenta hacia atrás y step fija de cuánto en cuánto se salta. Para cambiar una variable, += suma y guarda (x += 2 es x = x + 2) y *= multiplica y guarda (x *= 2 es x = x * 2). Un for puede ir dentro de otro (se dice que están anidados): por cada vuelta del de fuera, el de dentro da todas las suyas.

ejemplo mínimo: rangos y bucles (con comentarios)
fun main() {
    for (fila in 1..2) {                    // fila vale 1 y después 2
        for (asiento in 1..2) {             // un for dentro de otro: por cada fila, todos los asientos
            println("Asiento $fila-$asiento")
        }
    }
    for (segundos in 10 downTo 0 step 5) {  // cuenta atrás de 5 en 5: 10, 5, 0
        println("Faltan $segundos s")
    }
    var litros = 0
    while (litros < 10) litros += 4         // repite mientras se cumpla. Con una sola línea, sin llaves
    println("Depósito: $litros litros")     // 12: la vuelta que pasa de 10 es la última
}
Asiento 1-1
Asiento 1-2
Asiento 2-1
Asiento 2-2
Faltan 10 s
Faltan 5 s
Faltan 0 s
Depósito: 12 litros

Reto · Ejercicio: tablas y cuentas atrás

En el Kotlin Playground (el mismo de Tu primer proyecto en Kotlin; no toques tu proyecto), escribe un main con tres bucles. Un for imprime la tabla del 3 del 1 al 5. Otro for cuenta atrás de 9 a 0 de 3 en 3. Un while parte de un precio de 5, lo duplica mientras no pase de 100 e imprime con qué valor terminó. Pista: en el primer for, la cuenta 3 * i se puede hacer dentro del texto con ${…}, como viste en Funciones y parámetros. La consola debe mostrar:

3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
3 x 4 = 12
3 x 5 = 15
Faltan 9
Faltan 6
Faltan 3
Faltan 0
Pasó de 100 con 160
Ver solución
solución del ejercicio
fun main() {
    for (i in 1..5) {                       // el rango incluye el 5
        println("3 x $i = ${3 * i}")        // ${…} hace la cuenta dentro del texto
    }
    for (n in 9 downTo 0 step 3) {          // 9, 6, 3, 0
        println("Faltan $n")
    }
    var precio = 5
    // *= multiplica y guarda, como +=
    while (precio <= 100) precio *= 2       // 5, 10, 20, 40, 80, 160: para al pasar de 100
    println("Pasó de 100 con $precio")
}

Tu primer test

Un test es una función que llama a tu código y compara lo que devuelve con lo que esperas. kotlin.test es la librería de Kotlin para escribirlos, y ya la tienes: la instalaste en :torneo en Los ficheros de Gradle de tu proyecto, con testImplementation(libs.kotlin.test) en build.gradle.kts (Module :torneo). Si abres ese fichero, la línea sigue ahí. No tienes que instalar nada más.

Un test mínimo, para una función doble que no es del proyecto, con cada línea explicada:

ejemplo mínimo: un test, línea a línea (con comentarios)
package com.sebastiannarvaez.marcador.torneo     // el paquete del fichero, como en cualquier .kt
 
import kotlin.test.Test                          // trae la etiqueta @Test, que vive en otro sitio
import kotlin.test.assertEquals                  // trae la comprobación «son iguales»
import kotlin.test.assertTrue                    // trae la comprobación «esto es verdadero»
 
fun doble(x: Int) = x * 2                        // la función que vamos a examinar
 
class DobleTest {                                // una clase agrupa funciones bajo un nombre; los tests viven dentro de una
    @Test                                        // esta etiqueta marca la función de abajo como un test
    fun cincoSeDuplicaADiez() {                  // el nombre cuenta qué se comprueba, para leerlo cuando falle
        assertEquals(10, doble(5))               // assertEquals(esperado, real): el esperado va PRIMERO
    }
 
    @Test
    fun cuatroDuplicadoPasaDeCinco() {
        assertTrue(doble(4) > 5)                 // assertTrue(condición): falla si la condición es falsa
    }
}
  • import: trae a este fichero una pieza que vive en otro sitio (aquí, Test y las comprobaciones); sin él, Kotlin no sabe a qué te refieres. Android Studio suele ofrecer escribirlo por ti.
  • class …Test: una clase es, de momento, un contenedor con nombre que agrupa funciones; la verás a fondo en Clases y objetos. Los tests de una misma cosa van en una clase cuyo nombre acaba en Test por costumbre.
  • @Test: una anotación, una etiqueta que marca la función como un test. Sin ella, la función existe pero nadie la ejecuta.
  • assertEquals(esperado, real): compara dos valores. El primero es lo que debería salir y el segundo, lo que sale de verdad. Si te los cambias, el test sigue funcionando, pero el mensaje de error se lee al revés.
  • assertTrue(condición): pasa si la condición es verdadera. Hay su contrario, assertFalse.
  • Un test pasa si ninguna comprobación falla y falla en cuanto una no se cumple; no se queda con ningún resultado para devolver, así que en el ejemplo la función no lleva return. En el paso de abajo verás la forma corta, fun nombre() = assertEquals(…): es la misma función de expresión de Funciones y parámetros, solo que lo que hay tras el = es la propia comprobación, que no entrega ningún valor útil.

Se ejecutan con ./gradlew :torneo:test, en la Terminal de Android Studio (View → Tool Windows → Terminal, que se abre en la carpeta del proyecto, donde está gradlew). :torneo:test significa «la tarea test del módulo torneo», igual que :torneo:run. Gradle no imprime nada por cada test que pasa y termina en BUILD SUCCESSFUL.

Cómo contar los tests: el informe

Como Gradle no lista los tests que pasan, para saber cuántos se han ejecutado hay que abrir su informe. Cada vez que ejecutas ./gradlew :torneo:test, Gradle escribe en el disco un informe en el fichero torneo/build/reports/tests/test/index.html, y lo escribe siempre, también cuando todo pasa. Para abrirlo, ejecuta en la Terminal de Android Studio:

open torneo/build/reports/tests/test/index.html

open es la orden de macOS que abre un fichero con el programa que le toca, aquí el navegador. Arriba de la página verás un recuadro con el número de tests (por ejemplo, «5 tests») y, al lado, cuántos han fallado. Esa cifra es el recuento de tests que te pedirán los puntos de control de las lecciones siguientes: cada vez dirán «abre el informe y comprueba que pone N tests».

Paso · PuntosTest y el Main.kt con bucles

Objetivo: el primer test de verdad y un main() con bucles. PuntosTest.kt tiene cinco tests, los primeros que escribes; Main.kt se amplía con bucles. Al terminar, ./gradlew :torneo:test termina en BUILD SUCCESSFUL y ./gradlew :torneo:run imprime la tabla de marcadores.

torneo/
kotlin+java/
com.sebastiannarvaez.marcador.torneoel paquete que ya tienes
Puntos.ktya lo tienes, no se toca
Main.ktse modifica
com.sebastiannarvaez.marcador.torneo (test)se crea: los tests de :torneo
PuntosTest.ktse crea
Lo que tocas, en la vista Android. El paquete (test) es nuevo: lo creas en este paso.

1 · Crea el sitio de los tests. Los tests de un módulo viven aparte del código del programa y solo se compilan cuando ejecutas test. En el disco, el código del programa está dentro de una carpeta src/main y los tests, dentro de src/test (src viene de «source», código fuente).

La vista Android muestra las dos carpetas juntas, dentro de torneo → kotlin+java, y por eso verás dos paquetes con el mismo nombre: el del código y otro con la etiqueta (test). En :app ya tienes uno, com.sebastiannarvaez.marcador (test); :torneo todavía no tiene el suyo, y aquí lo creas. Android Studio lo muestra en cuanto hay un fichero dentro.

En la vista Android, haz clic derecho sobre el nodo torneo → New → File y escribe la ruta completa con el nombre del fichero: src/test/java/com/sebastiannarvaez/marcador/torneo/PuntosTest.kt. Pulsa Intro: Android Studio crea las carpetas y el fichero a la vez, y el paquete com.sebastiannarvaez.marcador.torneo (test) aparece solo dentro de torneo → kotlin+java. Es el camino que siempre funciona, porque una carpeta vacía puede no verse en la vista Android hasta que tiene un fichero dentro. Ojo: el fichero sale vacío, así que escribe package com.sebastiannarvaez.marcador.torneo como primera línea.

Piezas nuevas de este paso:

  • for (i in 1..3) { … }, for (m in 90 downTo 0 step 30) y while (condición) { … }: los bucles de arriba; un for dentro de otro recorre todas las parejas.
  • class PuntosTest { @Test fun nombre() = assertEquals(esperado, real) }: el test mínimo de arriba, con el cuerpo en una sola expresión.

Intenta escribirlo tú, en dos ficheros:

  • PuntosTest.kt: cinco tests. Tres comprueban puntosPor (una victoria 2-1 vale 3, un empate 0-0 vale 1, una derrota 0-3 vale 0) y dos encabezadoDeJornada (sin prefijo da «Jornada 2 de 5»; con el prefijo «Fecha» da «Fecha 1 de 3»).
  • Main.kt, que ya existe: sustituye todo su contenido. El programa presenta el torneo, escribe el encabezado de cada una de las tres jornadas con un bucle y uno con el prefijo «Fecha», calcula los puntos del local para todos los marcadores posibles de 0 a 2 goles por equipo, dice cuánto vale un empate, cuenta los minutos hacia atrás de 30 en 30 y, por último, avanza el partido de 45 en 45 minutos hasta el final.

Al ejecutar ./gradlew :torneo:run debe verse:

Torneo: Copa Barrio
Jornada 1 de 3
Jornada 2 de 3
Jornada 3 de 3
Fecha 1 de 3
0-0 -> 1 puntos para el local
0-1 -> 0 puntos para el local
0-2 -> 0 puntos para el local
1-0 -> 3 puntos para el local
1-1 -> 1 puntos para el local
1-2 -> 0 puntos para el local
2-0 -> 3 puntos para el local
2-1 -> 3 puntos para el local
2-2 -> 1 puntos para el local
Un empate vale 1 punto
Minuto 90
Minuto 60
Minuto 30
Minuto 0
El partido acaba en el minuto 90

Y ./gradlew :torneo:test debe terminar en BUILD SUCCESSFUL; después, abre el informe como acabas de aprender y comprueba que arriba pone 5 tests y 0 fallos.

El título de cada bloque de abajo es la ruta del fichero en tu proyecto.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/test/java/com/sebastiannarvaez/marcador/torneo/PuntosTest.kt
package com.sebastiannarvaez.marcador.torneo
 
import kotlin.test.Test
import kotlin.test.assertEquals
 
class PuntosTest {
    @Test
    fun victoriaValeTresPuntos() = assertEquals(3, puntosPor(2, 1))
 
    @Test
    fun empateValeUnPunto() = assertEquals(1, puntosPor(0, 0))
 
    @Test
    fun derrotaNoDaPuntos() = assertEquals(0, puntosPor(0, 3))
 
    @Test
    fun encabezadoUsaElPrefijoPorDefecto() =
        assertEquals("Jornada 2 de 5", encabezadoDeJornada(2, 5))
 
    @Test
    fun encabezadoAceptaPrefijoConNombre() =
        assertEquals("Fecha 1 de 3", encabezadoDeJornada(1, 3, prefijo = "Fecha"))
}
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
package com.sebastiannarvaez.marcador.torneo
 
// Punto de entrada de consola: ./gradlew :torneo:run
fun main() {
    val nombreDelTorneo = "Copa Barrio"
    val totalDeJornadas = 3
    println("Torneo: $nombreDelTorneo")
 
    // Rangos y bucles: cada jornada del 1 al total.
    for (jornada in 1..totalDeJornadas) {
        // Argumento con nombre: se lee sin adivinar qué es cada número.
        println(encabezadoDeJornada(numero = jornada, totalDeJornadas = totalDeJornadas))
    }
    println(encabezadoDeJornada(1, 3, prefijo = "Fecha"))
 
    // Todos los marcadores posibles de 0 a 2 goles y los puntos del local.
    for (golesLocal in 0..2) {
        for (golesVisitante in 0..2) {
            val puntos = puntosPor(golesLocal, golesVisitante)
            println("$golesLocal-$golesVisitante -> $puntos puntos para el local")
        }
    }
 
    // `if` como expresión, `downTo`, `step` y `while`.
    val hayEmpates = puntosPor(1, 1) == 1
    println(if (hayEmpates) "Un empate vale 1 punto" else "Sin empates")
    for (minuto in 90 downTo 0 step 30) println("Minuto $minuto")
    var minuto = 0
    while (minuto < 90) minuto += 45
    println("El partido acaba en el minuto $minuto")
}

Lo que ves en Main.kt: el for de 1..totalDeJornadas sustituye a las tres líneas de encabezado de la lección anterior, y dos for, uno dentro de otro, recorren los nueve marcadores. La función de test también es de expresión: = assertEquals(...).

Dos detalles. El for (minuto in 90 downTo 0 step 30) println(…) va sin llaves, en una sola línea, igual que el while del ejemplo de arriba: cuando el bucle repite una sola instrucción, las llaves son opcionales. Y la minuto de ese for solo existe dentro del bucle; la var minuto de dos líneas más abajo es otra variable distinta, que se crea después de que el bucle haya terminado.

Torneo: Copa Barrio
Jornada 1 de 3
Jornada 2 de 3
Jornada 3 de 3
Fecha 1 de 3
0-0 -> 1 puntos para el local
0-1 -> 0 puntos para el local
…
El partido acaba en el minuto 90

Cuando un test falla

Para ver un fallo de verdad, en PuntosTest.kt cambia el 3 esperado de victoriaValeTresPuntos por un 4 y vuelve a ejecutar ./gradlew :torneo:test. Gradle lo cuenta así:

PuntosTest > victoriaValeTresPuntos() FAILED
    org.opentest4j.AssertionFailedError at PuntosTest.kt:8
 
5 tests completed, 1 failed

AssertionFailedError («error de comprobación fallida») es el nombre del error que lanza assertEquals cuando los valores no coinciden (lanzar un error es provocarlo a propósito o por un fallo: el programa se para ahí hasta que alguien lo trate; lo verás a fondo en Excepciones: try, catch y require); org.opentest4j es solo el nombre de la librería de donde viene. El informe que ya sabes abrir (open torneo/build/reports/tests/test/index.html) muestra ahora ese test en rojo, con el valor esperado y el obtenido en un mensaje como expected: <4> but was: <3> («esperaba 4 pero era 3»). Devuelve el 4 a 3 antes de seguir.

Punto de control

  • En la vista Android, torneo → kotlin+java tiene ahora dos paquetes con el mismo nombre: el del código (Main.kt, Puntos.kt) y el (test) con PuntosTest.kt.
  • ./gradlew :torneo:test termina en BUILD SUCCESSFUL, y el informe (open torneo/build/reports/tests/test/index.html) pone arriba 5 tests. Con el 4 puesto, termina en FAILED y dice 5 tests completed, 1 failed.
  • ./gradlew :torneo:run imprime nueve líneas de marcadores, de 0-0 -> 1 puntos para el local a 2-2 -> 1 puntos para el local, y acaba con El partido acaba en el minuto 90.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué recorre for (i in 10 downTo 0 step 5)?

  2. 2.En PuntosTest, ¿qué significa assertEquals(3, puntosPor(2, 1))?

  3. 3.Lees este test: fun precioFinal(precio: Int, descuento: Int) = precio - descuento y assertEquals(50, precioFinal(80, 20)). Sin ejecutar nada, ¿qué pasa?

  4. 4.Una variable n empieza valiendo 3 y un while (n > 0) solo imprime n en cada vuelta, sin tocar su valor. ¿Qué ocurre al ejecutarlo?

  5. 5.Has ejecutado ./gradlew :torneo:test, termina en BUILD SUCCESSFUL y no ves ninguna lista de tests. ¿Cómo compruebas cuántos se han ejecutado?

Faltan 5 de 5