Generadores de tests


Un generador de tests es un programa específico de cada track que genera automáticamente los tests de un ejercicio de práctica. Lo hace convirtiendo los casos de test JSON del ejercicio en tests en el lenguaje del track.

Ventajas

Tener un generador de tests tiene varias ventajas:

  1. Los ejercicios se pueden añadir más rápido
  2. Automatiza las partes «aburridas» de añadir un ejercicio
  3. Es fácil sincronizar los tests con los datos canónicos más recientes

Casos de uso

Por lo general, un generador de tests se utiliza para una de estas dos cosas:

  1. Generar los tests de un ejercicio nuevo
  2. Actualizar los tests de un ejercicio existente

Generar los tests de un ejercicio nuevo

Añadir un generador de tests para un ejercicio nuevo permite generar su fichero o ficheros de tests. Siempre que el generador de tests ya esté implementado, generar los tests del nuevo ejercicio será (mucho) menos trabajo que escribirlos desde cero.

Actualizar los tests de un ejercicio existente

Cuando un ejercicio ya tiene un generador de tests, puedes volver a ejecutarlo para actualizar o sincronizar el ejercicio con sus datos canónicos más recientes. Te recomendamos hacerlo periódicamente, para comprobar si hay casos de test problemáticos que haya que actualizar o nuevos tests que quieras incluir.

Punto de partida

Hay dos puntos de partida posibles a la hora de implementar un generador de tests para un ejercicio:

  1. El ejercicio es nuevo y, por tanto, no tiene ningún test
  2. El ejercicio ya existe y, por tanto, ya tiene tests
Caution

Si ya hay tests, implementa el generador de tests de forma que los tests que genere no rompan las soluciones existentes.

Diseño

A grandes rasgos, los ficheros de tests se generan de una de estas dos formas:

  • Código: los ficheros de tests se generan (en su mayoría) mediante código
  • Plantillas: los ficheros de tests se generan (en su mayoría) mediante plantillas

Hemos comprobado que el enfoque basado en código da lugar a un código de generador de tests bastante complejo, mientras que el enfoque basado en plantillas es más sencillo.

Lo que recomendamos es el siguiente flujo:

  1. Leer los datos canónicos del ejercicio
  2. Excluir los casos de test marcados como include = false en el fichero tests.toml del ejercicio
  3. Convertir los datos canónicos del ejercicio a un formato que se pueda usar en una plantilla
  4. Pasar los datos canónicos del ejercicio a una plantilla específica del ejercicio

La principal ventaja de esta configuración es que cada ejercicio tiene su propia plantilla, lo que:

  • Deja claro cómo se generan los ficheros de tests
  • Hace que las plantillas sean más fáciles de depurar
  • Hace que editarlas sea seguro, sin riesgo de romper otro ejercicio
Caution

A la hora de diseñar el generador de tests, intenta:

  • Minimizar el preprocesado de los datos canónicos dentro del generador de tests
  • Reducir el acoplamiento entre plantillas

Implementación

Por lo general, el generador de tests está (en su mayor parte) escrito en el lenguaje del track.

Caution

Aunque eres libre de usar otros lenguajes, cada lenguaje adicional hará que sea más difícil mantener el track o contribuir a él. Por eso te recomendamos usar el lenguaje del track siempre que sea posible, porque hace que mantenerlo o contribuir a él sea más fácil.

Formato

Si tu track tiene herramientas para formatear código, plantéate ejecutarlas como paso de posprocesado después de renderizar tu plantilla.

Datos canónicos

El dato principal con el que trabaja el generador de tests es el fichero canonical-data.json de un ejercicio. Este fichero se define en el repositorio exercism/problem-specifications, que define metadatos compartidos para muchos de los ejercicios de Exercism.

Caution

¡No todos los ejercicios tienen un fichero canonical-data.json! Si no lo tienen, tendrás que crear los tests manualmente, ya que no hay datos con los que pueda trabajar el generador de tests.

Estructura

Los datos canónicos se definen en un objeto JSON. Este objeto contiene un campo "cases" que contiene los casos de test. Estos casos de test (normalmente) se corresponden uno a uno con los tests de tu track.

Cada caso de test tiene varias propiedades, de las cuales las más importantes son la descripción, la propiedad, el valor o valores de entrada y el valor esperado. Aquí tienes un ejemplo (parcial) del fichero canonical-data.json del ejercicio leap:

{
  "exercise": "leap",
  "cases": [
    {
      "uuid": "6466b30d-519c-438e-935d-388224ab5223",
      "description": "year not divisible by 4 in common year",
      "property": "leapYear",
      "input": {
        "year": 2015
      },
      "expected": false
    },
    {
      "uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
      "description": "year divisible by 4, not divisible by 100 in leap year",
      "property": "leapYear",
      "input": {
        "year": 1996
      },
      "expected": true
    }
  ]
}

La principal responsabilidad del generador de tests es transformar estos datos JSON en tests específicos del track. Así podría traducirse el JSON anterior a código de test de Nim:

import unittest
import leap

suite "Leap":
  test "year not divisible by 4 in common year":
    check isLeapYear(2015) == false

  test "year divisible by 4, not divisible by 100 in leap year":
    check isLeapYear(1996) == true

La estructura del fichero canonical-data.json está bien documentada y también tiene una definición de esquema JSON.

Anidamiento

Algunos ejercicios usan anidamiento en sus datos canónicos. Esto significa que cada elemento de un array cases puede ser una de estas dos cosas:

  1. Un caso de test normal (sin casos de test hijos)
  2. Una agrupación de casos de test (uno o más casos de test hijos)
Note

Puedes identificar el tipo de un elemento comprobando si están presentes los campos exclusivos de uno de los tipos. Probablemente la mejor forma de hacerlo sea usar la clave "cases", que solo está presente en las agrupaciones de casos de test.

Aquí tienes un ejemplo de casos de test anidados:

{
  "cases": [
    {
      "uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
      "description": "data is retained",
      "property": "data",
      "input": {
        "treeData": ["4"]
      },
      "expected": {
        "data": "4",
        "left": null,
        "right": null
      }
    },
    {
      "description": "insert data at proper node",
      "cases": [
        {
          "uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
          "description": "smaller number at left node",
          "property": "data",
          "input": {
            "treeData": ["4", "2"]
          },
          "expected": {
            "data": "4",
            "left": {
              "data": "2",
              "left": null,
              "right": null
            },
            "right": null
          }
        }
      ]
    }
  ]
}
Caution

Si tu track no admite agrupar tests, tendrás que:

  • Recorrer o aplanar la jerarquía de cases para quedarte solo con los casos de test más internos (las hojas)
  • Combinar la descripción del caso de test con la de su padre o padres para crear un nombre de test único

Valores de entrada y esperados

El contenido de las claves input y expected de un caso de test varía mucho. En la mayoría de los casos serán valores escalares (como números, booleanos o strings) u objetos simples. Sin embargo, de vez en cuando también te encontrarás con valores más complejos que probablemente requieran algo de preprocesado, como lambdas en pseudocódigo, listas de operaciones que hay que aplicar al código del estudiante y más.

Escenarios

Los casos de test tienen un campo scenarios opcional. El generador de tests puede usar este campo para tratar de forma especial ciertos casos de test. El uso más habitual es ignorar ciertos tipos de tests, por ejemplo los tests con el escenario "unicode", ya que puede que el lenguaje de tu track no admita Unicode.

Puedes encontrar la lista completa de escenarios aquí.

Leer ficheros canonical-data.json

Hay varias opciones para leer los ficheros canonical-data.json:

  1. Obtenerlos directamente del repositorio problem-specifications (por ejemplo, https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).
  2. Añadir el repositorio problem-specifications como submódulo de Git al repositorio del track.
  3. Leerlos desde la caché de configlet. La ubicación depende del sistema del usuario, pero puedes usar configlet info -o -v d | head -1 | cut -d " " -f 5 para obtenerla mediante programación.

Casos de test específicos del track

Si tu track quiere añadir algunos casos de test adicionales y específicos del track (que no están en los datos canónicos), una opción es crear un fichero additional-test-cases.json, que el generador de tests puede combinar con el fichero canonical-data.json antes de pasarlo a la plantilla para renderizarlo.

Plantillas

El motor de plantillas que se use probablemente sea específico del track. Lo ideal es que tus plantillas sean lo más sencillas posible, así que no te preocupes por la duplicación de código y ese tipo de cosas.

Las propias plantillas obtienen sus datos del generador de tests, sobre los que itera para renderizarlas.

Note

Para ayudar a que las plantillas sigan siendo sencillas, puede ser útil hacer algo de preprocesado en el lado del generador de tests o definir algunos «filtros» o el mecanismo de extensión que permitan tus plantillas.

Usar configlet

configlet es la principal herramienta de mantenimiento de tracks y se puede usar para:

  • Crear los ficheros de un ejercicio nuevo: ejecuta bin/configlet create --practice-exercise <slug>
  • Sincronizar el fichero tests.toml de un ejercicio existente: ejecuta bin/configlet sync --tests --update --exercise <slug>
  • Descargar los datos canónicos del ejercicio al disco (esto es un efecto secundario de cualquiera de los comandos anteriores)

Esto hace de configlet una gran herramienta para combinar con el generador de tests y lograr flujos de trabajo realmente potentes.

Interfaz de línea de comandos

Querrás que usar el generador de tests sea fácil y potente a la vez. Para ello, te recomendamos crear uno o varios ficheros de script.

Note

Eres libre de elegir el formato de fichero de script que mejor le venga a tu track. Los scripts de shell y los scripts de PowerShell son opciones habituales que funcionan bien.

Aquí tienes un ejemplo de script de shell que combina configlet y un generador de tests para crear rápidamente la estructura de un ejercicio nuevo:

bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>

Crear desde cero

Antes de empezar a crear un generador de tests, te sugerimos que eches un vistazo a un par de generadores de tests existentes para hacerte una idea de cómo los han implementado otros tracks:

Si tienes alguna pregunta, el foro es el mejor sitio para plantearla. También pueden resultarte útiles los debates del foro sobre el generador de tests de Rust y sobre los generadores de tests de JavaScript.

Producto mínimo viable

Te recomendamos crear el generador de tests de forma incremental, empezando por un producto mínimo viable. Una versión mínima básica leería el canonical-data.json de un ejercicio y se limitaría a pasar esos datos a la plantilla.

Empieza centrándote en un solo ejercicio, preferiblemente uno sencillo como leap. Solo cuando eso funcione deberías ir añadiendo más ejercicios poco a poco.

Y procura que el generador de tests sea lo más simple posible.

Note

Lo ideal sería que un contribuidor pudiera simplemente pegar o modificar una plantilla existente sin tener que entender cómo funciona el generador de tests por dentro.

Usar o contribuir

Cómo usar un generador de tests o contribuir a él es algo específico de cada track. Busca las instrucciones en el README.md del track, en su CONTRIBUTING.md o en el directorio del código del generador de tests.