rOpenSci | Un ejemplo de los principios DRY/DAMP para las pruebas de paquetes

Un ejemplo de los principios DRY/DAMP para las pruebas de paquetes

rOpenSci ha incorporado la segunda cohorte de Campeones ¡! Su formación comenzó con una sesión sobre el estilo del código a la que siguieron tres sesiones sobre los fundamentos del desarrollo de paquetes R, y finalizó con una sesión sobre desarrollo avanzado de paquetes R que consistió en un popurrí de consejos más un debate, seguido de tiempo para aplicar estos principios a los paquetes de los participantes. Aquí quiero compartir uno de los temas tratados: La comprobación de paquetes y, en particular, los principios DRY Don´t Repeat Yourself (“no te repitas”) y DAMP Descriptive And Meaningful Phrases (“frases descriptivas y significativas”). Para este tema, utilizamos un repositorio de GitHub que contiene un paquete R cuyos diferentes commits ilustran los dos principios. En cada paso compartiremos un commit o diff que ilustre los cambios realizados.

🔗 Etapa 1: ¡barro!

Primer commit: Configurar nuestros archivos de prueba, trabajos-de-prueba.R y prueba-ok.R

Estos dos archivos de prueba iniciales definen un objeto llamado test_object en el nivel superior, que se utiliza en dos pruebas de cada archivo.

prueba-trabajos.R

test_object <- list(a = 1, b = 2)

test_that("multiplication works", {
  expect_equal(test_object[["b"]] * 2, 4)
})

test_that("addition works", {
  expect_equal(test_object[["a"]] + 2, 3)
})

y

prueba-ok.R

test_object <- list(a = 1, b = 2)

test_that("division works", {
  expect_equal(test_object[["b"]] / 2, 1)
})

test_that("substraction works", {
  expect_equal(test_object[["a"]] - 1, 0)
})

No es un patrón óptimo porque no puedes mirar cada test_that() ya que prueba de forma aislada y no comprende rápidamente lo que ocurre. En un archivo de pruebas muy largo incluso tendrías que desplazarte hacia arriba y abajo! 😱 Además, estamos siendo repetitivos al definir el mismo objeto de prueba en dos archivos de prueba.

🔗 Fase 2: ¡SECADO!

Siguiente compromiso: SECAR estos archivos

En esta fase nos acordamos rápidamente de DRY, Don’t Repeat Yourself (No te repitas), y de la mecánica de archivos de ayuda de testthat: archivos cuyos nombres empiezan por helper- se cargan antes de todas las pruebas.

Así que creamos un archivo de ayuda (helper-swamp.R) dentro del cual test_object esté definido, por lo tanto, ¡disponible para las pruebas!

En tests/testthat/helper-swamp.R,

test_object <- list(a = 1, b = 2)

En los archivos de prueba, eliminamos la primera línea que definía test_object.

Pero las cosas aún no son perfectas. Cuando miramos cualquiera de los archivos de prueba, no podemos saber realmente qué test_object ya que su nombre no es “descriptivo y significativo”.

Además, ahora tenemos test_object que es siempre definida aunque no se utilice en una prueba. En el mejor de los casos, esto es innecesario e inútil; en el peor, podría tener efectos secundarios no deseados, ¡sobre todo con código más complejo! 1

🔗 Fase 3: Centrarse en el DAMP

Tercer compromiso: Aplicar los principios DAMP

En el archivo de ayuda, tests/testthat/helper-swamp.R refactorizamos el código en una función con un nombre más significativo (¡al menos hagamos como si lo fuera!).

basic_list <- function() {
  list(a = 1, b = 2)
}

A continuación, usamos esta función para definir el objeto en todas las pruebas en las que sea necesario. Así, los archivos de prueba pasan a ser

test_that("division works", {
  test_object <- basic_list()
  expect_equal(test_object[["b"]] / 2, 1)
})

y

test_that("substraction works", {
  test_object <- basic_list()
  expect_equal(test_object[["a"]] - 1, 0)
})

Ahora, aunque la definición real de la lista básica no está en todas las pruebas, tenemos una mejor idea de lo que ocurre al leer la prueba.

Además, si la prueba fallara, en la consola podríamos ejecutar devtools::load_all() y ejecutar el código de la prueba, como devtools::load_all() carga los archivos de ayuda de la prueba así basic_list() están disponibles.

🔗 Conclusión

El equilibrio entre DRY (“No te repitas”) y DAMP (“Frases descriptivas y significativas”) es un compromiso. Para mantener la analogía del agua, también tenemos que asegurarnos de que nuestro código no tenga efectos que puedan “filtrarse” inesperadamente. Lo que debemos procurar son pruebas autocontenidas que podamos entender y ejecutar sin demasiado contexto.

Otra consideración que no hemos tratado aquí son las pruebas que requieren elementos específicos, como variables de entorno u opciones. En esos casos, intenta utilizar conr como withr::local_envvar() en cada prueba que lo requiera.

Una idea poderosa del libro “Ingeniería de Software en Google” de Titus Winters, Tom Manshreck y Hyrum Wright, es que el código puede permitirse ser un poco menos obvio porque tiene pruebas que lo cubren, pero el código de prueba, que no está cubierto por pruebas, no tiene este lujo.

🔗 Más recursos


  1. Esto es una fuga. En otra prueba, uno podría preguntarse por qué existe algún objeto, por qué se ha establecido una opción concreta, etc. y es una pesadilla depurarlo. ↩︎