Java - Introdução ao Mockito com JUnit

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!

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

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.