Geradores de testes


Um Gerador de Testes é um software específico de cada trilha que gera automaticamente os testes de um exercício de prática. Ele faz isso convertendo os casos de teste em JSON do exercício para testes na linguagem da trilha.

Vantagens

Algumas vantagens de ter um Gerador de Testes:

  1. Exercícios podem ser adicionados mais rápido
  2. Automatiza as partes "chatas" de adicionar um exercício
  3. Facilita sincronizar os testes com os dados canônicos mais recentes

Casos de uso

Em geral, roda-se um Gerador de Testes para:

  1. Gerar os testes de um exercício novo
  2. Atualizar os testes de um exercício que já existe

Gerar testes para um exercício novo

Adicionar um Gerador de Testes para um exercício novo permite gerar o(s) arquivo(s) de teste dele. Desde que o próprio Gerador de Testes já esteja implementado, gerar os testes do exercício novo dá (muito) menos trabalho do que escrevê-los do zero.

Atualizar os testes de um exercício existente

Quando um exercício passa a ter um Gerador de Testes, você pode rodá-lo de novo para atualizar/sincronizar o exercício com os dados canônicos mais recentes. Recomendamos fazer isso periodicamente, para verificar se há casos de teste problemáticos que precisam ser atualizados ou testes novos que você queira incluir.

Ponto de partida

Há dois pontos de partida possíveis ao implementar um Gerador de Testes para um exercício:

  1. O exercício é novo e, portanto, ainda não tem testes
  2. O exercício já existe e, portanto, já tem testes
Caution

Se já existem testes, implemente o Gerador de Testes de forma que os testes que ele gera não quebrem as soluções existentes.

Design

De modo geral, os arquivos de teste são gerados de duas maneiras:

  • Código: os arquivos de teste são (em sua maioria) gerados por código
  • Templates: os arquivos de teste são (em sua maioria) gerados com templates

Descobrimos que a abordagem baseada em código leva a um código de Gerador de Testes bem complexo, enquanto a abordagem baseada em templates é mais simples.

O que recomendamos é o seguinte fluxo:

  1. Ler os dados canônicos do exercício
  2. Excluir os casos de teste marcados como include = false no arquivo tests.toml do exercício
  3. Converter os dados canônicos do exercício em um formato que possa ser usado em um template
  4. Passar os dados canônicos do exercício para um template específico do exercício

O principal benefício dessa configuração é que cada exercício tem o seu próprio template, o que:

  • Deixa óbvio como os arquivos de teste são gerados
  • Torna mais fácil depurá-los
  • Torna seguro editá-los sem correr o risco de quebrar outro exercício
Caution

Ao projetar o gerador de testes, tente:

  • Minimizar o pré-processamento dos dados canônicos dentro do Gerador de Testes
  • Reduzir o acoplamento entre os templates

Implementação

O Gerador de Testes normalmente é (em sua maior parte) escrito na linguagem da trilha.

Caution

Embora você seja livre para usar outras linguagens, cada linguagem a mais torna a trilha mais difícil de manter ou de contribuir. Por isso, recomendamos usar a linguagem da trilha sempre que possível, porque isso facilita a manutenção e a contribuição.

Formatação

Se a sua trilha tem ferramentas para formatar código, considere rodá-las como uma etapa de pós-processamento depois de renderizar o seu template.

Dados canônicos

O dado central com que o Gerador de Testes trabalha é o arquivo canonical-data.json de um exercício. Esse arquivo é definido no repositório exercism/problem-specifications, que define metadados compartilhados por muitos dos exercícios do Exercism.

Caution

Nem todos os exercícios têm um arquivo canonical-data.json! Se não tiverem, você vai precisar criar os testes manualmente, já que não há dados com que o Gerador de Testes possa trabalhar.

Estrutura

Os dados canônicos são definidos em um objeto JSON. Esse objeto contém um campo "cases" que contém os casos de teste. Esses casos de teste (normalmente) correspondem um a um aos testes da sua trilha.

Cada caso de teste tem algumas propriedades, sendo a descrição, a propriedade, o(s) valor(es) de entrada e o valor esperado as mais importantes. Veja um exemplo (parcial) do arquivo canonical-data.json do exercício 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
    }
  ]
}

A principal responsabilidade do Gerador de Testes é transformar esses dados JSON em testes específicos da trilha. Veja como o JSON acima poderia se transformar em código de teste em 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

A estrutura do arquivo canonical-data.json está bem documentada e também tem uma definição de JSON schema.

Aninhamento

Alguns exercícios usam aninhamento nos seus dados canônicos. Isso significa que cada elemento de um array cases pode ser:

  1. Um caso de teste comum (sem casos de teste filhos)
  2. Um agrupamento de casos de teste (um ou mais casos de teste filhos)
Note

Você pode identificar o tipo de um elemento verificando a presença de campos exclusivos de um dos tipos. Provavelmente a melhor forma de fazer isso é usar a chave "cases", que só existe em grupos de casos de teste.

Veja um exemplo de casos de teste aninhados:

{
  "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

Se a sua trilha não oferece suporte a testes agrupados, você vai precisar:

  • Percorrer/achatar a hierarquia de cases para ficar apenas com os casos de teste mais internos (as folhas)
  • Combinar a descrição do caso de teste com a(s) descrição(ões) do(s) seu(s) pai(s) para criar um nome de teste único

Valores de entrada e esperados

O conteúdo das chaves input e expected de um caso de teste varia bastante. Na maioria dos casos, são valores escalares (como números, Boolean ou strings) ou objetos simples. No entanto, às vezes você também encontra valores mais complexos que provavelmente exigem um pouco de pré-processamento, como lambdas em pseudocódigo, listas de operações a executar no código dos estudantes e mais.

Cenários

Os casos de teste têm um campo opcional scenarios. Esse campo pode ser usado pelo gerador de testes para tratar certos casos de teste de forma especial. O uso mais comum é ignorar certos tipos de teste, por exemplo testes com o cenário "unicode", já que a linguagem da sua trilha pode não oferecer suporte a Unicode.

A lista completa de cenários está aqui.

Lendo arquivos canonical-data.json

Há algumas opções para ler os arquivos canonical-data.json:

  1. Buscá-los diretamente no repositório problem-specifications (por exemplo, https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).
  2. Adicionar o repositório problem-specifications como um submódulo Git no repositório da trilha.
  3. Lê-los do cache do configlet. O local depende do sistema do usuário, mas você pode usar configlet info -o -v d | head -1 | cut -d " " -f 5 para obter o local programaticamente.

Casos de teste específicos da trilha

Se a sua trilha quiser adicionar alguns casos de teste extras, específicos da trilha (que não estão nos dados canônicos), uma opção é criar um arquivo additional-test-cases.json, que o Gerador de Testes pode mesclar com o arquivo canonical-data.json antes de passá-lo ao template para renderização.

Templates

O motor de templates a usar provavelmente será específico da trilha. O ideal é que os seus templates sejam o mais diretos possível, então não se preocupe com duplicação de código e coisas do tipo.

Os próprios templates recebem seus dados do Gerador de Testes, que itera sobre eles para renderizá-los.

Note

Para ajudar a manter os templates simples, pode ser útil fazer um pouco de pré-processamento do lado do Gerador de Testes ou então definir alguns "filtros" ou qualquer outro mecanismo de extensão que os seus templates permitam.

Usando o configlet

O configlet é a principal ferramenta de manutenção de trilhas e pode ser usado para:

  • Criar os arquivos de um exercício novo: rode bin/configlet create --practice-exercise <slug>
  • Sincronizar o arquivo tests.toml de um exercício existente: rode bin/configlet sync --tests --update --exercise <slug>
  • Buscar os dados canônicos do exercício para o disco (isso é um efeito colateral de qualquer um dos comandos acima)

Isso faz do configlet uma ótima ferramenta para usar junto com o Gerador de Testes em fluxos de trabalho realmente poderosos.

Interface de linha de comando

Você vai querer que usar o Gerador de Testes seja fácil e poderoso ao mesmo tempo. Para isso, recomendamos criar um ou mais arquivos de script.

Note

Você é livre para escolher o formato de arquivo de script que melhor se encaixa na sua trilha. Scripts shell e scripts PowerShell são opções comuns, e ambas funcionam bem.

Veja um exemplo de script shell que combina o configlet e um Gerador de Testes para criar rapidamente a estrutura de um exercício novo:

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

Construindo do zero

Antes de começar a construir um Gerador de Testes, sugerimos que você olhe alguns Geradores de Testes que já existem para ter uma ideia de como outras trilhas os implementaram:

Se você tiver dúvidas, o fórum é o melhor lugar para perguntá-las. As discussões no fórum sobre o gerador de testes do Rust e o do JavaScript também podem ajudar.

Produto Mínimo Viável

Recomendamos construir o Gerador de Testes de forma incremental, começando com um Produto Mínimo Viável. A versão mínima possível leria o canonical-data.json de um exercício e simplesmente passaria esses dados ao template.

Comece focando em um único exercício, de preferência um simples como o leap. Só depois que esse estiver funcionando você deve adicionar mais exercícios aos poucos.

E tente manter o Gerador de Testes o mais simples possível.

Note

O ideal é que quem contribui possa simplesmente colar/modificar um template existente sem precisar entender como o Gerador de Testes funciona por dentro.

Como usar ou contribuir

Como usar ou contribuir com um Gerador de Testes é específico de cada trilha. Procure instruções no README.md ou CONTRIBUTING.md da trilha, ou no diretório do código do Gerador de Testes.