Java - Introdução ao Mockito com JUnit
Mockito é um framework open-source que nos permite criar facilmente double tests (mocks). Um double test é um termo genérico para qualquer caso em que substituímos um objeto de produção para fins de teste. No Mockito, geralmente trabalhamos com os seguintes tipos de double tests:
- Stubs – São objetos que possuem valores de retorno pré-definidos para as execuções de métodos feitas durante o teste;
- Spies – São objetos semelhantes aos stubs, mas também registram as estatísticas de como foram executados;
- Mocks – São objetos que possuem valores de retorno para execuções de métodos feitas durante o teste e têm expectativas registradas dessas execuções. Mocks podem lançar uma exceção se receberem uma chamada que não esperavam e são verificados para garantir que receberam todas as chamadas que esperavam.
Podemos fazer mock de interfaces e classes na classe de teste. O Mockito também ajuda a produzir código padrão mínimo, se usarmos as anotações do Mockito. Depois de criada, uma simulação se lembrará de todas as interações. Então, podemos verificar seletivamente quaisquer interações nas quais estamos interessados.
Mockito Setup
Maven
Para adicionar o Mockito ao projeto, podemos adicionar a versão mais recente por qualquer meio, por exemplo, Maven, Gradle ou arquivo jar.
pom.xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
build.gradle
testCompile group: 'org.mockito', name: 'mockito-core', version: '4.6.1'
Bootstrapping com JUnit
Para processar as anotações do Mockito com o JUnit 5, precisamos usar o MockitoExtension da seguinte forma:
@ExtendWith(MockitoExtension.class)
public class ApplicationTest {
//code
}
Para JUnit 4 legado, podemos usar as classes MockitoJUnitRunner ou MockitoRule.
@RunWith(MockitoJUnitRunner.class)
public class ApplicationTest {
//code
}
public class ApplicationTest {
@Rule public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
//code
}
O stubbing estrito garante testes limpos, reduz a duplicação do código de teste e melhora a capacidade de depuração. O teste falha antecipadamente quando o código em teste invoca um método stubbed com argumentos diferentes ou quando stubs não utilizados estão presentes.
Como alternativa, podemos inicializar o Mockito programaticamente usando o método openMocks() em algum lugar na classe base ou em um executor de teste. Este método inicializa campos anotados com anotações Mockito @Mock, @Spy, @Captor, @InjectMocks.
O método initMocks() usado anteriormente, agora está obsoleto.
public class ApplicationTest {
MockitoAnnotations.openMocks(this);
}
Mockito Annotations
Antes de bater no teclado para escrever testes de unidade, vamos passar rapidamente pelas anotações úteis do mockito:
- @Mock é usado para criação de simulação. Isso torna a classe de teste mais legível.
- @Spy é usado para criar uma instância de espionagem. Podemos usá-lo em vez do método spy(Object).
- @InjectMocks é usado para instanciar o objeto testado automaticamente e injetar nele todas as dependências anotadas @Mock ou @Spy (se aplicável). Vale a pena saber a diferença entre as anotações @Mock e @InitMocks.
- @Captor é usado para criar um captor de argumento.
public class ApplicationTest {
@Mock
Depedency mock;
@InjectMocks
Service codeUnderTest;
}
Vamos verificar alguns comportamentos!
Os exemplos a seguir simulam uma lista, porque a maioria das pessoas está familiarizada com a interface (como os métodos add(), get(), clear()). Na verdade, não faça mock da classe List. Em vez disso, use uma instância real.
//Let's import Mockito statically so that the code looks clearer
import static org.mockito.Mockito.*;
//mock creation
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();
Que tal algum stubbing?
//You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);
//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
//following prints "first"
System.out.println(mockedList.get(0));
//following throws runtime exception
System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
//Although it is possible to verify a stubbed invocation, usually it's just redundant
//If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
//If your code doesn't care what get(0) returns, then it should not be stubbed.
verify(mockedList).get(0);
Argument matchers
//stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");
//stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
when(mockedList.contains(argThat(isValid()))).thenReturn(true);
//following prints "element"
System.out.println(mockedList.get(999));
//you can also verify using an argument matcher
verify(mockedList).get(anyInt());
//argument matchers can also be written as Java 8 Lambdas
verify(mockedList).add(argThat(someString -> someString.length() > 5));
Verificando o número exato de invocações / at least x / never
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
//exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
//verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened");
//verification using atLeast()/atMost()
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");
Stubbing métodos void com exceções
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
Verificação em ordem
// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");
//create an inOrder verifier for a single mock
InOrder inOrder = inOrder(singleMock);
//following will make sure that add is first called with "was added first", then with "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// B. Multiple mocks that must be used in a particular order
List firstMock = mock(List.class);
List secondMock = mock(List.class);
//using mocks
firstMock.add("was called first");
secondMock.add("was called second");
//create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);
//following will make sure that firstMock was called before secondMock
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
// Oh, and A + B can be mixed together at will
Certificando-se de que as interações nunca aconteceram na simulação
//using mocks - only mockOne is interacted
mockOne.add("one");
//ordinary verification
verify(mockOne).add("one");
//verify that method was never called on a mock
verify(mockOne, never()).add("two");
Encontrando invocações redundantes
//using mocks
mockedList.add("one");
mockedList.add("two");
verify(mockedList).add("one");
//following verification will fail
verifyNoMoreInteractions(mockedList);
Stubbing de chamadas consecutivas
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
//First call: throws runtime exception:
mock.someMethod("some arg");
//Second call: prints "foo"
System.out.println(mock.someMethod("some arg"));
//Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.someMethod("some arg"));
Versão alternativa e mais curta de stubbing consecutivo
when(mock.someMethod("some arg"))
.thenReturn("one", "two", "three");
Aviso: se em vez de encadear chamadas .thenReturn(), vários stubbing com os mesmos matchers ou argumentos forem usados, cada stubbing substituirá o anterior.
//All mock.someMethod("some arg") calls will return "two"
when(mock.someMethod("some arg"))
.thenReturn("one")
when(mock.someMethod("some arg"))
.thenReturn("two")
Stubbing com callbacks
Permite stubbing com interface de resposta genérica. Ainda há outro recurso controverso que não foi incluído no Mockito originalmente. Recomendamos fazer stub com thenReturn() ou thenThrow(), que deve ser suficiente para testar/test-drive qualquer código limpo e simples. No entanto, se você precisar fazer um stub com a interface genérica Answer, aqui está um exemplo:
when(mock.someMethod(anyString())).thenAnswer(
new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + Arrays.toString(args);
}
});
//Following prints "called with arguments: [foo]"
System.out.println(mock.someMethod("foo"));
doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() família de métodos
Fazer stub de métodos void requer uma abordagem diferente de when(Object) porque o compilador não gosta de métodos void dentro de colchetes.
Use doThrow() quando quiser interromper um método void com uma exceção:
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();
Você pode usar doThrow(), doAnswer(), doNothing(), doReturn() e doCallRealMethod() no lugar da chamada correspondente com when(), para qualquer método. É necessário quando você:
- stub métodos void;
- stub métodos em objetos spy;
- stub o mesmo método mais de uma vez, para mudar o comportamento de um mock no meio de um teste.
Mas você pode preferir usar esses métodos no lugar da alternativa com when(), para todas as suas chamadas de stub.
Spying objetos reais
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
//using the spy calls *real* methods
spy.add("one");
spy.add("two");
//prints "one" - the first element of a list
System.out.println(spy.get(0));
//size() method was stubbed - 100 is printed
System.out.println(spy.size());
//optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");
Dica importante sobre espionagem de objetos reais:
Às vezes é impossível ou impraticável usar when(Object) para spies stub. Portanto, ao usar espiões, considere doReturn|Answer|Throw() família de métodos para stubbing. Exemplo:
List list = new LinkedList();
List spy = spy(list);
//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo");
//You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);
Capturando argumentos para outras assertions
ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
verify(mock).doSomething(argument.capture());
assertEquals("John", argument.getValue().getName());
Mocks reais parciais
//you can create partial mock with spy() method:
List list = spy(new LinkedList());
//you can enable partial mock capabilities selectively on mocks:
Foo mock = mock(Foo.class);
//Be sure the real implementation is 'safe'.
//If real implementation throws exceptions or depends on the specific state of the object then //you're in trouble.
when(mock.someMethod()).thenCallRealMethod();
Redefinindo mocks
Os usuários inteligentes do Mockito dificilmente usam esse recurso porque sabem que pode ser um sinal de testes ruins. Normalmente, você não precisa redefinir seus mocks, basta criar novos mocks para cada método de teste.
Em vez de reset(), considere escrever métodos de teste simples, pequenos e focados em testes longos e super especificados.
List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);
reset(mock);
//at this point the mock forgot any interactions and stubbing
Mocks Serializáveis
Mocks podem se tornar serializáveis. Com esse recurso, você pode usar um mock em um local que exija que as dependências sejam serializáveis.
Observação: Isso raramente deve ser usado em testes de unidade.
List serializableMock = mock(List.class, withSettings().serializable());
A simulação pode ser serializada assumindo que todos os requisitos normais de serialização sejam atendidos pela classe.
List<Object> list = new ArrayList<Object>();
List<Object> spy = mock(ArrayList.class, withSettings()
.spiedInstance(list)
.defaultAnswer(CALLS_REAL_METHODS)
.serializable());
Instanciação automática de @Spies, @InjectMocks e injeção de construtor
Mockito agora tentará instanciar @Spy e instanciará campos @InjectMocks usando injeção de construtor, injeção de setter ou injeção de campo.
Para aproveitar esse recurso, você precisa usar MockitoAnnotations.openMocks(Object), MockitoJUnitRunner ou MockitoRule.
//instead:
@Spy BeerDrinker drinker = new BeerDrinker();
//you can write:
@Spy BeerDrinker drinker;
//same applies to @InjectMocks annotation:
@InjectMocks LocalPub;
Verificação ignorando stubs
O Mockito agora permitirá ignorar o stubbing para fins de verificação. Às vezes, é útil quando associado a VerifyNoMoreInteractions() ou a verificação inOrder(). Ajuda a evitar a verificação redundante de chamadas fragmentadas - normalmente não estamos interessados em verificar fragmentos.
Observação: O ignoreStubs() pode levar ao uso excessivo de verifyNoMoreInteractions(ignoreStubs(...)). Lembre-se que Mockito não recomenda bombardear todos os testes com VerifyNoMoreInteractions() pelos motivos descritos no javadoc para VerifyNoMoreInteractions(Object...).
Alguns exemplos:
verify(mock).foo();
verify(mockTwo).bar();
//ignores all stubbed methods:
verifyNoMoreInteractions(ignoreStubs(mock, mockTwo));
//creates InOrder that will ignore stubbed
InOrder inOrder = inOrder(ignoreStubs(mock, mockTwo));
inOrder.verify(mock).foo();
inOrder.verify(mockTwo).bar();
inOrder.verifyNoMoreInteractions();
Verificação de estilo BDD
Ativa a verificação de estilo de Desenvolvimento Orientado a Comportamento (BDD), iniciando a verificação com a palavra-chave BDD then.
given(dog.bark()).willReturn(2);
// when
...
then(person).should(times(2)).ride(bike);
Suporte a Java 8 Lambda Matcher
Você pode usar expressões lambda Java 8 com ArgumentMatcher para reduzir a dependência de ArgumentCaptor. Se você precisar verificar se a entrada para uma chamada de função em uma simulação estava correta, utilize o ArgumentCaptor para localizar os operandos usados e, em seguida, faça asserções subsequentes sobre eles. Escrever um lambda para expressar o match é bastante fácil.
O argumento para sua função, quando usado em conjunto com argThat, será passado para o ArgumentMatcher como um objeto fortemente tipado, então é possível fazer qualquer coisa com ele.
Exemplos:
// verify a list only had strings of a certain length added to it
// note - this will only compile under Java 8
verify(list, times(2)).add(argThat(string -> string.length() < 5));
// Java 7 equivalent - not as neat
verify(list, times(2)).add(argThat(new ArgumentMatcher<String>(){
public boolean matches(String arg) {
return arg.length() < 5;
}
}));
// more complex Java 8 example - where you can specify complex //verification behaviour functionally
verify(target, times(1)).receiveComplexObject(argThat(obj -> obj.getSubObject().get(0).equals("expected")));
// this can also be used when defining the behaviour of a mock under //different inputs
// in this case if the input list was fewer than 3 items the mock returns null
when(mock.someMethod(argThat(list -> list.size()<3))).thenReturn(null);
Suporte de Answer personalizada do Java 8
Como a interface Answer possui apenas um método, já é possível implementá-lo em Java 8 utilizando uma expressão lambda para situações bem simples. Quanto mais você precisar usar os parâmetros da chamada do método, mais precisará converter os argumentos de InvocationOnMock.
Exemplos:
// answer by returning 12 every time
doAnswer(invocation -> 12).when(mock).doSomething();
// answer by using one of the parameters - converting into the right
// type as your go - in this case, returning the length of the second //string parameter
// as the answer. This gets long-winded quickly, with casting of //parameters.
doAnswer(invocation -> ((String)invocation.getArgument(1)).length())
.when(mock).doSomething(anyString(), anyString(), anyString());
Por conveniência, é possível escrever respostas/ações personalizadas, que usam os parâmetros para a chamada do método, como o Java 8 utiliza lambdas. Mesmo no Java 7, que é inferior, essas respostas personalizadas com base em uma interface digitada podem reduzir o clichê.
Em particular, essa abordagem facilitará o teste de funções que usam callbacks. Os métodos AdditionalAnswers.answer(Answer1)} e AdditionalAnswers.answerVoid(VoidAnswer1) podem ser usados para criar a resposta.
Eles contam com as interfaces de respostas relacionadas em org.mockito.stubbing que suportam respostas de até 5 parâmetros.
// Example interface to be mocked has a function like:
void execute(String operand, Callback callback);
// the example callback has a function and the class under test
// will depend on the callback being invoked
void receive(String item);
// Java 8 - style 1
doAnswer(AdditionalAnswers.<String,Callback>answerVoid((operand, callback) -> callback.receive("dummy")))
.when(mock).execute(anyString(), any(Callback.class));
// Java 8 - style 2 - assuming static import of AdditionalAnswers
doAnswer(answerVoid((String operand, Callback callback) -> callback.receive("dummy")))
.when(mock).execute(anyString(), any(Callback.class));
// Java 8 - style 3 - where mocking function to is a static member of test //class
private static void dummyCallbackImpl(String operation, Callback callback) {
callback.receive("dummy");
}
doAnswer(answerVoid(TestClass::dummyCallbackImpl))
.when(mock).execute(anyString(), any(Callback.class))
// Java 7
doAnswer(answerVoid(new VoidAnswer2<String, Callback>() {
public void answer(String operation, Callback callback) {
callback.receive("dummy");
}})).when(mock).execute(anyString(), any(Callback.class));
// returning a value is possible with the answer() function
// and the non-void version of the functional interfaces
// so if the mock interface had a method like
boolean isSameString(String input1, String input2);
// this could be mocked
// Java 8
doAnswer(AdditionalAnswers.<Boolean,String,String>answer((input1, input2) -> input1.equals(input2)))
.when(mock).execute(anyString(), anyString());
// Java 7
doAnswer(answer(new Answer2<String, String, String>() {
public String answer(String input1, String input2) {
return input1 + input2;
}})).when(mock).execute(anyString(), anyString());
Até logo!
A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.