¿Cómo puede Mockito capturar los argumentos pasados ​​a los métodos de un objeto simulado inyectado?

5 minutos de lectura

Avatar de usuario de Steve Perkins
steveperkins

Estoy tratando de probar una clase de servicio, que internamente utiliza un objeto de conexión Spring AMQP. Spring inyecta este objeto de conexión. Sin embargo, no quiero que mi prueba unitaria se comunique realmente con el agente de AMQP, por lo que estoy usando Mockito para inyectar una simulación del objeto de conexión.

/** 
 * The real service class being tested.  Has an injected dependency. 
 */ 
public class UserService {

   @Autowired
   private AmqpTemplate amqpTemplate;

   public final String doSomething(final String inputString) {
      final String requestId = UUID.randomUUID().toString();
      final Message message = ...;
      amqpTemplate.send(requestId, message);
      return requestId;
   }
}

/** 
 * Unit test 
 */
public class UserServiceTest {

   /** This is the class whose real code I want to test */
   @InjectMocks
   private UserService userService;

   /** This is a dependency of the real class, that I wish to override with a mock */
   @Mock
   private AmqpTemplate amqpTemplateMock;

   @Before
   public void initMocks() {
      MockitoAnnotations.initMocks(this);
   }

   @Test
   public void testDoSomething() {
      doNothing().when(amqpTemplateMock).send(anyString(), any(Message.class));

      // Call the real service class method, which internally will make 
      // use of the mock (I've verified that this works right).
      userService.doSomething(...);

      // Okay, now I need to verify that UUID string returned by 
      // "userService.doSomething(...) matches the argument that method 
      // internally passed to "amqpTemplateMock.send(...)".  Up here 
      // at the unit test level, how can I capture the arguments passed 
      // to that inject mock for comparison?
      //
      // Since the value being compared is a UUID string created 
      // internally within "userService", I cannot just verify against 
      // a fixed expected value.  The UUID will by definition always be
      // unique.
   }
}

Se espera que los comentarios en este ejemplo de código expongan la pregunta con claridad. Cuando Mockito inyecta una dependencia simulada en una clase real y las pruebas unitarias en la clase real provocan que se realicen llamadas al simulacro, ¿cómo puede recuperar más tarde los argumentos exactos que se pasaron al simulacro inyectado?

Utilice uno o más ArgumentCaptors.

No está claro cuáles son sus tipos aquí, pero de todos modos. Supongamos que tiene un simulacro que tiene un método doSomething() tomar una Foo como argumento, entonces haces esto:

final ArgumentCaptor<Foo> captor = ArgumentCaptor.forClass(Foo.class);

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

final Foo argument = captor.getValue();

// Test the argument

Además, parece que su método devuelve vacío y no quiere que haga nada. Solo escribe esto:

doNothing().when(theMock).doSomething(any());

  • Gracias por el consejo sobre doAnswer() contra doNothing(), he limpiado el ejemplo de código anterior en consecuencia. Sin embargo, no estoy seguro de si me estoy perdiendo algo o si su respuesta aún está un nivel por debajo de mi problema. Cuando empiezo llamando verify() en mi objeto simulado ANTES de llamar a la clase real que usa el simulacro internamente, luego aparece el mensaje de excepción: “En realidad, no hubo interacciones con este simulacro”. La ejecución falla en el verify() línea… y ni siquiera llega al código real que estoy tratando de probar.

    –Steve Perkins

    20 de marzo de 2015 a las 15:32


  • En su ejemplo, ¿está tratando de pasar un argumento a su objeto simulado directamente desde la prueba unitaria? No estoy… Estoy tratando de capturar un argumento que pasa por una capa intermedia entre la prueba unitaria y el objeto simulado. El simulacro está siendo inyectado en este intermediario.

    –Steve Perkins

    20 de marzo de 2015 a las 15:33

  • ¡Ah! El problema que no estaba claro en su fragmento es que el verify() llame al simulacro, así como a la getValue() llama al captor, ambos vienen DESPUÉS de la invocación del método en el objeto real que se está probando. estaba llamando por error verify() antes de la invocación del método de objeto real.

    –Steve Perkins

    20 de marzo de 2015 a las 15:54

  • @StevePerkins, lo siento, no estaba allí, pero veo que lo has descubierto;)

    – fge

    20 de marzo de 2015 a las 16:51

  • También hay un atajo para crear captor en el nivel de clase: @Captor private ArgumentCaptor fooCaptor;

    – ozeray

    18 de mayo de 2018 a las 7:35


Avatar de usuario de Kirby
Kirby

puedes enganchar doAnswer() al trozo de la send() método en amqpTemplateMock y luego capturar los argumentos de invocación de AmqpTemplate.send().

Haz la primera línea de tu testDoSomething() ser esto

    Mockito.doAnswer(new Answer<Void>() {
          @Override
          public Void answer(final InvocationOnMock invocation) {
            final Object[] args = invocation.getArguments();
            System.out.println("UUID=" + args[0]);  // do your assertions here
            return null;
          }
    }).when(amqpTemplateMock).send(Matchers.anyString(), Matchers.anyObject());

poniéndolo todo junto, la prueba se convierte en

import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class UserServiceTest {

  /** This is the class whose real code I want to test */
  @InjectMocks
  private UserService userService;

  /** This is a dependency of the real class, that I wish to override with a mock */
  @Mock
  private AmqpTemplate amqpTemplateMock;

  @Before
  public void initMocks() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void testDoSomething() throws Exception {
    Mockito.doAnswer(new Answer<Void>() {
      @Override
      public Void answer(final InvocationOnMock invocation) {
        final Object[] args = invocation.getArguments();
        System.out.println("UUID=" + args[0]);  // do your assertions here
        return null;
      }
    }).when(amqpTemplateMock).send(Matchers.anyString(), Matchers.anyObject());
    userService.doSomething(Long.toString(System.currentTimeMillis()));
  }
}

Esto da salida

UUID=8e276a73-12fa-4a7e-a7cc-488d1ce0291f

Encontré esto al leer esta publicación, Cómo hacer métodos simulados para anular con mockito

  • Buena respuesta. Además, las versiones recientes de Mockito (estoy usando 2.24.5) incluyen interfaces Answer1 a Answer5 para facilitar el uso de invocaciones con hasta cinco argumentos, por ejemplo, para implementar Answer1<String, Integer> tienes que proporcionar un public String answer(final Integer argument0) throws Throwable método.

    – SJuan76

    28 de julio de 2021 a las 11:14

¿Ha sido útil esta solución?