Injetar parâmetros nos testes de unidade JUnit Jupiter
1. Overview
Antes do JUnit 5, para introduzir um novo recurso interessante, a equipe do JUnit precisaria fazê-lo na API principal. Com o JUnit 5, a equipe decidiu que era hora de ampliar a capacidade da API JUnit principal para fora da própria JUnit, uma filosofia principal do JUnit 5 chamada “https://github.com/junit-team/junit5/wiki/Core-Principles [prefira pontos de extensão sobre recursos] ”.
Neste artigo, vamos nos concentrar em uma dessas interfaces de ponto de extensão -ParameterResolver - que você pode usar para injetar parâmetros em seus métodos de teste. Existem algumas maneiras diferentes de tornar a plataforma JUnit ciente de sua extensão (um processo conhecido como "registro") e, neste artigo, vamos nos concentrar no registro dedeclarative (ou seja, registro via código-fonte) .
2. ParameterResolver
A injeção de parâmetros nos métodos de teste poderia ser feita usando a API do JUnit 4, mas era bastante limitada. Com o JUnit 5, a API Jupiter pode ser estendida - implementandoParameterResolver - para servir objetos de qualquer tipo aos seus métodos de teste. Vamos dar uma olhada.
2.1. FooParameterResolver
public class FooParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Foo.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return new Foo();
}
}
Primeiro, precisamos implementarParameterResolver –, que tem dois métodos:
-
supportsParameter() - retorna verdadeiro se o tipo do parâmetro for compatível (Foo neste exemplo), e
-
resolveParamater() - serve um objeto do tipo correto (uma nova instância de Foo neste exemplo), que será então injetado em seu método de teste
2.2. FooTest
@ExtendWith(FooParameterResolver.class)
public class FooTest {
@Test
public void testIt(Foo fooInstance) {
// TEST CODE GOES HERE
}
}
Então, para usar a extensão, precisamos declará-la - ou seja, informar a plataforma JUnit sobre ela - por meio da anotação@ExtendWith (Linha 1).
Quando a plataforma JUnit executa seu teste de unidade, ela obterá uma instânciaFoo deFooParameterResolvere a passará para o métodotestIt() (Linha 4).
A extensão tem umscope of influence, que ativa a extensão, dependendo dewhere declarada.
A extensão pode estar ativa no:
-
nível de método, onde está ativo apenas para esse método, ou
-
nível de classe, onde está ativo para toda a classe de teste, ou@Nested classe de teste, como veremos em breve
Note: you should not declare a ParameterResolverat both scopes for the same parameter type, or the JUnit Platform will complain about this ambiguity.
Para este artigo, veremos como escrever e usar duas extensões para injetar objetosPerson: uma que injeta dados "bons" (chamadosValidPersonParameterResolver) e outra que injeta dados "ruins" (InvalidPersonParameterResolver). Usaremos esses dados para testar a unidade de uma classe chamadaPersonValidator, que valida o estado de um objetoPerson.
3. Write the Extensions
Agora que entendemos o que é uma extensãoParameterResolver, estamos prontos para escrever:
-
um que fornecevalid objetosPerson (ValidPersonParameterResolver), e
-
um que forneceinvalid objetosPerson (InvalidPersonParameterResolver)
3.1. ValidPersonParameterResolver
public class ValidPersonParameterResolver implements ParameterResolver {
public static Person[] VALID_PERSONS = {
new Person().setId(1L).setLastName("Adams").setFirstName("Jill"),
new Person().setId(2L).setLastName("Baker").setFirstName("James"),
new Person().setId(3L).setLastName("Carter").setFirstName("Samanta"),
new Person().setId(4L).setLastName("Daniels").setFirstName("Joseph"),
new Person().setId(5L).setLastName("English").setFirstName("Jane"),
new Person().setId(6L).setLastName("Fontana").setFirstName("Enrique"),
};
Observe a matrizVALID_PERSONS de objetosPerson. Este é o repositório de objetosPerson válidos, dos quais um será escolhido aleatoriamente cada vez que o métodoresolveParameter() for chamado pela plataforma JUnit.
Ter os objetos Person válidos aqui realiza duas coisas:
-
Separação de preocupações entre o teste de unidade e os dados que o impulsionam
-
Reutilize, caso outros testes de unidade exijam objetosPerson válidos para conduzi-los
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}
Se o tipo de parâmetro forPerson, a extensão informa à plataforma JUnit que ela oferece suporte a esse tipo de parâmetro, caso contrário, retorna falso, dizendo que não.
Por que isso importa? Embora os exemplos neste artigo sejam simples, em um aplicativo do mundo real, as classes de teste de unidade podem ser muito grandes e complexas, com muitos métodos de teste que utilizam diferentes tipos de parâmetros. A plataforma JUnit deve verificar todos osParameterResolvers registrados ao resolver os parâmetroswithin the current scope of influence.
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = VALID_PERSONS[new Random().nextInt(VALID_PERSONS.length)];
}
return ret;
}
Um objetoPerson aleatório é retornado da matrizVALID_PERSONS. Observe comoresolveParameter() só é chamado pela plataforma JUnit sesupportsParameter() retornartrue.
3.2. InvalidPersonParameterResolver
public class InvalidPersonParameterResolver implements ParameterResolver {
public static Person[] INVALID_PERSONS = {
new Person().setId(1L).setLastName("Ad_ams").setFirstName("Jill,"),
new Person().setId(2L).setLastName(",Baker").setFirstName(""),
new Person().setId(3L).setLastName(null).setFirstName(null),
new Person().setId(4L).setLastName("Daniel&").setFirstName("{Joseph}"),
new Person().setId(5L).setLastName("").setFirstName("English, Jane"),
new Person()/*.setId(6L).setLastName("Fontana").setFirstName("Enrique")*/,
};
Observe a matrizINVALID_PERSONS de objetosPerson. Assim como comValidPersonParameterResolver, esta classe contém um armazenamento de dados “ruins” (ou seja, inválidos) para uso por testes de unidade para garantir, por exemplo, quePersonValidator.ValidationExceptions sejam lançados corretamente na presença de dados inválidos :
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = INVALID_PERSONS[new Random().nextInt(INVALID_PERSONS.length)];
}
return ret;
}
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}
O restante desta classe naturalmente se comporta exatamente como seu equivalente "bom".
4. Declarar e usar as extensões
Agora que temos doisParameterResolvers, é hora de colocá-los em uso. Vamos criar uma classe de teste JUnit paraPersonValidator chamadaPersonValidatorTest.
Usaremos vários recursos disponíveis apenas no JUnit Jupiter:
-
@DisplayName - este é o nome que aparece nos relatórios de teste e muito mais legível por humanos
-
@Nested - cria uma classe de teste aninhada, completa com seu próprio ciclo de vida de teste, separada de sua classe pai
-
@RepeatedTest - o teste é repetido o número de vezes especificado pelo atributo de valor (10 em cada exemplo)
Ao usar classes @Nested, podemos testar dados válidos e inválidos na mesma classe de teste, ao mesmo tempo que os mantemos completamente isolados um do outro:
@DisplayName("Testing PersonValidator")
public class PersonValidatorTest {
@Nested
@DisplayName("When using Valid data")
@ExtendWith(ValidPersonParameterResolver.class)
public class ValidData {
@RepeatedTest(value = 10)
@DisplayName("All first names are valid")
public void validateFirstName(Person person) {
try {
assertTrue(PersonValidator.validateFirstName(person));
} catch (PersonValidator.ValidationException e) {
fail("Exception not expected: " + e.getLocalizedMessage());
}
}
}
@Nested
@DisplayName("When using Invalid data")
@ExtendWith(InvalidPersonParameterResolver.class)
public class InvalidData {
@RepeatedTest(value = 10)
@DisplayName("All first names are invalid")
public void validateFirstName(Person person) {
assertThrows(
PersonValidator.ValidationException.class,
() -> PersonValidator.validateFirstName(person));
}
}
}
Observe como podemos usar as extensõesValidPersonParameterResolvereInvalidPersonParameterResolver dentro da mesma classe de teste principal - declarando-as apenas no nível de classe @Nested. Experimente isso com o JUnit 4! (Alerta de spoiler: você não pode fazê-lo!)
5. Conclusão
Neste artigo, exploramos como escrever duas extensõesParameterResolver - para servir objetos válidos e inválidos. Em seguida, demos uma olhada em como usar essas implementações de doisParameterResolver em um teste de unidade.
Como sempre, o código está disponívelover on Github.
E, se você quiser aprender mais sobre o modelo de extensão JUnit Júpiter, verifiqueJUnit 5 User’s Guide oupart 2 of my tutorial on developerWorks.