Java - Introducción a Mockito con JUnit

Java - Introducción a Mockito con JUnit

Mockito es un framework open-source que permite crear fácilmente double tests (mocks). Um double test es un término genérico para cualquier caso en el que reemplazamos un objeto de producción con fines de prueba. En Mockito, generalmente trabajamos con los siguientes tipos de double tests:

  • Stubs – Estos son objetos que tienen valores de retorno predefinidos para las ejecuciones de métodos realizadas durante la prueba;
  • Spies – Son objetos similares a los stubs, pero también registran estadísticas de cómo fueron ejecutados;
  • Mocks – Estos son objetos que tienen valores de retorno para las ejecuciones de métodos realizadas durante las pruebas y han registrado expectativas de esas ejecuciones. Los simulacros pueden generar una excepción si reciben una llamada que no esperaban y se verifican para garantizar que recibieron todas las llamadas que esperaban.

Podemos burlarnos de interfaces y clases en la clase de prueba. Mockito también ayuda a producir un código repetitivo mínimo si usamos anotaciones de Mockito. Una vez creada, una simulación recordará todas las interacciones. Luego podemos verificar selectivamente cualquier interacción que nos interese.

Mockito Setup

Maven

Para agregar Mockito al proyecto, podemos agregar la última versión por cualquier medio, por ejemplo, Maven, Gradle o un archivo 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 con JUnit

Para procesar anotaciones de Mockito con JUnit 5, necesitamos usar el MockitoExtension de la siguiente forma:

@ExtendWith(MockitoExtension.class)

public class ApplicationTest {

//code

}

Para JUnit 4 heredado, podemos usar las clases MockitoJUnitRunner o MockitoRule.

@RunWith(MockitoJUnitRunner.class)

public class ApplicationTest {

//code

}

public class ApplicationTest {

@Rule public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

//code

}

El stubbing estricto garantiza pruebas limpias, reduce la duplicación de códigos de prueba y mejora la capacidad de depuración. La prueba falla temprano cuando el código bajo prueba invoca un método de código auxiliar con diferentes argumentos o cuando hay códigos auxiliares no utilizados.

Alternativamente, podemos inicializar Mockito mediante programación usando el método openMocks() en algún lugar de la clase base o en un corredor de prueba. Este método inicializa campos anotados con anotaciones. Mockito @Mock, @Spy, @Captor, @InjectMocks.

El método initMocks() usado anteriormente, agora está obsoleto.

public class ApplicationTest {

MockitoAnnotations.openMocks(this);

}

Mockito Annotations

Antes de presionar el teclado para escribir pruebas unitarias, repasemos rápidamente las notas útiles de Mockito:

  • @Mock se utiliza para la creación de simulaciones. Esto hace que la clase de prueba sea más legible.
  • @Spy se utiliza para crear una instancia espía. Podemos usarlo en lugar del método. spy(Object).
  • @InjectMocks se utiliza para crear una instancia del objeto probado automáticamente e inyectar todas las dependencias anotadas en él @Mock o @Spy (si aplica). Vale la pena conocer la diferencia entre anotaciones. @Mock y @InitMocks.
  • @Captor se utiliza para crear un captor de argumentos.

public class ApplicationTest {

@Mock

Depedency mock;

@InjectMocks

Service codeUnderTest;

}

¡Vamos a verificar algunos comportamientos!

Los siguientes ejemplos simulan una lista porque la mayoría de las personas están familiarizadas con la interfaz (como los métodos add(), get(), clear()). En realidad, no hagas mock de la clase List. En vez de eso, usa una instancia 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();

¿Qué tal algún 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 el número exacto de invocaciones / 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 con excepciones

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:

mockedList.clear();

Verificación en orden

// 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

Asegurándose de que las interacciones nunca ocurrirán en la simulación

//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 invocaciones redundantes

//using mocks

mockedList.add("one");

mockedList.add("two");

verify(mockedList).add("one");

//following verification will fail

verifyNoMoreInteractions(mockedList);




Stubbing de llamadas 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"));

Versión alternativa y más corta de stubbing consecutivo

when(mock.someMethod("some arg"))

.thenReturn("one", "two", "three");

Aviso: si en vez de encadenar llamadas .thenReturn(), con varios stubbing cuando se utilizan los mismos comparadores o argumentos, cada trozo reemplazará al 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 con callbacks

Permite tropezar con una interfaz de respuesta genérica. Todavía hay otra característica controvertida que no se incluyó originalmente en Mockito. Recomendamos hacer stub con thenReturn() o thenThrow(), que debe ser suficiente para testar/test-drive cualquier código limpio y simple. Sin embargo, si necesitas hacer un stub con la interfaz genérica Answer, aquí está un ejemplo:

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(): familia de métodos

Hacer stub de métodos void requiere un abordaje diferente de when(Object) porque al compilador no le gustan métodos void dentro de corchetes.

Usa doThrow() cuando quieras interrumpir un método void con una excepción:

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:

mockedList.clear();

Puedes usar doThrow(), doAnswer(), doNothing(), doReturn() y doCallRealMethod() en lugar de la correspondiente convocatoria con when(), para cualquier método. Es necesario que:

  • stub métodos void;
  • stub métodos en objetos spy;
  • stub el mismo método más de una vez para cambiar el comportamiento de un mock a media prueba.

Mas você pode preferir usar esses métodos no lugar da alternativa com when(), para todas as suas chamadas de stub.

Spying objetos reales

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");


Consejo importante sobre espiar objetos reales:

A veces es imposible o poco práctico utilizar when(Object) para spies stub. Por lo tanto, cuando utilice espías, considera doReturn|Answer|Throw() y la familia de métodos para stubbing. Ejemplo:

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 otras assertions

ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);

verify(mock).doSomething(argument.capture());

assertEquals("John", argument.getValue().getName());

Mocks reales parciales

//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();

Redefiniendo mocks

Los usuarios inteligentes de Mockito dificilmente usan este recurso porque saben que puede ser señal de pruebas desagradables. Normalmente, no necesitas redefinir tus mocks, sino que basta crear nuevos mocks para cada método de prueba.

En vez de reset(), considera escribir métodos de prueba simple, pequeños y enfocados en pruebas largas y muy específicas.

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 serializables

Los Mocks pueden volverse serializables. Con esta característica, puedes usar un simulacro en un lugar que requiere que las dependencias sean serializables.

Observación: Eso raramente debe usarse en pruebas de unidad.

List serializableMock = mock(List.class, withSettings().serializable());

La simulación se puede serializar asumiendo que la clase cumple con todos los requisitos de serialización normales.

List<Object> list = new ArrayList<Object>();

List<Object> spy = mock(ArrayList.class, withSettings()

.spiedInstance(list)

.defaultAnswer(CALLS_REAL_METHODS)

.serializable());

Instanciado automático de @Spies, @InjectMocks e inyección de constructor

Mockito ahora intentará instanciar @Spy y lo hará con los campos @InjectMocks usando inyección de constructor, de setter o de campo.

Para aprovechar este recurso, necesitas usar MockitoAnnotations.openMocks(Object), MockitoJUnitRunner o MockitoRule.

//instead:

@Spy BeerDrinker drinker = new BeerDrinker();

//you can write:

@Spy BeerDrinker drinker;

//same applies to @InjectMocks annotation:

@InjectMocks LocalPub;

Verificación ignorando stubs

Mockito ahora permitirá ignorar el stubbing para fines de verificación. A veces, es útil al asociarse a VerifyNoMoreInteractions() o a la verificación inOrder(). Ayuda a evitar comprobaciones redundantes de llamadas fragmentadas; normalmente no estamos interesados ​​en comprobar fragmentos.

Observación: ignoreStubs() puede provocar un uso excesivo de verifyNoMoreInteractions(ignoreStubs(...)). Recuerda que Mockito no recomienda bombardear todas las pruebas con VerifyNoMoreInteractions() por los motivos descritos en el javadoc para VerifyNoMoreInteractions(Object...).

Algunos ejemplos:

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();

Verificación de estilo BDD

Habilita la verificación del estilo de Desarrollo Basado en el Comportamiento (BDD), iniciando a verificación con la palabra-clave BDD then.

given(dog.bark()).willReturn(2);

// when

...

then(person).should(times(2)).ride(bike);

Soporte a Java 8 Lambda Matcher

Puede utilizar expresiones lambda de Java 8 con ArgumentMatcher para reducir la dependencia de ArgumentCaptor. Si necesita verificar si la entrada a una llamada de función en una simulación fue correcta, usa el ArgumentCaptor para encontrar los operandos utilizados y luego hacer afirmaciones posteriores sobre ellos. Escribir una lambda para expresar la match es bastante fácil.

El argumento para tu función, cuando se usa en conjunto con argThat, será pasado al ArgumentMatcher como un objeto fuertemente tipado, por lo que es posible hacer cualquier cosa com él.

Ejemplos:

// 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);


Soporte de Answer personalizado de Java 8

Como la interfaz Answer tiene un solo método, ahora es posible implementarla en Java 8 usando una expresión lambda para situaciones muy simples. Cuanto más necesite utilizar los parámetros de llamada al método, más necesitará convertir los argumentos de la llamada al método InvocationOnMock.

Ejemplos:

// 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());


Para mayor comodidad, puedes escribir respuestas/acciones personalizadas que utilizan los parámetros para la llamada al método, como Java 8 usa lambdas. Incluso en Java 7, que es inferior, estas respuestas personalizadas basadas en una interfaz escrita pueden reducir el texto repetitivo.

En particular, este enfoque facilitará la prueba de funciones que utilizan callbacks. Los métodos AdditionalAnswers.answer(Answer1)} y AdditionalAnswers.answerVoid(VoidAnswer1) pueden usarse ​​para crear la respuesta.

Se basan en las interfaces de respuesta relacionadas en org.mockito.stubbing que soportan respuestas de hasta 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());


Eso es todo. ¡Hasta luego!

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.