Generadores de tests


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

Beneficios

Algunos beneficios de tener un Generador de tests son:

  1. Se pueden agregar ejercicios más rápido
  2. Automatiza las partes «aburridas» de agregar un ejercicio
  3. Facilita sincronizar los tests con los datos canónicos más recientes

Casos de uso

En general, se ejecuta un Generador de tests para una de 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

Agregar un Generador de tests para un ejercicio nuevo permite generar su archivo o archivos de tests. Siempre que el Generador de tests ya esté implementado, generar los tests del ejercicio nuevo será (mucho) menos trabajo que escribirlos desde cero.

Actualizar los tests de un ejercicio existente

Una vez que un ejercicio tiene un Generador de tests, puedes volver a ejecutarlo para actualizar o sincronizar el ejercicio con sus datos canónicos más recientes. Recomendamos hacerlo de forma periódica, para revisar si hay casos de test problemáticos que deban actualizarse o tests nuevos que quieras incluir.

Punto de partida

Hay dos posibles puntos de partida al implementar un Generador de tests para un ejercicio:

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

Si ya existen tests, implementa el Generador de tests de manera que los tests que genere no rompan las soluciones existentes.

Diseño

En términos generales, los archivos de tests se generan de una de dos formas:

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

Hemos visto que el enfoque basado en código da como resultado 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 que están marcados como include = false en el archivo 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

El beneficio clave de esta configuración es que cada ejercicio tiene su propia plantilla, lo cual:

  • Hace evidente cómo se generan los archivos de tests
  • Hace que sea más fácil depurarlos
  • Hace que sea seguro editarlos sin arriesgarte a romper otro ejercicio
Caution

Al diseñar el Generador de tests, procura:

  • Minimizar el preprocesamiento 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á escrito (en su mayoría) en el lenguaje del track.

Caution

Si bien 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 recomendamos usar el lenguaje del track siempre que sea posible, porque hace que mantenerlo o contribuir sea más fácil.

Formato

Si tu track tiene herramientas para dar formato al código, considera ejecutarlas como un paso de posprocesamiento después de renderizar tu plantilla.

Datos canónicos

Los datos principales con los que trabaja el Generador de tests son el canonical-data.json file de un ejercicio. Este archivo se define en el repo exercism/problem-specifications, que define metadatos compartidos para muchos de los ejercicios de Exercism.

Caution

¡No todos los ejercicios tienen un archivo canonical-data.json! Si no lo tienen, deberás 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) corresponden uno a uno con los tests de tu track.

Cada caso de test tiene varias propiedades, y las más importantes son la descripción, la propiedad, el valor o los valores de entrada y el valor esperado. Aquí hay un ejemplo (parcial) del archivo 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 responsabilidad principal 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 tests en 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 archivo 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 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 revisando si están presentes los campos que son exclusivos de un tipo de elemento. Probablemente la mejor forma de hacerlo sea usar la clave "cases", que solo está presente en los grupos de casos de test.

Aquí hay 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 los tests, deberás:

  • 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 descripción o descripciones de su padre 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 son valores escalares (como números, Boolean o strings) u objetos simples. Sin embargo, de vez en cuando también encontrarás valores más complejos que probablemente requieran algo de preprocesamiento, como lambdas en pseudocódigo, listas de operaciones que se deben realizar sobre el código del estudiante y más.

Escenarios

Los casos de test tienen un campo opcional scenarios. El generador de tests puede usar este campo para tratar de forma especial ciertos casos de test. El caso de uso más común 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.

La lista completa de escenarios está aquí.

Leer los archivos canonical-data.json

Hay un par de opciones para leer los archivos 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. Agregar el repo problem-specifications como submódulo de Git al repo 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 obtener la ubicación de forma programática.

Casos de test específicos del track

Si tu track quiere agregar algunos casos de test adicionales y específicos del track (que no se encuentran en los datos canónicos), una opción es crear un archivo additional-test-cases.json, que el Generador de tests puede combinar con el archivo canonical-data.json antes de pasarlo a la plantilla para renderizarla.

Plantillas

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

Las plantillas reciben sus datos del Generador de tests, que itera sobre ellos para renderizarlas.

Note

Para ayudar a que las plantillas se mantengan simples, puede ser útil hacer un poco de preprocesamiento del lado del Generador de tests o, si no, definir algunos «filtros» o el mecanismo de extensión que permitan tus plantillas.

Usar configlet

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

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

Esto hace de configlet una gran herramienta para usar junto con el Generador de tests y lograr flujos de trabajo muy potentes.

Interfaz de línea de comandos

Querrás que usar el Generador de tests sea fácil y potente a la vez. Para eso, recomendamos crear uno o más archivos de script.

Note

Eres libre de elegir el formato de archivo de script que mejor se adapte a tu track. Los scripts de shell y los scripts de PowerShell son opciones comunes que funcionan bien.

Aquí hay un ejemplo de un 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>

Cómo construir desde cero

Antes de empezar a construir un Generador de tests, te sugerimos echar 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 lugar para hacerla. Las discusiones del foro sobre el generador de tests de Rust y el de JavaScript también pueden ser útiles.

Producto mínimo viable

Recomendamos construir 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 simplemente pasaría esos datos a la plantilla.

Empieza enfocándote en un solo ejercicio, preferiblemente uno simple como leap. Solo cuando eso funcione deberías agregar más ejercicios poco a poco.

Y trata de mantener el Generador de tests tan simple como sea posible.

Note

Lo ideal es que quien contribuya pueda simplemente pegar o modificar una plantilla existente sin tener que entender cómo funciona el Generador de tests por dentro.

Cómo usarlo 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 o el CONTRIBUTING.md del track, o en el directorio del código del Generador de tests.