Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito

Prefacio

En la programación de computadoras, la prueba unitaria es un método de prueba de software mediante el cual puede probar si cada función unitaria del código fuente es adecuada para su uso. Escribir pruebas unitarias para código tiene muchos beneficios, incluida la detección temprana de errores de código, facilitar cambios, simplificar la integración, facilitar la refactorización del código y muchas otras características. Los amigos que usan el lenguaje Java deberían haber usado o escuchado que Junit se usa para pruebas unitarias, entonces, ¿por qué necesitamos un marco de prueba Mockito? Imagina un escenario común como este. Cuando la clase actual que se va a probar depende de otros objetos de clase, si usas Junit para las pruebas unitarias, debemos crear manualmente estos objetos dependientes. Esta es en realidad una tarea engorrosa. En este punto, puedes Utilice el marco de prueba de Mockito para simular las clases dependientes Estos objetos simulados actúan como objetos virtuales o clones de objetos reales en la prueba, y Mockito también proporciona una verificación conveniente del comportamiento de la prueba. Esto nos permite prestar más atención a la lógica de la clase de prueba actual, en lugar de a los objetos de los que depende.

1

Método de generación de objetos simulados

Para usar Mockito, primero debemos introducir la dependencia del marco de prueba de Mockito en nuestro proyecto. Las siguientes dependencias se pueden introducir en el proyecto basado en Maven:


<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.3.3</version>
    <scope>test</scope>
</dependency>

Si es un proyecto creado en Gradle, se introducen las siguientes dependencias:

testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'

Por lo general, hay dos formas comunes de crear objetos Mock usando Mockito.

1.1

Utilice el método Mockito.mock (clazz)

Crea un objeto Mock a través del método estático mock de la clase Mockito. Por ejemplo, lo siguiente crea un objeto Mock de tipo List:


List<String> mockList = Mockito.mock(ArrayList.class);

Dado que el método simulado es un método estático, generalmente se escribe como un método de importación estático, es decir, ListmockList = mock (ArrayList.class).

1.2

Utilice el método de anotación @Mock

La segunda forma es usar el método de anotación @Mock para crear objetos Mock. De esta manera, debe prestar atención al uso de MockitoAnnotations.initMocks (esto) o agregar @ExtendWith (MockitoExtension.class) a la clase de prueba unitaria antes de ejecutar la Método de prueba. Anotación, el siguiente código crea un objeto Mock de tipo Lista (PS: @BeforeEach es una anotación de Junit 5, y su función es similar a la anotación @Before de Junit 4.):


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {

  @Mock
  private List<String> mockList;

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

2

Prueba de confirmación

El método estático Mockito.verify se proporciona en el marco de pruebas de Mockito para que podamos realizar pruebas de verificación de manera conveniente, como verificación de llamada de método, verificación de tiempos de llamada de método, verificación de secuencia de llamada de método, etc. Echemos un vistazo al código específico.

2.1

Método de verificación llamada única

Si se llama al método de verificación una sola vez, puede agregar directamente el método de verificación después del método de verificación. La función del siguiente código es verificar que el método de tamaño del objeto mockList se llame una vez.


/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_SimpleInvocationOnMock() {
    mockList.size();
    verify(mockList).size();
  }
}

2.2

El método de verificación llama el número especificado de veces

Además de verificar una sola llamada, a veces necesitamos verificar que algunos métodos se llaman varias veces o un número específico de veces, luego puede usar el método verify + times para verificar el número especificado de llamadas al método, y también puede combinar el método atLeast + atMost para proporcionar el rango de la cantidad de llamadas y el método como nunca para verificar que no se llama.


/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_NumberOfInteractionsWithMock() {
    mockList.size();
    mockList.size();

    verify(mockList, times(2)).size();
    verify(mockList, atLeast(1)).size();
    verify(mockList, atMost(10)).size();
  }
}

2.3

Secuencia de llamada al método de verificación

Al mismo tiempo, puede utilizar el método inOrder para verificar la secuencia de llamada de los métodos. El siguiente ejemplo verifica la secuencia de llamada de los métodos size, add y clear del objeto mockList.


/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_OrderedInvocationsOnMock() {
    mockList.size();
    mockList.add("add a parameter");
    mockList.clear();

    InOrder inOrder = inOrder(mockList);

    inOrder.verify(mockList).size();
    inOrder.verify(mockList).add("add a parameter");
    inOrder.verify(mockList).clear();
  }
}

Lo anterior es solo una lista de algunas pruebas de verificación simples. También hay tiempos de espera de llamadas de métodos de prueba de verificación y se pueden explorar y aprender más pruebas de verificación a través de documentos oficiales relevantes.

3

El método de verificación es anormal

Para las pruebas de excepción, necesitamos usar algunas definiciones de comportamiento de llamadas proporcionadas por el marco Mockito. Mockito proporciona when (...). ThenXXX (...) para permitirnos definir comportamientos de llamadas de métodos. El siguiente código define cuándo el método get de Se llama a mockMap independientemente de si se pasa. Cualquier entrada de parámetro arrojará una excepción NullPointerException de puntero nulo y luego usará Assertions.assertThrows para verificar el resultado de la llamada.


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {

  @Mock
  public Map<String, Integer> mockMap;

  @Test
  public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    when(mockMap.get(anyString())).thenThrow(NullPointerException.class);

    assertThrows(NullPointerException.class, () -> mockMap.get("mghio"));
  }
}

Al mismo tiempo, cuando (...). ThenXXX (...) no solo puede definir la llamada al método para lanzar una excepción, sino también definir el resultado devuelto después de que se llama al método, como when (mockMap.get (" mghio ")). thenReturn (21); Define que cuando llamamos al método get de mockMap y pasamos el parámetro mghio, el resultado devuelto es 21. Una cosa a tener en cuenta aquí es que el uso de la prueba de objeto simulado definido de esta manera no afecta realmente el estado interno del objeto, como se muestra en la siguiente figura:

Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito

Aunque hemos llamado al método add en el objeto mockList, de hecho la colección mockList no agrega mghio. Si necesita afectar el objeto simulado en este momento, debe usar el método espía para generar el objeto simulado.


public class MockitoTest {

  private List<String> mockList = spy(ArrayList.class);

  @Test
  public void add_spyMockList_thenAffect() {
    mockList.add("mghio");

    assertEquals(0, mockList.size());
  }
}

Después del punto de interrupción, se puede encontrar que cuando el objeto simulado creado por el método espía llama al método add, mghio se agrega correctamente a la colección mockList.

4


Integración con Spring Framework

El marco Mockito proporciona anotaciones @MockBean para inyectar objetos simulados en el contenedor Spring. Este objeto reemplazará cualquier bean existente del mismo tipo en el contenedor. Esta anotación es muy útil en escenarios de prueba que necesitan simular beans específicos (como servicios externos ) .Funciona. Si está utilizando Spring Boot 2.0+ y hay beans del mismo tipo en el contenedor actual, debe establecer spring.main.allow-bean-definition-overriding en verdadero (el valor predeterminado es falso) para permitir la anulación de la definición de bean. Lo siguiente supone que se quiere probar la consulta de información del usuario a través del código de usuario, existe un UserRepository de la capa de operación de la base de datos, es decir, el objeto que queremos simular a continuación, definido de la siguiente manera:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Repository
public interface UserRepository {

  User findUserById(Long id);

}

También existe la clase UserService relacionada con las operaciones del usuario, que se define de la siguiente manera:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Service
public class UserService {

  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User findUserById(Long id) {
    return userRepository.findUserById(id);
  }
}

Use @MockBean para anotar la propiedad UserRepository en la clase de prueba para indicar que este tipo de bean usa objetos simulados, y use la anotación @Autowired para indicar que la propiedad UserService usa objetos en el contenedor Spring, y luego use @SpringBootTest para habilitar el Ambiente primaveral.


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@SpringBootTest
public class UserServiceUnitTest {

  @Autowired
  private UserService userService;

  @MockBean
  private UserRepository userRepository;

  @Test
  public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
    User expectedUser = new User(9527L, "mghio", "18288888880");
    when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
    User actualUser = userService.findUserById(9527L);
    assertEquals(expectedUser, actualUser);
  }
}

5

Cómo funciona el marco Mockito

A través de la introducción anterior, podemos encontrar que Mockito es muy fácil de usar y puede verificar fácilmente el comportamiento de algunos métodos. Creo que ya lo ha visto. Los pasos a seguir son crear primero un objeto que necesita simulacro. El objeto es como sigue:

public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

Luego usamos directamente el método Mockito.mock y when (...). ThenReturn (...) para generar el objeto simulado y especificar el comportamiento cuando se llama al método. El código es el siguiente:


@Test
public void test_foo() {
  String expectedResult = "Mocked mghio";
  when(mockTarget.foo("mghio")).thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

Observe cuidadosamente lo anterior when (mockTarget.foo ("mghio")). ThenReturn (esperabaResult) línea de código. También me resulta extraño usarlo por primera vez. El parámetro de entrada del método when resulta ser el retorno value mockTarget.foo ("mghio"), creo que el código correcto debería ser when (mockTarget) .foo ("mghio"), pero esta forma de escribir en realidad no se puede compilar. Dado que el valor de retorno del método Target.foo es de tipo String, ¿se pueden utilizar los siguientes métodos?


Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");

El resultado es que la compilación pasa, pero se informa un error en tiempo de ejecución:

Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito

Se puede ver en el mensaje de error que el método when requiere un parámetro de llamada al método. De hecho, solo necesita la llamada al método more objeto antes del método when. Echemos un vistazo al siguiente código de prueba:


@Test
public void test_mockitoWhenMethod() {
  String expectedResult = "Mocked mghio";
  mockTarget.foo("mghio");
  when("Hello, I am mghio").thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

El código anterior se puede probar normalmente y los resultados son los siguientes:

Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito

¿Por qué la prueba puede pasar normalmente? Esto se debe a que cuando llamamos al método foo del objeto simulado, Mockito interceptará la llamada al método y guardará la información detallada de la llamada al método en el contexto del objeto simulado. Cuando se llama al método Mockito.when, en realidad se obtiene desde el contexto Se llama al último método registrado, y luego el parámetro de thenReturn se guarda como su valor de retorno. Luego, cuando volvamos a llamar al método del objeto simulado, el comportamiento del método previamente registrado se reproducirá nuevamente, lo que activa el interceptor para volver a llamar y devolver el valor de retorno que especificamos en el método thenReturn. El siguiente es el código fuente del método Mockito.when:

Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito

Este método utiliza directamente el método MockitoCore. Cuando el método, continúe con el seguimiento, el código fuente del método es el siguiente:

Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito

Una mirada más cercana revela que el parámetro methodCall no se usa en el código fuente, pero el objeto OngoingStubbing se obtiene de la instancia de MockingProgress. Este objeto OngoingStubbing es el objeto de contexto mencionado anteriormente. Personalmente, creo que para proporcionar una API concisa y fácil de usar, Mockito crea el "fantasma" de las llamadas al método when. En resumen, el marco Mockito funciona almacenando y recuperando los detalles de la llamada al método en el contexto mediante la intercepción del método. .

6


Cómo implementar un marco micro Mock ***

Después de conocer el principio operativo de Mockito, echemos un vistazo a cómo implementar un marco simulado con funciones similares por ti mismo. Creo que ya sabes cómo interceptar el método. De hecho, esto es AOP, pero leyendo su código fuente, Encontré Mockito De hecho, no usamos el conocido método Spring AOP o AspectJ para interceptar, sino que generamos e inicializamos objetos simulados a través de la biblioteca de mejora de tiempo de ejecución Byte Buddy y la biblioteca de herramientas de reflexión Objenesis. Ahora, a través del análisis anterior y la lectura del código fuente, se puede definir una versión simple del marco simulado, y el marco simulado personalizado se llama imock. Una cosa a tener en cuenta aquí es que Mockito tiene la ventaja de que no necesita inicializarse y se puede usar inmediatamente a través de los métodos estáticos que proporciona. Aquí también usamos el método estático del mismo nombre, a través del código fuente de Mockito:
Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito
Es fácil ver que la clase Mockito finalmente se delega a MockitoCore para implementar las funciones, y solo proporciona algunos métodos estáticos que son fáciles de usar para usuarios. Aquí también define un objeto proxy IMockCore. Esta clase necesita un método simulado para crear un objeto simulado y un método thenReturn para establecer el valor de retorno del método. Al mismo tiempo, esta clase contiene una lista de detalles de llamadas al método Colección InvocationDetail. Esta clase se usa para registrar la información detallada de la invocación del método, luego el método when solo devuelve el último InvocationDetail en la lista. La lista aquí puede usar directamente el ArrayList comúnmente usado en Java. La lista de colección ArrayList aquí implementa el Función Stubbing en Mockito. Según los tres elementos del método, el nombre del método, los parámetros del método y el valor de retorno del método, es fácil escribir el código de la clase InvocationDetail. Para distinguir el caso en el que el método tiene el mismo nombre en diferentes clases, también es necesario agregar el campo de nombre completo de la clase y anular la clase Los métodos equals y hashCode (para determinar si se debe llamar a la lista de conjuntos de métodos debe juzgarse de acuerdo con este método), el código es el siguiente:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class InvocationDetail<T> {

  private String attachedClassName;

  private String methodName;

  private Object[] arguments;

  private T result;

  public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
    this.attachedClassName = attachedClassName;
    this.methodName = methodName;
    this.arguments = arguments;
  }

  public void thenReturn(T t) {
    this.result = t;
  }

  public T getResult() {
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    InvocationDetail<?> behaviour = (InvocationDetail<?>) o;
    return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
        Objects.equals(methodName, behaviour.methodName) &&
        Arrays.equals(arguments, behaviour.arguments);
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(attachedClassName, methodName);
    result = 31 * result + Arrays.hashCode(arguments);
    return result;
  }
}

El siguiente paso es cómo crear nuestro objeto simulado. Aquí también usamos las bibliotecas Byte Buddy y Objenesis para crear objetos simulados. La interfaz IMockCreator se define de la siguiente manera:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public interface IMockCreator {

  <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);

}

La clase de implementación ByteBuddyIMockCreator usa la biblioteca Byte Buddy para generar dinámicamente código de objeto simulado en tiempo de ejecución y luego usa Objenesis para instanciar el objeto. el código se muestra a continuación:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class ByteBuddyIMockCreator implements IMockCreator {

  private final ObjenesisStd objenesisStd = new ObjenesisStd();

  @Override
  public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
    ByteBuddy byteBuddy = new ByteBuddy();

    Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
        .method(ElementMatchers.any())
        .intercept(MethodDelegation.to(InterceptorDelegate.class))
        .defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
        .implement(IMockIntercepable.class)
        .intercept(FieldAccessor.ofBeanProperty())
        .make()
        .load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();

    T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
    ((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));

    return mockTargetInstance;
  }
}

Según el análisis anterior, podemos escribir fácilmente el código de la clase IMockCore que crea objetos simulados de la siguiente manera:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockCore {

  private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);

  private final IMockCreator mockCreator = new ByteBuddyIMockCreator();

  public <T> T mock(Class<T> mockTargetClass) {
    T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
    return result;
  }

  @SuppressWarnings("unchecked")
  public <T> InvocationDetail<T> when(T methodCall) {
    int currentSize = invocationDetailList.size();
    return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1);
  }
}

La clase IMock proporcionada a los usuarios es solo una simple llamada a IMockCore. El código es el siguiente:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMock {

  private static final IMockCore IMOCK_CORE = new IMockCore();

  public static <T> T mock(Class<T> clazz) {
    return IMOCK_CORE.mock(clazz);
  }

  public static <T> InvocationDetail when(T methodCall) {
    return IMOCK_CORE.when(methodCall);
  }
}

A través de los pasos anteriores, hemos implementado un marco simulado en miniatura. Probémoslo con un ejemplo práctico. Primero, cree un objeto Target:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

Luego escriba su clase de prueba correspondiente clase IMockTest de la siguiente manera:


/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockTest {

  @Test
  public void test_foo_method() {
    String exceptedResult = "Mocked mghio";
    Target mockTarget = IMock.mock(Target.class);

    IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);

    String actualResult = mockTarget.foo("mghio");

    assertEquals(exceptedResult, actualResult);
  }

}

La prueba anterior puede ejecutarse normalmente, logrando el mismo efecto que el marco de prueba Mockito. Los resultados de ejecución son los siguientes:

Este artículo le permite comenzar rápidamente con el marco de prueba de unidades de Mockito
Lo anterior es solo una lista del código fuente de algunas clases clave. Todos los códigos para personalizar el marco de trabajo de IMock se han subido al repositorio de Github imock, y los amigos interesados ​​pueden consultarlo.


7

para resumir

Este artículo solo presenta algunos métodos para usar Mockito. Esta es solo la función más básica proporcionada por el marco. Para un uso más avanzado, puede ir al sitio web oficial para leer documentos relacionados, y luego introducir el cuándo (...). thenReturn (...) Definir la implementación del método de comportamiento e implementar un imock simple de la misma función de acuerdo con la idea del código fuente. Aunque las pruebas unitarias tienen muchas ventajas, las pruebas unitarias no se pueden hacer a ciegas. En la mayoría de los casos, solo necesitamos hacer un buen trabajo de pruebas unitarias para los módulos comerciales centrales que son más complejos y difíciles de entender en el proyecto y los módulos. que dependen públicamente del proyecto. Eso es todo.

Supongo que te gusta

Origin blog.51cto.com/15075507/2607590
Recomendado
Clasificación