Testes parametrizados permitem que desenvolvedores testem seu código de forma eficiente com uma gama de valores de entrada. No domínio do teste JUnit, usuários experientes já lidaram por muito tempo com a complexidade de implementar esses testes. Mas com o lançamento do JUnit 5.7, uma nova era de testes parametrizados entra em cena, oferecendo suporte de primeira classe e capacidades aprimoradas para desenvolvedores. Vamos explorar as emocionantes possibilidades que o JUnit 5.7 traz para a mesa de testes parametrizados!
Amostras de Parametrização do JUnit 5.7 Docs
Vamos ver alguns exemplos da documentação:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
A anotação @ParameterizedTest
deve ser acompanhada por uma das várias anotações de origem fornecidas, descrevendo de onde pegar os parâmetros. A fonte dos parâmetros é frequentemente chamada de “provedor de dados”.
I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:
- O
@ValueSource
é limitado a fornecer apenas um valor de parâmetro. Em outras palavras, o método de teste não pode ter mais de um argumento, e os tipos que podem ser usados são restritos também. - Passar múltiplos argumentos é parcialmente abordado pelo
@CsvSource
, analisando cada string em um registro que é então passado como argumentos campo a campo. Isso pode ficar difícil de ler com strings longas e/ou muitos argumentos. Os tipos que podem ser usados também são restritos — mais sobre isso mais tarde. - Todas as fontes que declararam os valores reais em anotações estão restritas a valores que são constantes em tempo de compilação (limitação de anotações do Java, não JUnit).
@MethodSource
e@ArgumentsSource
fornecem um fluxo/coleção de n-tuplas (não tipadas) que são então passadas como argumentos de método. Vários tipos reais são suportados para representar a sequência de n-tuplas, mas nenhum deles garante que caberão na lista de argumentos do método. Este tipo de fonte requer métodos ou classes adicionais, mas não impõe restrições sobre onde e como obter os dados de teste.
Como você pode ver, os tipos de fontes disponíveis variam desde os simples (fáceis de usar, mas limitados em funcionalidade) até os mais flexíveis, que requerem mais código para funcionar.
- Nota lateral — Isto geralmente é um sinal de bom design: um pouco de código é necessário para funcionalidade essencial, e adicionar complexidade extra é justificado quando usado para permitir um caso de uso mais exigente.
O que não parece se encaixar neste continuum hipotético de simples para flexível é o @EnumSource
. Vamos olhar para este exemplo não trivial de quatro conjuntos de parâmetros com 2 valores cada.
- Nota — Enquanto o
@EnumSource
passa o valor do enum como um único parâmetro do método de teste, conceitualmente, o teste é parametrizado pelos campos do enum, o que não impõe restrição sobre o número de parâmetros.
enum Direction {
UP(0, '^'),
RIGHT(90, '>'),
DOWN(180, 'v'),
LEFT(270, '<');
private final int degrees;
private final char ch;
Direction(int degrees, char ch) {
this.degrees = degrees;
this.ch = ch;
}
}
@ParameterizedTest
@EnumSource
void direction(Direction dir) {
assertEquals(0, dir.degrees % 90);
assertFalse(Character.isWhitespace(dir.ch));
int orientation = player.getOrientation();
player.turn(dir);
assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
}
Pense nisso: a lista de valores hardcoded restringe sua flexibilidade severamente (sem dados externos ou gerados), enquanto a quantidade de código adicional necessária para declarar o enum
torna essa opção bastante verbosa em comparação com, digamos, @CsvSource
.
Porém, essa é apenas uma primeira impressão. Vamos ver como isso pode ficar elegante ao aproveitar o verdadeiro poder dos enums do Java.
- Nota lateral: Este artigo não aborda a verificação de enums que fazem parte do seu código de produção. Esses, é claro, precisaram ser declarados independentemente de como você escolhe verificá-los. Em vez disso, ele se concentra em quando e como expressar seus dados de teste na forma de enums.
Quando Usar
Há situações em que os enums performam melhor do que as alternativas:
Parâmetros Múltiplos por Teste
Quando tudo o que você precisa é de um único parâmetro, você provavelmente não quer complicar as coisas além do @ValueSource
. Mas assim que você precisa de múltiplos parâmetros — digamos, entradas e resultados esperados — você tem que recorrer a @CsvSource,
@MethodSource/@ArgumentsSource
ou @EnumSource
.
De certa forma, o enum
permite que você “esconda” qualquer número de campos de dados.
Portanto, quando você precisar adicionar mais parâmetros ao método de teste no futuro, simplesmente adiciona mais campos em seus enums existentes, deixando as assinaturas dos métodos de teste intactas. Isso se torna invaluable quando você reutiliza seu provedor de dados em múltiplos testes.
Para outras fontes, é necessário empregar ArgumentsAccessor
s ou ArgumentsAggregator
s pela flexibilidade que os enums têm “fora da caixa”.
Segurança de Tipo
Para desenvolvedores Java, isso deve ser um grande ponto.
Parâmetros lidos de CSV (arquivos ou literais), @MethodSource
ou @ArgumentsSource
, eles não fornecem garantia em tempo de compilação de que a contagem de parâmetros e seus tipos vão corresponder à assinatura.
Obviamente, o JUnit vai reclamar em tempo de execução, mas esqueça qualquer assistência de código do seu IDE.
Como antes, isso se acumula quando você reutiliza os mesmos parâmetros para múltiplos testes. Usar uma abordagem de tipo seguro seria um grande ganho ao estender o conjunto de parâmetros no futuro.
Tipos Personalizados
Esta é principalmente uma vantagem sobre fontes baseadas em texto, como as que leem dados de CSV — os valores codificados no texto precisam ser convertidos para tipos Java.
Se você tem uma classe personalizada para instanciar a partir do registro CSV, pode fazer isso usando ArgumentsAggregator
. No entanto, sua declaração de dados ainda não é segura em termos de tipo — qualquer correspondência incorreta entre a assinatura do método e os dados declarados aparecerá em tempo de execução ao “agregar” argumentos. Sem mencionar que declarar a classe do agregador adiciona mais código de suporte necessário para que sua parametrização funcione. E sempre favorecemos @CsvSource
sobre @EnumSource
para evitar o código extra.
Documentável
Ao contrário dos outros métodos, a fonte enum tem símbolos Java para ambos os conjuntos de parâmetros (instâncias enum) e todos os parâmetros que contêm (campos enum). Eles fornecem um local direto onde anexar documentação na sua forma mais natural — o JavaDoc.
Não é que a documentação não possa ser colocada em outro lugar, mas será — por definição — colocada mais longe do que dokumenta e, portanto, mais difícil de encontrar e mais fácil de se tornar obsoleta.
Mas há mais!
Agora: Enums. São. Classes.
Parece que muitos desenvolvedores junior ainda não perceberam quão poderosos os enums do Java realmente são.
Em outras linguagens de programação, eles são realmente apenas constantes glorificadas. Mas no Java, eles são implementações convenientes de um padrão de design Flyweight com (muitos dos) benefícios de classes completas.
Por que isso é uma coisa boa?
Comportamento Relacionado ao Test Fixture
Como com qualquer outra classe, os enums podem ter métodos adicionados a eles.
Isso se torna útil se os parâmetros de teste enum são reutilizados entre testes — mesmos dados, apenas testados de maneira um pouco diferente. Para trabalhar eficazmente com os parâmetros sem cópia e cola significativa, algum código de ajuda precisa ser compartilhado entre esses testes.
Não é algo que uma classe auxiliar e alguns métodos estáticos não resolveriam.
- Nota lateral: Note que tal design sofre de Covetância de Funcionalidade. Métodos de teste — ou pior, métodos de classe auxiliar — teriam que extrair os dados dos objetos enum para realizar ações sobre esses dados.
Embora essa seja a (única) forma em programação procedural, no mundo orientado a objetos, podemos fazer melhor.
Declarando os métodos “auxiliares” diretamente na declaração do enum, moveríamos o código para onde os dados estão. Ou, para usar linguagem de POO, os métodos auxiliares se tornariam o “comportamento” dos fixtures de teste implementados como enums. Isso não só tornaria o código mais idiomático (chamando métodos sensíveis em instâncias em vez de métodos estáticos passando dados), mas também facilitaria a reutilização de parâmetros enum entre casos de teste.
Herença
Enums podem implementar interfaces com métodos (padrão). Quando usados com sensatez, isso pode ser explorado para compartilhar comportamento entre vários provedores de dados — vários enums.
Um exemplo que facilmente vem à mente são enums separados para testes positivos e negativos. Se eles representam um tipo semelhante de fixture de teste, é provável que tenham algum comportamento para compartilhar.
A Palavra é Barata
Vamos ilustrar isso em uma suíte de testes de um conversor hipotético de arquivos de código-fonte, não muito diferente do que realiza a conversão de Python 2 para Python 3.
Para ter confiança real no que uma ferramenta tão abrangente faz, acabaríamos com um conjunto extenso de arquivos de entrada que manifestam vários aspectos da linguagem, e arquivos de correspondência para comparar o resultado da conversão. Exceto isso, é necessário verificar quais avisos/erros são apresentados ao usuário para entradas problemáticas.
Esto é uma adaptação natural para testes parametrizados devido ao grande número de amostras a serem verificadas, mas não se encaixa perfeitamente em nenhuma das fontes de parâmetros simples do JUnit, pois os dados são um pouco complexos.
Veja abaixo:
enum Conversion {
CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
"Using module 'xyz' that is deprecated"
)),
SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
// Muitos, muitos outros ...
@Nonnull
final String inFile;
@CheckForNull
final String expectedOutput;
@CheckForNull
final Exception expectedException;
@Nonnull
final Set expectedWarnings;
Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
this(inFile, expectedOutput, null, expectedWarnings);
}
Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
this(inFile, null, expectedException, Set.of());
}
Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
this.inFile = inFile;
this.expectedOutput = expectedOutput;
this.expectedException = expectedException;
this.expectedWarnings = expectedWarnings;
}
public File getV2File() { ... }
public File getV3File() { ... }
}
@ParameterizedTest
@EnumSource
void upgrade(Conversion con) {
try {
File actual = convert(con.getV2File());
if (con.expectedException != null) {
fail("No exception thrown when one was expected", con.expectedException);
}
assertEquals(con.expectedWarnings, getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
} catch (Exception ex) {
assertTypeAndMessageEquals(con.expectedException, ex);
}
}
O uso de enums não nos restringe em quanto complexos podem ser os dados. Como você pode ver, podemos definir vários construtores convenientes nos enums, então declarar novos conjuntos de parâmetros é agradável e limpo. Isso evita o uso de longas listas de argumentos que frequentemente terminam cheias de muitos valores “vazios” (nulos, strings vazias ou coleções) que deixam alguém se perguntando o que o argumento #7 — você sabe, um dos nulos — realmente representa.
Notem como enums permitem o uso de tipos complexos (Set
, RuntimeException
) sem restrições ou conversões mágicas. Passar esses dados também é completamente seguro.
Agora, sei o que você está pensando. Isso é estranhamente verboso. Bem, até um ponto. Realisticamente, você terá muito mais amostras de dados para verificar, então a quantidade de código de boilerplate será menos significativa em comparação.
Também vejam como testes relacionados podem ser escritos usando os mesmos enums e seus métodos auxiliares:
@ParameterizedTest
@EnumSource
// Atualizar arquivos já atualizados sempre passa, não faz alterações, não emite avisos.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// Fazer downgrade de arquivos criados pelo procedimento de atualização é esperado para sempre passar sem avisos.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
Some More Talk After All
Conceptualmente, @EnumSource
encoraja você a criar uma descrição complexa e legível por máquina de cenários de teste individuais, des樊tando a linha entre provedores de dados e fixtures de teste.
Outra coisa ótima sobre ter cada conjunto de dados expresso como um símbolo Java (elemento de enum) é que eles podem ser usados individualmente; completamente fora de provedores de dados/testes parametrizados. Como eles têm um nome razoável e são autocontidos (em termos de dados e comportamento), eles contribuem para testes bonitos e legíveis.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read() é um método auxiliar compartilhado por todos os FixtureXmls
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
Agora, @EnumSource
não vai ser uma das suas fontes de argumento mais usadas, e isso é uma boa coisa, pois usá-la em excesso não seria benéfico. Mas nas circunstâncias certas, é útil saber como usar tudo o que ela tem a oferecer.
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource