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!