Testando duas implementações diferentes usando o mesmo teste no TestNG
Estou criando uma implementação parcial da linguagem AsciiDoc.
A linguagem AsciiDoc possui uma documentação. Esta faz o papel de especificação até o momento. A documentação dá a entender que a especificação de fato é a implementação Asciidoctor.
Na prática, então, eu preciso fazer uma engenharia reversa. Isto é, dada uma entrada específica, preciso comparar a saída produzida pelas duas implementações:
-
Asciidoctor, a minha referência; e
-
Objectos AsciiDoc, a que estou criando.
Neste artigo eu vou mostrar como estou fazendo isto com o TestNG.
Bora lá.
O nosso teste de exemplo
Para o nosso exemplo, ao invés de um parser AsciiDoc, vamos fazer algo bem menor.
O seguinte teste estabelece o que o nosso parser deve fazer:
public class Exemplo01 {
@Test(description = """
Testar o caminho feliz
- entrada válida
- entrada formatada c/ espaço depois da vírgula
""")
public void teste() {
var entrada = "4, 8, 15, 16, 23, 42";
var saida = new int[] {4, 8, 15, 16, 23, 42};
int[] res = Referencia.parse(entrada);
assertEquals(res, saida);
}
}
Assim, nossa entrada é uma string formada por números inteiros separados por vírgulas:
var entrada = "4, 8, 15, 16, 23, 42";
E nosso parser deve retornar um array de inteiros que corresponde à entrada:
var saida = new int[] {4, 8, 15, 16, 23, 42};
Isto é, o array de saída deve:
-
conter o mesmo número de inteiros; e
-
os inteiros devem estar na mesma ordem da definida na entrada.
Testando nossa implementação
No exemplo anterior, usamos o parser que serve de referência:
int[] res = Referencia.parse(entrada);
A ideia, no entanto, é conseguir utilizar o mesmo teste para:
-
a implementação existente (a de referência); e
-
a implementação que estamos desenvolvendo (a Objectos).
Para isso, vamos adicionar uma fachada no nosso teste.
A nossa fachada
O código da nossa fachada é o seguinte:
public abstract class Tester {
static final Tester OBJECTOS = new Tester() {
@Override
public final void test(String entrada, int[] saida) {
var parser = new Objectos();
int[] res = parser.parse(entrada);
assertEquals(res, saida);
}
};
static final Tester REFERENCIA = new Tester() {
@Override
public final void test(String entrada, int[] saida) {
int[] res = Referencia.parse(entrada);
assertEquals(res, saida);
}
};
Tester() {}
public abstract void test(String entrada, int[] saida);
}
A classe Tester
possui um único método público test
.
O método recebe tanto:
-
a string de entrada; como
-
o resultado esperado.
A fachada provê duas implementações distintas OBJECTOS
e REFERENCIA
:
-
a primeira utiliza o parser que estamos desenvolvendo; e
-
a segunda utiliza o parser de referência
Usando a fachada no teste
Alteramos o nosso teste e introduzimos a fachada Tester
:
public class Exemplo02 {
Tester tester = Tester.OBJECTOS;
@Test(description = """
Testar o caminho feliz
- entrada válida
- entrada formatada c/ espaço depois da vírgula
""")
public void teste() {
var entrada = "4, 8, 15, 16, 23, 42";
var saida = new int[] {4, 8, 15, 16, 23, 42};
tester.test(entrada, saida);
}
}
Veja que o teste em si foi delegado à nossa fachada:
tester.test(entrada, saida);
E que, por padrão, o teste roda com a implementação que estamos desenvolvendo.
Veja o atributo tester
:
Tester tester = Tester.OBJECTOS;
Assim, é a nossa implementação que é testada quando executamos o teste no IDE, por exemplo.
Em outras palavras, o ciclo 'natural' de desenvolvimento é mantido.
Rodando o mesmo teste com a referência
Nosso teste passou. Precisamos, no entanto, garantir que, ao longo do desenvolvimento, ele continue gerando resultado igual ao da nossa referência.
Em outras palavras, é também necessário rodar o teste que passou com a implementação de referência.
Uma solução é usar a anotação Factory
do TestNG.
Adicionando construtores ao teste
Vamos refatorar nosso teste e adicionar dois construtores. O 'cabeçalho' do teste fica assim:
public class Exemplo03 {
Tester tester = Tester.OBJECTOS;
public Exemplo03() {}
Exemplo03(Tester tester) {
this.tester = tester;
}
...
}
O construtor público e sem argumentos permite que o nosso IDE rode o teste sem percalços.
O segundo construtor permite construir uma nova instância do teste especificando diretamente a fachada Tester
.
Este passo de adicionar construtores não é estritamente necessário.
Afinal, o atributo tester
não é private
e tampouco final
.
No entanto, os construtores facilitarão nossa vida no passo seguinte.
Usando a anotação Factory
Criamos, então, uma classe com a anotação Factory
do TestNG:
public class Exemplo03Referencia {
@Factory
public Object[] factory() {
var tester = Tester.REFERENCIA;
return new Object[] {
new Exemplo03(tester)
};
}
}
A anotação indica ao TestNG que todos os objetos retornados pelo método devem ser executados como testes.
Note que o método retorna um array contendo o nosso teste.
O nosso teste, por sua vez, foi criado com a fachada Tester
de referência.
Conseguimos, assim:
-
rodar nosso teste no IDE com nossa implementação;
-
rodar o mesmo teste com a implementação de referência.
Integração contínua
Como dito no final da última seção, conseguimos rodar o teste com a nossa implementação no IDE.
Em outras palavras, no atual momento, o teste não rodará como parte do build Maven, por exemplo. No processo de integração contínua, somente o teste com a implementação de referência será executado.
Para que o teste rode com nossa implementação rode, é necessário criar uma segunda Factory
:
public class Exemplo03Objectos {
@Factory
public Object[] factory() {
var tester = Tester.OBJECTOS;
return new Object[] {
new Exemplo03(tester)
};
}
}
Isto cria um problema de ter manter a mesma lista de testes em duas classes separadas.
Factory
e DataProvider
Como alternativa é possível utilizar Factory
juntamente com a anotação DataProvider
:
public class Exemplo03Alternativa {
@Factory(dataProvider = "tester")
public Object[] factory(Tester tester) {
return new Object[] {
new Exemplo03(tester)
};
}
@DataProvider
public Object[][] tester() {
return new Object[][] {
{Tester.OBJECTOS},
{Tester.REFERENCIA}
};
}
}
Assim, quando for necessário adicionar mais um teste à lista, é necessário fazê-lo em apenas um lugar.
Projeto Objectos AsciiDoc
No projeto Objectos AsciiDoc eu utilizo a primeira técnica. Isto é, quando um teste é adicionado, eu preciso atualizar a lista em duas classes distintas.
A razão é que as classes distintas permitem que eu rode os grupos de testes separadamente no IDE. Isto é importante porque, no caso do Objectos AsciiDoc, a implementação de referência é consideravelmente mais lenta.
Até o próximo artigo
Por hoje é isto. Espero que tenha gostado.
O código para todos os exemplos pode ser encontrado no GitHub.
Se tiver comentários, dúvidas ou correções sobre este post, por favor, envie um email.
Siga-me no Twitter.