7. Tests

⏱ Enseñanza: 8 minutos · Ejercicios: 12 minutos

Preguntas

  • ¿Cómo sé que mi función sigue funcionando después de cambiarla?
  • ¿Cómo se prueba una función que da resultados al azar?

Objetivos

  • Preparar el paquete para usar testthat.
  • Escribir tests con test_that() y la familia expect_*().
  • Correr los tests con devtools::test().
  • Probar que una función da el error correcto.

De probar en la consola a tests automáticos

Hasta ahora probamos las funciones escribiendo en la consola y mirando el resultado. Funciona, pero cada vez que cambiamos algo hay que volver a probar todo y es muy posible que nos olvidemos de algo.

Un test es esa misma prueba de la consola, guardada en un archivo, que se corre automáticamente y nos avisa si el resultado es o no es el esperado.

Preparar testthat

💻 En vivo

usethis::use_testthat(3)
#> ✔ Adding testthat to Suggests field in DESCRIPTION.
#> ✔ Adding "3" to Config/testthat/edition.
#> ✔ Creating tests/testthat/.
#> ✔ Writing tests/testthat.R.

Se hace una sola vez por paquete. El 3 es la edición de testthat, la más moderna.

Un archivo de tests por archivo de código

La convención: las funciones de R/dicho_al_azar.R se testean en tests/testthat/test-dicho_al_azar.R. usethis lo crea por nosotros:

💻 En vivo

usethis::use_test("dicho_al_azar")

Un test

Un test tiene una descripción de lo que esperamos y una o más expectativas:

💻 En vivo, en tests/testthat/test-dicho_al_azar.R

test_that("dicho_al_azar() devuelve un solo texto", {
  resultado <- dicho_al_azar()
  expect_type(resultado, "character")
  expect_length(resultado, 1)
})

¿Cómo se testea algo que es al azar? No sabemos qué dicho va a salir, pero sí sabemos cómo tiene que ser: un texto, de largo 1, que esté en los datos.

test_that("dicho_al_azar() respeta el país", {
  resultado <- dicho_al_azar(pais = "Chile")
  expect_true(resultado %in% dichos$dicho[dichos$pais == "Chile"])
})

Algunas expectativas útiles

Expectativa Comprueba que…
expect_equal(x, y) x es igual a y
expect_type(x, "character") x es de ese tipo
expect_length(x, 1) x tiene ese largo
expect_true(x), expect_false(x) x es TRUE (verdadero) o FALSE (falso)
expect_gt(x, y) x es mayor que y
expect_s3_class(x, "data.frame") x es de esa clase
expect_error(codigo) el código da un error
expect_message(codigo) el código da un mensaje
expect_snapshot(codigo) la salida es igual a la última vez

Testear errores

Que una función dé un error cuando tiene que darlo también es parte de que funcione bien. Nos interesa que falle por la razón correcta y que el mensaje sirva:

test_that("dicho_al_azar() avisa si no conoce el país", {
  expect_error(dicho_al_azar(pais = "Narnia"), "Narnia")
})

El segundo argumento es un texto que tiene que aparecer en el mensaje de error.

TipSnapshots: una foto del mensaje (para leer en casa)

Para ver el mensaje completo, usá un snapshot test:

test_that("el error es claro", {
  expect_snapshot(dicho_al_azar(pais = "Narnia"), error = TRUE)
})

La primera vez, testthat guarda la salida en tests/testthat/_snaps/. Las veces siguientes, compara con esa “foto” y te avisa si cambió. Es ideal para revisar que los mensajes de error se lean bien.

Correr los tests

💻 En vivo

devtools::test()
#> ✔ | F W  S  OK | Context
#> ✔ |          4 | dicho_al_azar
#>
#> ══ Results ═══════════════════════════════
#> [ FAIL 0 | WARN 0 | SKIP 0 | PASS 4 ]

Atajo de teclado: Ctrl + Shift + T (en Mac, Cmd + Shift + T).

devtools::check() también corre todos los tests.

NotaLas dos ruedas siempre girando 🚲

Ahora el ciclo tiene una rueda más: editar, cargar, probar en la consola… y correr los tests. Cada vez que encontrás un error (un bug), escribí un test que lo detecte antes de arreglarlo. Así no vuelve.

Desafío 7.1: Tus primeros tests 8 minutos

  1. Corré usethis::use_testthat(3) y usethis::use_test("dicho_al_azar").
  2. Escribí un test que compruebe que dicho_al_azar() devuelve un solo texto.
  3. Escribí un test que compruebe que, para un país que no existe, la función da un error.
  4. Corré los tests con devtools::test().
test_that("dicho_al_azar() devuelve un solo texto", {
  resultado <- dicho_al_azar()
  expect_type(resultado, "character")
  expect_length(resultado, 1)
})

test_that("dicho_al_azar() avisa si no conoce el país", {
  expect_error(dicho_al_azar(pais = "Narnia"), "Narnia")
})
devtools::test()
#> [ FAIL 0 | WARN 0 | SKIP 0 | PASS 3 ]

Desafío 7.2: Rompé el paquete a propósito 3 minutos

En R/dicho_al_azar.R, cambiá size = 1 por size = 2. Antes de correr los tests, pensá: ¿qué test va a fallar?

Corré devtools::test(), leé el mensaje y después volvé a dejar size = 1.

Falla expect_length(resultado, 1), porque ahora la función devuelve dos dichos:

── Failure: dicho_al_azar() devuelve un solo texto ──
`resultado` has length 2, not length 1.

Romper el código a propósito es una forma de comprobar que los tests sirven.

Desafío 7.3: Una función nueva con su test para quien termine antes

Escribí la función buscar_dicho(palabra), que devuelve las filas de dichos donde la palabra aparece en el dicho o en el significado. Pista: grepl(palabra, dichos$dicho, ignore.case = TRUE).

Documentala, escribí un test con usethis::use_test("buscar_dicho") y corré devtools::check().

#' Buscar dichos por palabra
#'
#' Busca una palabra en los dichos y en sus significados.
#'
#' @param palabra Texto a buscar. No distingue mayúsculas de minúsculas.
#'
#' @return Un data frame con las filas de [dichos] que contienen la palabra.
#' @export
#'
#' @examples
#' buscar_dicho("dinero")
buscar_dicho <- function(palabra) {
  encontrados <- grepl(palabra, dichos$dicho, ignore.case = TRUE) |
    grepl(palabra, dichos$significado, ignore.case = TRUE)
  dichos[encontrados, ]
}
test_that("buscar_dicho() encuentra dichos", {
  resultado <- buscar_dicho("dinero")
  expect_s3_class(resultado, "data.frame")
  expect_gt(nrow(resultado), 0)
})

test_that("buscar_dicho() no distingue mayúsculas", {
  expect_equal(buscar_dicho("PERRO"), buscar_dicho("perro"))
})

La versión completa, con un mensaje cuando no encuentra nada, está en el paquete terminado.

Puntos clave

  • usethis::use_testthat(3) prepara el paquete; usethis::use_test() crea un archivo de tests.
  • Un test es una prueba que haciamos en la consola y ahora guardamos en un archivo: test_that() con una o más expect_*().
  • Para funciones al azar, testeá las propiedades del resultado, no el valor exacto.
  • Testear los errores también es testear.
  • devtools::test() corre todos los tests.