Testando uma classe abstrata com JUnit
1. Visão geral
Neste tutorial, vamos analisar vários casos de uso e possíveis soluções alternativas para o teste de unidade de classes abstratas com métodos não abstratos.
Observe quetesting abstract classes should almost always go through the public API of the concrete implementations, então não aplique as técnicas abaixo a menos que você tenha certeza do que está fazendo.
2. Dependências do Maven
Vamos começar com as dependências do Maven:
org.junit.jupiter
junit-jupiter-engine
5.1.0
test
org.mockito
mockito-core
2.8.9
test
org.powermock
powermock-module-junit4
1.7.4
test
junit
junit
org.powermock
powermock-api-mockito2
1.7.4
test
Você pode encontrar as versões mais recentes dessas bibliotecas emMaven Central.
Powermock não é totalmente compatível com Junit5. Além disso,powermock-module-junit4 é usado apenas para um exemplo apresentado na seção 5.
3. Método Independente Não Abstrato
Vamos considerar um caso em que temos uma classe abstrata com um método público não abstrato:
public abstract class AbstractIndependent {
public abstract int abstractFunc();
public String defaultImpl() {
return "DEFAULT-1";
}
}
Queremos testar o métododefaultImpl(), e temos duas soluções possíveis - usando uma classe concreta ou usando Mockito.
3.1. Usando uma classe concreta
Crie uma classe concreta que estendaAbstractIndependent class e use-a para testar o método:
public class ConcreteImpl extends AbstractIndependent {
@Override
public int abstractFunc() {
return 4;
}
}
@Test
public void givenNonAbstractMethod_whenConcreteImpl_testCorrectBehaviour() {
ConcreteImpl conClass = new ConcreteImpl();
String actual = conClass.defaultImpl();
assertEquals("DEFAULT-1", actual);
}
A desvantagem desta solução é a necessidade de criar a classe concreta com implementações fictícias de todos os métodos abstratos.
3.2. Usando o Mockito
Como alternativa, podemos usarMockito para criar uma simulação:
@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
AbstractIndependent absCls = Mockito.mock(
AbstractIndependent.class,
Mockito.CALLS_REAL_METHODS);
assertEquals("DEFAULT-1", absCls.defaultImpl());
}
A parte mais importante aqui épreparation of the mock to use the real code when a method is invoked usandoMockito.CALLS_REAL_METHODS.
4. Método abstrato chamado de método não abstrato
Nesse caso, o método não abstrato define o fluxo de execução global, enquanto o método abstrato pode ser gravado de diferentes maneiras, dependendo do caso de uso:
public abstract class AbstractMethodCalling {
public abstract String abstractFunc();
public String defaultImpl() {
String res = abstractFunc();
return (res == null) ? "Default" : (res + " Default");
}
}
Para testar esse código, podemos usar as mesmas duas abordagens de antes - criar uma classe concreta ou usar o Mockito para criar uma simulação:
@Test
public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() {
AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
Mockito.when(cls.abstractFunc())
.thenReturn("Abstract");
Mockito.doCallRealMethod()
.when(cls)
.defaultImpl();
assertEquals("Abstract Default", cls.defaultImpl());
}
Aqui, oabstractFunc() é fragmentado com o valor de retorno que preferimos para o teste. Isso significa que quando chamarmos o método não abstratodefaultImpl(), ele usará este stub.
5. Método não abstrato com obstrução de teste
Em alguns cenários, o método que queremos testar chama um método privado que contém uma obstrução de teste.
Precisamos ignorar o método de teste obstrutivo antes de testar o método de destino:
public abstract class AbstractPrivateMethods {
public abstract int abstractFunc();
public String defaultImpl() {
return getCurrentDateTime() + "DEFAULT-1";
}
private String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
Neste exemplo, o métododefaultImpl() chama o método privadogetCurrentDateTime(). Esse método privado obtém o tempo atual no tempo de execução, o que deve ser evitado em nossos testes de unidade.
Agora, para simular o comportamento padrão deste método privado, não podemos nem usarMockito porque ele não pode controlar métodos privados.
Em vez disso, precisamos usarPowerMock (note that this example works only with JUnit 4 because support for this dependency isn’t available for JUnit 5):
@RunWith(PowerMockRunner.class)
@PrepareForTest(AbstractPrivateMethods.class)
public class AbstractPrivateMethodsUnitTest {
@Test
public void whenMockPrivateMethod_thenVerifyBehaviour() {
AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class);
PowerMockito.doCallRealMethod()
.when(mockClass)
.defaultImpl();
String dateTime = LocalDateTime.now().toString();
PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime");
String actual = mockClass.defaultImpl();
assertEquals(dateTime + "DEFAULT-1", actual);
}
}
Bits importantes neste exemplo:
-
@RunWith define o PowerMock como o executor para o teste
-
@PrepareForTest(class) tells PowerMock para preparar a classe para processamento posterior
Curiosamente, estamos pedindo aPowerMock para fazer o stub do método privadogetCurrentDateTime(). PowerMock usará reflexão para encontrá-lo porque não é acessível de fora.
Portanto,, quando chamarmosdefaultImpl(), o stub criado para um método privado será chamado em vez do método real.
6. Método não abstrato que acessa campos de instância
Classes abstratas podem ter um estado interno implementado com campos de classe. O valor dos campos pode ter um efeito significativo no método que está sendo testado.
Se um campo é público ou protegido, podemos acessá-lo facilmente a partir do método de teste.
Mas se for privado, temos que usarPowerMockito:
public abstract class AbstractInstanceFields {
protected int count;
private boolean active = false;
public abstract int abstractFunc();
public String testFunc() {
if (count > 5) {
return "Overflow";
}
return active ? "Added" : "Blocked";
}
}
Aqui, o métodotestFunc() está usando os campos de nível de instânciacounteactive antes de retornar.
Ao testartestFunc(), podemos alterar o valor do campocount acessando a instância criada usandoMockito.
Por outro lado, para testar o comportamento com o campo privadoactive, teremos novamente que usarPowerMockito, e sua classeWhitebox:
@Test
public void whenPowerMockitoAndActiveFieldTrue_thenCorrectBehaviour() {
AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class);
PowerMockito.doCallRealMethod()
.when(instClass)
.testFunc();
Whitebox.setInternalState(instClass, "active", true);
assertEquals("Added", instClass.testFunc());
}
Estamos criando uma classe stub usandoPowerMockito.mock(), e estamos usando a classeWhitebox para controlar o estado interno do objeto.
O valor do campoactive é alterado paratrue.
7. Conclusão
Neste tutorial, vimos vários exemplos que abrangem muitos casos de uso. Podemos usar classes abstratas em muitos outros cenários, dependendo do design seguido.
Além disso, escrever testes de unidade para métodos de classe abstrata é tão importante quanto para classes e métodos normais. Podemos testar cada um deles usando técnicas diferentes ou diferentes bibliotecas de suporte de teste disponíveis.
O código-fonte completo está disponívelover on GitHub.