Mockito when

De ChuWiki


Cuando hacemos test con mockito utilizamos mocks de algunas clases. Debemos configurar qué queremos que devuelvan los métodos de dicho mock cuando los llamamamos de acuerdo a lo que necesitemos en nuestro test. Para ello, se utiliza Mockito.when(). Veamos ejemplos desde los más sencillos hasta los algo más elaborados.

Tienes el código de ejemplo en SomeWhenTest.java. Utiliza una IfzDao.java que simula ser para consultas a base de datos y de ella es de la que se va a hacer el Mock y configurar qué debe devolver en su método Data findById(int). Data es otra clase de nuestro ejemplo que puedes ver en Data.java

El mock se obtiene con

IfzDao mockDao = Mockito.mock(IfzDao.class);

Mockito When Simple[editar]

La forma más simple es indicar qué debe devolver cuando se hace la llamada con un valor concreto para el parámetro

Mockito.when(mockDao.findById(1))
   .thenReturn(new Data(1,"Uno"));

Mockito.when() admite un parámetro que es la llamada al método del mock que queramos configurar, valores de los parámetros incluidos. Es decir, de mockDao.findById(1). De esta forma, estamos configurando qué queremos que devuelva mockDao durante el test si llamamos a findById() pasando un 1 como parámetro.

Mockito.when() devuelve una clase con métodos para indicar qué debe devolver cuando se haga la llamada a mockDao.findById(1). Uno de estos métodos en thenReturn() al que pasamos como parámetro qué devolver. Como IfzDao.findById() devuelve una instancia de Data, simplemente la instanciamos con los valores que queramos.

Una vez hecho esto, cada vez que llamemos a mockDato.findById(1) nos devolverá una instancia de Data. Si lo llamamos con otro valor, devolverá null

Data data = mockDao.findById(1);
// data es ahora Data(1,"Uno")

data = mockDao.findById(2);
// data es ahora null

Mockito When Any[editar]

Podemos no querer un valor concreto, sino que nos vale cualquier valor. Para ello tenemos métodos en la clase Mockito que nos permiten determinar qué valores nos valen como parámetro. Por ejemplo, si queremos que nos valga para cualquier valor entero, podemos poner

Mockito.when(mockDao.findById(Mockito.anyInt()))
   .thenReturn(new Data(1,"Uno"));

Con esto da igual qué entero pasemos de parámetros, simpre devolverá Data(1,"Uno")

Data data = mockDao.findById(1);
// data es ahora Data(1,"Uno")

data = mockDao.findById(2);
// data es ahora también Data(1,"Uno")

Mockito thenReturn[editar]

thenReturn() devuelve un objeto al que podemos seguir llamando al método thenReturn consecutivamente. Por ejemplo

Mockito.when(mockDao.findById(Mockito.anyInt()))
   .thenReturn(new Data(1,"Uno"))
   .thenReturn(new Data(2,"Dos"))
   .thenReturn(new Data(3,"Tres"));

Estos valores nos irá devolviendo consecutivamente en cada llamada que cumpla con la condición del parámetro Mockito.anyInt() o la que pongamos en su lugar. Cuando se acaben los valores a devolver, devolverá el último dato repetidamente

Data data = mockDao.findById(1);
// data es ahora Data(1,"Uno")

data = mockDao.findById(12);
// data es ahora también Data(2,"Dos")

Data data = mockDao.findById(2);
// data es ahora Data(3,"Tres")

data = mockDao.findById(21);
// data es ahora también Data(3,"Tres")
// y lo seguira siendo en las siguientes llamadas que hagamos.

Mockito when varias veces[editar]

Si queremos afinar más, podemos poner varias llamadas a Mockito.when() para el mismo mock y método. Por ejemplo

Mockito.when(mockDao.findById(1)).thenReturn(new Data(1,"Uno"));
Mockito.when(mockDao.findById(2)).thenReturn(new Data(2,"Dos"));

De esta forma, nos devolverá un resultado si llamamos al método con 1, otro si llamamos con 2 y null si llamamos con cualquier valor que no sea ni 1 ni 2.

Es importante que los valores que pongamos en la condición no se pisen, ya que prevalece el último que pongamos. Por ejemplo

Mockito.when(mockDao.findById(1)).thenReturn(new Data(1,"Uno"));
Mockito.when(mockDao.findById(Mockito.anyInt())).thenReturn(new Data(2,"Dos"));

la segunda llamada machaca la primera. Si llamamos con 1, nos devolverá Data(2,"Dos"). Es más, Mockito dará un warning indicando que la primera línea sobra, puesto que la machacamos en la segunda. Sin embargo, si funcionaría si las ponemos al revés

Mockito.when(mockDao.findById(Mockito.anyInt())).thenReturn(new Data(2,"Dos"));
Mockito.when(mockDao.findById(1)).thenReturn(new Data(1,"Uno"));

Devolverá Data(2,"dos") para todas las llamadas excepto si llamamos con un 1, que devolverá Data(1,"Uno")

Mockito argThat[editar]

Podemos afinar todavía más qué valores nos valen para el parámetro. Mockito tiene varios métodos que permiten afinar, dependendiendo del tipo de parámetro. En nuestro caso, que nuestro parámetro es un entero, podemos usar Mockito.intThat() pasando como parámetro una condición que debe cumplir el entero. Hay métodos Mockit.argThat() para parámetros tipo Object y en general métodos xxxThat donde xxx es alguno de los tipos habituales de java, como int, short, byte, etc. Hay también métodos específicos para cadenas de texto, como Mockito.matches(regex) para expresiones regulares, Mockito.startsWith() o Mockito.endsWith().

En nuestro ejemplo

Mockito.when(mockDao.findById(Mockito.intThat(value -> value<10))).thenReturn(new Data(1,"1"));

Pasamos como parámetro de Mockito.intThat una expresión Lambda que recibe el parámetro que nos pasen en la llamada (el entero) y debemos devolver un boolean indicando si es válido para esta regla o no. Si es válido, con theReturn, devolveremos Data(1,"1").

Por ejemplo, si usamos el método IfzDao.getByName(String), podemos poner una regla when con expresión regular para ver si el parámetro la cumple.

Mockito.when(mockDao.findByName(Mockito.matches("\\d+"))).thenReturn(List.of(new Data(1,"1"), new Data(2,"2")));

Si nos llaman con un String compuesto de uno o más dígitos (es lo que significa la expresión \\d+), como puede ser "123" o "2", devolveremos una lista con dos elementos Data. si nos llaman con una cadena que no cumpla la expresión regular, como "abc", devolveremos una lista vacía (lo hace mockito por defecto).

Mockito thenAnswer[editar]

Si la respuesta que queremos no es tan sencilla como algo que podamos poner como paraémtro en un thenReturn, tenemos como alternativa el método thenAnswer que nos permite elaborar un poco más el valor que queremos devolver. Por ejemplo

Mockito.when(mockDao.findById(Mockito.anyInt())).thenAnswer(invocationOnMock -> {
     Integer argument = invocationOnMock.getArgument(0);
     return new Data(argument, argument.toString());
});

En thenAnswer() ponemos una expresión Lamba. El parámetro invocationOnMock es un objeto propio de Mockito al que podemos interrogar sobre algunos aspectos de la llamada que han hecho al mock. En concreto, con getArgument() podemos obtener el parámetro con el que llaman al método findById. Si el método tuviera varios parámetros, los obtendríamos con invocationOnMock.getArgument(0), invocationOnMock.getArgument(1), etc.

Sabemos que el parámetro es entero, porque hemos puesto en la condición Mockito.anyInt(), así que lo recogemos con Integer. Y construimos el resultado que queremos devolver a medida, es decir return new Data(argument, argument.toString()).

Así que obtenmmos lo siguiente

Data data = mockDao.findById(1);
// data vale Data(1,"1")

data = mockDao.findById(2);
// data vale Data(2,"2")

Mockito thenThrow[editar]

Si queremos que una llamada provoque una excepción, en vez de thenReturn(), podemos usar thenThrow(). Siguiendo con el ejemplo anterior

Mockito.when(mockDao.findById(Mockito.intThat(value -> value<10))).thenAnswer(invocationOnMock -> {
     Integer argument = invocationOnMock.getArgument(0);
     return new Data(argument, argument.toString());
Mockito.when(mockDao.findById(Mockito.intThat(value -> value>=10))).thenThrow(new NoSuchElementException("No hay"));

Hemos afinando la regla. Si el entero que nos pasan en menor que 10, devolvemos un valor de Data, pero si es mayor o igual que 10, lanzamos una excpeción NoSuchElementException.

Método que devuelve void. Mockito doNothing[editar]

Si el método de nuestro mock devuelve void (nada), esta sintaxis no nos vale. En vez de Mockito.when(), debemos usar Mockito.doNothing(). Imagina que queremos llamar a void IfzDao.method(String). El código sería

Mockito.doNothing().when(mockDao).method(Mockito.anyString());

Llamamos a Mockito.doNothing(), encadenamos .when(mockDao), pero no ponemos ahí el método al que queremos llamar, sólo ponemos la instancia del mock. Pero podemos entonces encadenar los métodos del mock mockDao, en concreto, llamando a .method(...).

Así tal cual no tiene mucho sentido, ya que no hay necesidad de configurar qué debe hacer el mock cuando llaman a un método que no devuelve nada. De hecho, si no configuramos nada, es lo que hará mockito por defecto cuando llamemos al mock. Nada.

Si tiene sentido si nos interesa capturar de alguna manera los parámetros con los que se llama al mock para hacer algo con ello. Por ejemplo, imagina que queremos que salte una excepción si se llama con un determinado valor del parámetro

Mockito.doAnswer(invocationOnMock -> {
    String parameter = invocationOnMock.getArgument(0);
    if ("fail".equals(parameter)){
        throw new IllegalArgumentException();
    }
    return null;
}).when(mockDao).method(Mockito.anyString());

Hemos usado Mockito.doAnswer() de forma similar a como lo hicimos antes. Si el parámetro que nos pasan es "fail", lanzamos una excpeción. Si no, devolvemos null, ya que el método es void y no debe devolver nada. Luego encademanos .when(mockDao) y el .method() con cualquier String.

try {
    mockDao.method("fail");
} catch (IllegalArgumentException e){
    return;
}

La llamada con el parámetro "fail" hará saltar la excepción.

Mockito static[editar]

Imagina que queremos un mock de la clase StaticClass.java que tiene métodos estáticos, el siguiente código nos muestra cómo hacerlo.

try (MockedStatic<StaticClass> staticClassMockedStatic = Mockito.mockStatic(StaticClass.class)){
    staticClassMockedStatic.when(StaticClass::getName).thenReturn("Mockeado");
    staticClassMockedStatic.when(()->StaticClass.addOne(1)).thenReturn(33);

    Assertions.assertEquals("Mockeado", StaticClass.getName());
    Assertions.assertEquals(33, StaticClass.addOne(1));
}

Realmente no estamos haciendo un mock, sino que estamos interceptando las llamadas a la clase para devolver otra cosa. Usamos Mockito.mockStatic(StaticClass.class) para obtener una instancia de MockedStatic<StaticClass>. Lo hacemos en un try-with-resources de forma que cuando terminemos el try, dejemos de interceptar las llamadas.

Si el método estático no tiene parámetros, llamamos a .when(StaticClass::getName) para indicar el método que queremos modificar y .thenReturn("Mockeado") para decir qué queremos que devuelva. La clase original en este método devuelve "StaticClass" si miras el código de ella. Ahora va a devolver "Mockeado". A partir de aquí simplemente tenemos que usar la clase con métodos estáticos de forma normal, pero devolverá el valor que hemos indicado.

Si el método estático tiene parámetros, llamamos a .when(()->StaticClass.addOne(1)) poniendo la llamada con el valor concreto del parámetro. Y con .thenReturn(33) el valor que queramos devolver. Nuestra clase original devolvía incrementado el valor del parámetro que se le pasa. Ahora va a devolver 33 si le pasamos un 1.