eguatech/egua

Biblioteca de testes

Opened this issue · 10 comments

Olá pessoal! Estava dando uma olhada nos testes da linguagem e pensei em como poderia tornar a experiência de escrever os testes mais agradável para novos e antigos contribuidores. Tive a ideia de implementar uma biblioteca de testes que oferece funções que ajude a testar os elementos da linguagem. Sei que é um tópico complicado, uma vez que isso afeta todo o escopo e funcionamento linguagem, mas acho que é interessante avaliar a proposta.

Motivações:

  • Facilidade: Permite a construção de testes de maneira simplificada.
  • Legibilidade: Torna o código dos testes mais legível e elegante.
  • Clareza: A biblioteca apresenta de forma clara e concisa o resultados.

Dificuldades:

  • Manutenção: Assegurar que a biblioteca esteja funcionando corretamente.
  • Reformulação: Reescrever os testes no novo formato.
  • Validação: Garantir que a versão da linguagem que roda os testes não contenha bugs.
  • Cobertura: Garantir que a biblioteca cubra os testes necessários.

Atuais problemas da biblioteca:

  • Cada conjunto é um objeto global no escopo da biblioteca.
  • As comparações entre objetos não é precisa.
  • Ainda não há implementação para testes do tipo (Tente/Pegue, escreva, ...)
  • Ainda não é possível passar funções para testar, apenas os valores retornados.

Funcionamento básico da biblioteca:

// testes.egua
var testes = importar("testes"); //Importando a biblioteca;

var testes_basicos = função(){
  //Inicia novo conjuto de testes.
  testes.novo_conjunto("Testes básicos");

  testes.novo_teste("Verifica se 1 == 1").esperado_que(1 == 1).seja_verdadeiro();
  testes.novo_teste("Verifica se 1 != 2").esperado_que(1 == 2).seja_falso();

  // Finaliza o conjunto de testes e mostra o resultado!.
  escreva(testes.resultados());
};

var testes_objetos = função(){
    testes.novo_conjunto("Testes de objetos");
     // Exemplos testes com objetos
    testes.novo_teste("Verifica se [1, 2] == [1, 2]").esperado_que([1,2]).seja_objeto([1, 2]);
    testes.novo_teste("Verifica se {one: 1, two: 2} == {one: 1, two: 2}")
                          .esperado_que({"one": 1, "two": 2})
                          .seja_objeto({"one": 1, "two": 2});
    escreva(testes.resultados());
};

var testes_numeros = função(){
  //Inicia novo conjuto de testes.
  testes.novo_conjunto("Testes de números");
 
  // Exemplos testes com números
  testes.novo_teste("Verifica se 0.1 + 0.2 == 0.3").esperado_que(0.1 + 0.2).seja(0.3);
  testes.novo_teste("Vefica se 0.1 + 0.3 é próximo a 0.3").esperado_que(0.1 + 0.2).seja_proximo(0.3);

  // Finaliza o conjunto de testes e mostra o resultado!.
  escreva(testes.resultados());
};
// Chamando os testes.
testes_basicos();
testes_objetos();
testes_numeros();

A saída do programa:

Testes básicos -- PASSOU
  ✔  Verifica se 1 == 1
  ✔  Verifica se 1 != 2
Testes: 2 passou, 0 falhou, 2 total

Testes de objetos -- PASSOU
  ✔  Verifica se [1, 2] == [1, 2]
  ✔  Verifica se {one: 1, two: 2} == {one: 1, two: 2}
Testes: 2 passou, 0 falhou, 2 total

Testes de números -- FALHOU
  ✖  Verifica se 0.1 + 0.2 == 0.3
	 ● Testes de números > Verifica se 0.1 + 0.2 == 0.3  
	        esperado_que(obtido).seja(esperado) //Object.is igualdade
	        Esperado: 0.3
	        Obtido: 0.30000000000000004
  ✔  Vefica se 0.1 + 0.3 é próximo a 0.3
Testes: 1 passou, 1 falhou, 2 total

Olá @Andre0n.
A sua ideia seria criar uma biblioteca de testes em égua para reescrever os testes do arquivo /tests/tests.egua?

Sim! Hoje o arquivo tests.egua contém muitas linhas, muitas delas são chamadas da função escreva(), fora que saída não é exatamente clara.
A minha proposta é construir uma biblioteca para gerenciar esses testes, como no exemplo acima, permitindo uma melhora na escrita dos testes e clareza nos resultados. Já estou trabalhando nisso, o código acima é baseado na implementação que fiz, mas acho que é bom discutir e avaliar outras soluções para esse problema.

Então, essa parte dos testes tem como propósito testar a linguagem após alguma alteração, pra ter certeza que nada além do que foi alterado teve impacto. Em resumo ele substitui, de uma forma burra, os testes unitários. A ideia é realmente ele apenas rodar, de maneira crua, tudo que a linguagem faz, pra garantir que o interpretador não quebrou em algum lugar.
A construção de uma biblioteca de testes em égua não seria o suficiente para testar o interpretador da linguagem em si, pelo menos não consigo enxergar isso, no entanto seria interessante para o ensino de testes unitários. Adorei a ideia, mas o propósito precisa ser alinhado.
Pra que a linguagem consiga atender aos requisitos que você mostrou acima, como testes.novo_teste("Verifica se 1 != 2").esperado_que(1 == 2).seja_falso();, creio (posso estar errado) que seja necessário a inferência de tipos, coisa que a linguagem não suporta hoje em dia. Ademais, como está a sua implementação? Você está fazendo algo no sentido da inferência de tipos? Ou adotou outra estratégia?

Entendi, implementação inicial que fiz foi em javascript mesmo e está disponível aqui: https://github.com/Andre0n/egua/tree/bib_testes/src/lib/testes

A implementação é um pouco extensa, mas acho que dá para pegar a ideia com os códigos abaixo:

Basicamante ele composta de alguns módulos:

  • index.js
  • conjunto.js
  • correspondencias.js
  • saida_testes.js

No módulo index (o que fica disponível na linguagem):

const StandardFn = require("../../structures/standardFn");
const { novo_conjunto } = require("./conjunto");
const { correspondencias } = require("./correspondencias");
const { novo_erro } = require("./erro");

let conjunto_atual = null;

const esperado_que = (obtido) => {
    return correspondencias(obtido, conjunto_atual);
};

module.exports.novo_conjunto = function (descricao = "") {
    if (typeof descricao !== "string") {
        novo_erro(this.token, "A descrição do conjunto deve ser um texto");
    }
    if (descricao === "") {
        novo_erro(this.token, "A descrição do conjunto não pode ser vazia");
    }
    conjunto_atual = novo_conjunto(descricao, this.token);
};

module.exports.novo_teste = function (descricao = "") {
    if (typeof descricao !== "string") {
        novo_erro(this.token, "A descrição  do teste deve ser um texto");
    }
    if (descricao === "") {
        novo_erro(this.token, "A descrição  do teste não pode ser vazia");
    }
    conjunto_atual.teste_atual = descricao;
    conjunto_atual.token = this.token;
    return {
        esperado_que: new StandardFn(0, esperado_que),
    };
};

module.exports.resultados = function () {
    if (conjunto_atual == null) {
        novo_erro(this.token, "O conjunto de testes não foi descrito");
    }
    const resumo = conjunto_atual.resumo();
    conjunto_atual = null;
    return resumo;
};

No módulo do conjunto temos a função novo conjunto que retorna um objeto:

const novo_conjunto = (descricao) => {
    return {
        descricao: descricao,
        total_passou: 0,
        total_falhou: 0,
        teste_atual: "",
        resultados: [],
        token_atual: null,
        // Lista de funções auxiliares
        ...
};
module.exports.novo_conjunto = novo_conjunto;

No módulo de correspondencias temos a função novo conjunto que retorna um objeto:

const correspondencias = (obtido, conjuto_testes) => {
    conjunto_atual = conjuto_testes;
    return {
        seja: new StandardFn(1, (esperado) => {
            if (Object.is(obtido, esperado)) {
                conjuto_testes.teste_passou();
                return;
            }
            conjuto_testes.teste_falhou("seja", obtido, esperado);
        }),
        // Outras funções omitidas...
        // Código baseado na biblioteca Jest
        seja_proximo: new StandardFn(1, (esperado, precisao = 0.1) => {
            if (typeof esperado !== "number") {
                conjuto_testes.erro("`esperado` precisa ser do tipo número");
            }
            if (typeof obtido !== "number") {
                conjuto_testes.erro("`obtido` precisa ser do tipo número");
            }

            let passou;
            if (obtido === Infinity && esperado === Infinity) {
                passou = true;
            } else if (obtido === -Infinity && esperado === -Infinity) {
                passou = true;
            } else {
                let dif_esperado = Math.pow(10, -precisao) / 2;
                let dif_obtido = Math.abs(esperado - obtido);
                passou = dif_obtido < dif_esperado;
            }

            if (passou) {
                conjuto_testes.teste_passou();
                return;
            }
            conjuto_testes.teste_falhou("seja_proximo", obtido, esperado);
        }),
        // Outras funções omitidas...
    };
};

module.exports.correspondencias = correspondencias;

E o módulo saida_testes basicamente mostra o resultado dos testes.

const reporta_seja = (esperado, obtido) => {
    let menssagem =
        "\tesperado_que(obtido).seja(esperado) //Object.is igualdade";
    menssagem += `\n\tEsperado: ${esperado}\n\tObtido: ${obtido}`;
    return menssagem;
};
// Outras implementações do tipo reporta_algo
const novo_resultado = (
    passou,
    nome_teste,
    nome_conjunto,
    onde,
    obtido,
    esperado
) => {
    let resultado = `  ${passou ? "✔" : "✖"}  ${nome_teste}`;

    if (passou) {
        return resultado;
    }

    resultado += `\n\t ● ${nome_conjunto} > ${nome_teste}  \n`;

    switch (onde) {
        case "seja":
            resultado += reporta_seja(esperado, obtido);
            break;
        // Outros cases...
    }
    return resultado;
};
module.exports.novo_resultado = novo_resultado;

Hm... Interessante. Retornar a chamada de outra função tem sido suficiente pra permitir o uso de testes.novo_teste("Verifica se 1 != 2").esperado_que(1 == 2).seja_falso();?

Bom, não sei se foi isso que quis dizer:

var testes = importar("testes");

var retorna_1 = função(){
    retorna 1;
};

var testes_chamada = função(){

  testes.novo_conjunto("Testes função");

  testes.novo_teste("Verifica se a função retorna_1 retorna 1").esperado_que(retorna_1() == 1).seja_verdadeiro();
  escreva(testes.resultados());
};

testes_chamada();

A saída foi:

Testes função -- PASSOU
  ✔  Verifica se a função retorna_1 retorna 1
Testes: 1 passou, 0 falhou, 1 total

Era isso mesmo, está ficando sensacional.

Valeu! Ainda tem bastante coisa para fazer, mas é basicamente isso. Vou tentar seguir lógica de ser um instrumento de aprendizagem.

Pretendo, em breve, escrever testes unitários pro interpretador pra poder excluir o arquivo teste.egua.

É uma boa ideia! Eu estava cogitando a ideia de abrir uma issue em relação a isso, mas achei que os testes fossem ficar escritos na linguagem mesmo. Bom era isso, vou terminar de implementar e documentar o código e subir uma PR em breve. Valeu!