Geradores de testes


Um Gerador de Testes é um software específico de cada track que gera automaticamente os testes de um exercício de prática. Fá-lo convertendo os casos de teste JSON do exercício em testes na linguagem do track.

Vantagens

Algumas vantagens de ter um Gerador de Testes são:

  1. Os exercícios podem ser adicionados mais depressa
  2. Automatiza as partes "aborrecidas" de adicionar um exercício
  3. É fácil sincronizar os testes com os dados canónicos mais recentes

Casos de utilização

Em geral, executa-se um Gerador de Testes para uma de duas coisas:

  1. Gerar os testes de um exercício novo
  2. Atualizar os testes de um exercício existente

Gerar os testes de um novo exercício

Adicionar um Gerador de Testes para um novo exercício permite gerar o(s) ficheiro(s) de teste. Partindo do princípio de que o próprio Gerador de Testes já está implementado, gerar os testes para o novo exercício dará (muito) menos trabalho do que escrevê-los de raiz.

Atualizar os testes de um exercício existente

Assim que um exercício tem um Gerador de Testes, podes voltar a executá-lo para atualizar/sincronizar o exercício com os dados canónicos mais recentes. Recomendamos que o faças periodicamente, para verificar se há casos de teste problemáticos que precisam de ser atualizados ou novos testes que queiras incluir.

Ponto de partida

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

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

Se já existirem testes, implementa o Gerador de Testes de forma a que os testes que gera não quebrem as soluções existentes.

Conceção

De um modo geral, os ficheiros de teste são gerados de duas formas:

  • Código: os ficheiros de teste são gerados (na sua maioria) através de código
  • Templates: os ficheiros de teste são gerados (na sua maioria) com recurso a templates

Verificámos que a abordagem baseada em código torna o código do Gerador de Testes bastante complexo, ao passo que 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 com include = false no ficheiro tests.toml do exercício
  3. Converter os dados canónicos do exercício num formato que possa ser usado num template
  4. Passar os dados canónicos do exercício a um template específico do exercício

A principal vantagem desta configuração é que cada exercício tem o seu próprio template, o que:

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

Ao conceber o gerador de testes, tenta:

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

Implementação

O Gerador de Testes é normalmente escrito (na sua maioria) na linguagem do track.

Caution

Embora sejas livre de usar outras linguagens, cada linguagem adicional torna mais difícil dar manutenção ao track ou contribuir para ele. Por isso, recomendamos que uses a linguagem do track sempre que possível, porque assim é mais fácil dar manutenção ou contribuir.

Formatação

Se o teu track tiver ferramentas para formatar código, considera executá-las como um passo de pós-processamento depois de renderizares o template.

Dados canónicos

Os dados centrais com que o Gerador de Testes trabalha são o ficheiro canonical-data.json de um exercício. Este ficheiro é definido no repositório exercism/problem-specifications, que define metadados partilhados para muitos dos exercícios do Exercism.

Caution

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

Estrutura

Os dados canónicos são definidos num objeto JSON. Este objeto contém um campo "cases" que contém os casos de teste. Estes casos de teste correspondem (normalmente) um a um aos testes do teu track.

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. Eis um exemplo (parcial) do ficheiro 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 estes dados JSON em testes específicos do track. Eis como o JSON acima poderia ser traduzido para 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 ficheiro canonical-data.json está bem documentada e tem também uma definição de esquema JSON.

Aninhamento

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

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

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

Eis 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 o teu track não suportar o agrupamento de testes, tens de:

  • 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) ascendente(s) para criar um nome de teste único

Valores de entrada e valores esperados

O conteúdo das chaves input e expected dos casos de teste varia muito. Na maioria dos casos, são valores escalares (como números, Boolean ou strings) ou objetos simples. No entanto, ocasionalmente também encontras valores mais complexos que provavelmente exigem algum pré-processamento, como lambdas em pseudocódigo, listas de operações a realizar sobre o código dos alunos e muito mais.

Cenários

Os casos de teste têm um campo opcional scenarios. Este campo pode ser usado pelo gerador de testes para tratar certos casos de teste de forma especial. O caso de utilização mais comum é ignorar certos tipos de testes, por exemplo testes com o cenário "unicode", pois a linguagem do teu track pode não suportar Unicode.

A lista completa de cenários pode ser consultada aqui.

Ler ficheiros canonical-data.json

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

  1. Obtê-los diretamente do 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 submódulo Git ao repositório do track.
  3. Lê-los a partir da cache do configlet. A localização depende do sistema do utilizador, mas podes usar configlet info -o -v d | head -1 | cut -d " " -f 5 para obter a localização de forma programática.

Casos de teste específicos do track

Se o teu track quiser adicionar alguns casos de teste adicionais, específicos do track (que não se encontram nos dados canónicos), uma opção é criar um ficheiro additional-test-cases.json, que o Gerador de Testes pode depois combinar com o ficheiro canonical-data.json antes de o passar ao template para renderização.

Templates

O motor de templates a usar será provavelmente específico do track. Idealmente, queres que os teus templates sejam o mais simples possível, por isso não te preocupes com a duplicação de código e coisas do género.

Os próprios templates recebem os seus dados do Gerador de Testes, sobre os quais este itera para os renderizar.

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 mecanismo de extensão que os teus templates permitam.

Utilizar o configlet

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

  • Criar os ficheiros de um novo exercício: executa bin/configlet create --practice-exercise <slug>
  • Sincronizar o ficheiro tests.toml de um exercício existente: executa bin/configlet sync --tests --update --exercise <slug>
  • Obter os dados canónicos do exercício para o disco (isto é um efeito secundário de qualquer um dos comandos acima)

Isto faz do configlet uma excelente ferramenta para usar em conjunto com o Gerador de Testes, permitindo fluxos de trabalho bastante poderosos.

Interface de linha de comandos

Vais querer que a utilização do Gerador de Testes seja fácil e poderosa. Para isso, recomendamos que cries um ou mais ficheiros de script.

Note

És livre de escolher o formato de ficheiro de script que melhor se adapta ao teu track. Scripts de shell e scripts PowerShell são opções comuns, ambas funcionais.

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

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

Construir de raiz

Antes de começares a construir um Gerador de Testes, sugerimos que vejas alguns Geradores de Testes já existentes para perceberes como outras tracks os implementaram:

Se tiveres alguma dúvida, o fórum é o melhor sítio para a colocares. As discussões no fórum sobre o gerador de testes de Rust e sobre o de JavaScript também podem ser úteis.

Produto Mínimo Viável

Recomendamos construir o Gerador de Testes de forma incremental, começando por um Produto Mínimo Viável. Uma versão mínima consistiria apenas em ler o canonical-data.json de um exercício e passar esses dados ao template.

Começa por te focar num único exercício, de preferência num simples como o leap. Só quando isso estiver a funcionar é que deves acrescentar gradualmente mais exercícios.

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

Note

Idealmente, um contribuidor poderia limitar-se a colar/modificar um template existente sem ter de perceber como o Gerador de Testes funciona internamente.

Utilizar ou contribuir

A forma de usar ou contribuir para um Gerador de Testes é específica de cada track. Procura instruções no README.md do track, no CONTRIBUTING.md ou no diretório do código do Gerador de Testes.