Una introducción a las pruebas unitarias.

En el proceso diario de I+D, las pruebas unitarias generalmente las escribe I+D. Con la aparición de modelos grandes, la tecnología de análisis de código y la IA se pueden utilizar como desarrollo de pruebas para generar de forma inteligente casos de prueba únicos, reduciendo los costos de I+D y acortando todo el ciclo de entrega del producto.
Las pruebas unitarias a menudo se encuentran en la etapa de escritura de código. En comparación con la etapa de prueba, se pueden encontrar a bajo costo y con más problemas. Con la creciente demanda de pruebas hacia la izquierda, se presentan casos de prueba unitaria perfectos, efectivos, de alta cobertura y completos. Sigue siendo muy valioso, echemos un vistazo a qué es una prueba única.

¿Por qué escribir pruebas?

Los tiempos nos piden que escribamos pruebas.

El alcance de la responsabilidad del programador se está expandiendo un poco, y la razón clave es que el desarrollo de software se está volviendo cada vez más complejo .

Las pruebas nos permiten avanzar de manera constante en un desarrollo de software cada vez más complejo. Por un lado, a la hora de escribir nuevas funciones, las pruebas pueden verificar la corrección de nuestro código, permitiéndonos tener módulos estables uno a uno. Por otro lado, las pruebas pueden ayudarnos a seguir regresando en el proceso a largo plazo, haciendo que cada paso sea más estable.

Hay un chiste sobre las pruebas: todo programador quiere tener pruebas cuando modifica el código, pero cuando escribe código, no quiere escribir pruebas.

Escribir exámenes vale la pena

El padre de XML, Tim Bray, recientemente dijo un dicho divertido en su blog: "Codificar sin escribir pruebas es como ir al baño sin lavarse las manos... Las pruebas unitarias son una inversión esencial en el futuro del software". unidad ¿Cuáles son los beneficios de las pruebas?

  • Es la prueba más sencilla para garantizar una cobertura del código del 100%.

  • Puede reducir en gran medida el índice de tensión al conectarse.

  • Las pruebas unitarias pueden encontrar problemas más rápido (consulte la izquierda de la figura siguiente).

  • Las pruebas unitarias son las más rentables, porque cuanto más tarde se encuentra el error, mayor será el costo de solucionarlo y la dificultad aumenta exponencialmente, por lo que debemos realizar la prueba lo antes posible (consulte la derecha de la figura a continuación).

  • Los codificadores, y en general los principales ejecutores de las pruebas unitarias, son los únicos que pueden producir programas libres de errores, y nadie más puede hacerlo.

  • Ayuda a optimizar el código fuente, haciéndolo más estandarizado, proporcionando comentarios rápidos y refactorizando con confianza.

imagen

imagen

Esta imagen proviene de las estadísticas de Microsoft: los errores se encuentran en la fase de prueba de la unidad y lleva un promedio de 3,25 horas. Si se filtra a la fase de prueba del sistema, tomará 11,5 horas. Esta imagen pretende ilustrar dos problemas: el 85% de los defectos se generan en la etapa de diseño del código, y cuanto más tarde es la etapa de descubrimiento de errores, mayor es el costo y el aumento exponencial.

Aunque las pruebas unitarias tienen tales beneficios, en nuestro trabajo diario todavía hay muchos proyectos cuyas pruebas unitarias están incompletas o faltan. Las razones más comunes se resumen a continuación: la lógica del código es demasiado compleja; se necesita mucho tiempo para escribir pruebas unitarias; la tarea es pesada, el plazo es ajustado o no está escrito en absoluto.

Con base en los problemas anteriores, en comparación con las pruebas unitarias tradicionales de JUnit, hoy recomiendo un marco de prueba llamado Spock. En la actualidad, la mayoría de los servicios back-end del equipo de tecnología de logística óptima de Meituan han adoptado Spock como marco de prueba, lo que ha logrado buenos beneficios en términos de eficiencia de desarrollo, legibilidad y mantenibilidad.

Sin embargo, la información de Spock en línea es relativamente simple, incluso incluida la demostración en el sitio web oficial, que no puede resolver los problemas que enfrentan los escenarios comerciales complejos en nuestro proyecto. Después de un estudio y práctica en profundidad, este artículo compartirá algo de experiencia, con la esperanza de ayudar a todos a mejorar la eficiencia del desarrollo y las pruebas.

que es una prueba

De la autoprueba al marco de prueba automatizado

La gran popularidad de los marcos de prueba se debe al marco de prueba automatizado JUnit, escrito por Kent Beck y Erich Gamma . Kent Beck es el fundador de Extreme Programming y es muy conocido en el campo de la ingeniería de software, mientras que Erich Gamma es el autor de los famosos "Design Patterns" y Visual Studio Code, con el que mucha gente está familiarizada, también tiene su especialidad. contribuciones.

Una vez, Kent Beck y Erich Gamma volaron de Zurich a Atlanta para participar en la conferencia OOPLSA (Programación, sistemas, lenguajes y aplicaciones orientada a objetos). En el vuelo, dos personas emparejaron programación y escribieron JUnit.

Introducción al marco de pruebas

Tutorial de Junit: https://www.baeldung.com/junit

Hay dos puntos clave para que comprendamos el marco de prueba: uno es comprender la estructura de la organización de la prueba y el otro es comprender la afirmación. Dominar estos dos puntos es suficiente para afrontar la mayoría de las situaciones cotidianas.

estructura de prueba

Todo el mundo debería estar familiarizado con cómo JUnit expresa casos de prueba.

@Test
public should_work() {
  ...
}

Por ejemplo, el mismo código de inicialización se escribe repetidamente y, debido a las particularidades de la prueba, estos códigos de inicialización deben ejecutarse antes de cada prueba. Para resolver este problema, JUnit introdujo setUp para realizar el trabajo de inicialización.

@BeforeEach
void setUp() {
  ...
}
  • @TestFactory  : denota un método que es una fábrica de pruebas para pruebas dinámicas.

  • @DisplayName  : define un nombre para mostrar personalizado para una clase de prueba o un método de prueba.

  • @Nested  : indica que la clase anotada es una clase de prueba anidada y no estática.

  • @Tag  : declara etiquetas para pruebas de filtrado

  • @ExtendWith  : registra extensiones personalizadas

  • @BeforeEach:  indica que el método anotado se ejecutará antes de cada método de prueba (anteriormente  @Before ).

  • @AfterEach  : indica que el método anotado se ejecutará después de cada método de prueba (anteriormente  @After ).

  • @BeforeAll  : indica que el método anotado se ejecutará antes que todos los métodos de prueba en la clase actual (anteriormente  @BeforeClass ).

  • @AfterAll  : indica que el método anotado se ejecutará después de todos los métodos de prueba en la clase actual (anteriormente  @AfterClass ).

  • @Disable  : deshabilita una clase o método de prueba (anteriormente  @Ignore )

Afirmación

Veamos el segundo punto clave para comprender el marco de prueba: la afirmación. La estructura de prueba garantiza que los casos de prueba se puedan ejecutar como se espera, y la aserción garantiza que nuestras pruebas deben tener un objetivo, es decir, lo que queremos probar.

La afirmación, para decirlo claramente, es comparar el resultado de la ejecución con el resultado esperado. Si ejecutar una prueba ni siquiera tiene expectativas, ¿qué está probando exactamente? Entonces, podemos decir que una prueba sin afirmaciones no es una buena prueba.

Casi todos los marcos de prueba tienen su propio mecanismo de aserción incorporado, como el que se muestra a continuación.

assertEquals(2, calculator.add(1, 1));

Esta afirmaciónEquals es la afirmación más típica y casi la afirmación más utilizada. Muchos marcos de prueba en otros lenguajes también la han movido intacta. Pero esta afirmación tiene un problema grave: si no observa la API, no puede recordar cuál debería ser el valor esperado y cuál debería ser el valor real devuelto por su función. Este es un problema típico de diseño de API, que dificulta su buen uso.

Por lo tanto, ha surgido en la comunidad una gran cantidad de bibliotecas de afirmaciones de terceros, como Hamcrest, AssertJ y Truth. Entre ellos, Hamcrest es una biblioteca de aserciones de estilo de composición de funciones, que una vez estuvo integrada en JUnit 4, pero para alentar la competencia de la comunidad, JUnit 5 la eliminó nuevamente. El siguiente es un fragmento de código que usa Harmcrest.

assertThat(calculator.subtract(4, 1), is(equalTo(3)));

AssertJ es una biblioteca de estilo fluido con buena escalabilidad. También es la biblioteca que elegimos en la parte de combate anterior. El siguiente es un fragmento de código que usa AssertJ.

assertThat(frodo.getName()).startsWith("Fro")
                           .endsWith("do")
                           .isEqualToIgnoringCase("frodo");

Truth es una biblioteca de aserciones de código abierto de Google. Es muy similar a AssertJ. Es mejor compatible con los programas de Android. También puse un fragmento de código, que es exactamente el mismo que AssertJ en estilo.

assertThat(projectsByTeam())
    .valuesForKey("corelibs")
    .containsExactly("guava", "dagger", "truth", "auto", "caliper");

Las aserciones incluyen no solo el procesamiento de valores de retorno, sino también otros casos especiales. Por ejemplo, también se pueden hacer aserciones cuando se lanzan excepciones. Esta es una aserción de excepción incorporada en JUnit 5. Puede consultarla.

Assertions.assertThrows(IllegalArgumentException.class, () -> {
  Integer.parseInt("One");
});

Para situaciones específicas en las que se pueden realizar afirmaciones, puede consultar la documentación API de la biblioteca de afirmaciones que utiliza.

Finalmente, hay una aserción que no está en estas bibliotecas de aserciones y es una aserción proporcionada por el marco Mock: verificar.

Con respecto al marco simulado, hablaremos de ello más adelante, pero aquí mencionaremos brevemente que la función de verificación es verificar si se ha llamado a una función. En algunas pruebas, la función no devuelve un valor ni genera una excepción. Por ejemplo, al guardar un objeto, la única forma en que podemos juzgar si la acción de guardar se ejecuta correctamente es usar verificar para verificar si se llama a la función guardada, como se muestra a continuación.

verify(repository).save(obj);

Especificación de prueba

¿Cómo garantizar la exactitud de la prueba?

Dado que no es factible escribir pruebas para pruebas, la única solución factible es escribir pruebas tan simples que sean claras de un vistazo sin demostrar su exactitud . A partir de esto, podemos saber que una prueba compleja definitivamente no es una buena prueba.

¿Cómo debería ser una prueba sencilla? Veamos juntos un ejemplo, que es la primera prueba que realizamos en la sesión de combate real.

@Test
public void should_add_todo_item() {
  // 准备
  TodoItemRepository repository = mock(TodoItemRepository.class);
  when(repository.save(any())).then(returnsFirstArg());
  TodoItemService service = new TodoItemService(repository);
  
  // 执行  
  TodoItem item = service.addTodoItem(new TodoParameter("foo"));
  
  // 断言  
  assertThat(item.getContent()).isEqualTo("foo");
  
  // 清理(可选)
  
}

Dividí esta prueba en cuatro secciones, a saber , preparación, ejecución, afirmación y limpieza . Estas son también las cuatro etapas que tendrá una prueba general, echemos un vistazo por separado.

Preparar. Esta etapa es para algunos preparativos para las pruebas, como iniciar servicios dependientes externamente y almacenar algunos datos preestablecidos. En nuestro ejemplo, se trata de establecer el comportamiento de los componentes requeridos y luego ensamblar estos componentes.

implementar. Esta etapa es la parte central de toda la prueba y desencadena el comportamiento del objetivo bajo prueba. En términos generales, es un punto de prueba y, en la mayoría de los casos, la ejecución debe ser una llamada de función. Si está probando un sistema externo, está realizando una solicitud. En nuestro código, simplemente llama a una función.

afirmación. Las aseveraciones son nuestras expectativas, las cuales se encargan de verificar que los resultados de la ejecución sean correctos. Por ejemplo, si el sistema bajo prueba devuelve la respuesta correcta. En este ejemplo, estamos verificando que el contenido del elemento Todo es lo que agregamos.

limpiar. La limpieza es una parte posible. Si se utilizan recursos externos en la prueba, deben liberarse a tiempo en esta parte para garantizar que el entorno de prueba se restablezca a su estado original, como si nada hubiera sucedido. Por ejemplo, insertamos datos en la base de datos durante la prueba y, después de la ejecución, necesitamos eliminar los datos insertados durante la prueba. Algunos marcos de prueba ya brindan soporte para algunas situaciones comunes, como los archivos temporales que usamos antes.

Si la preparación y la limpieza son comunes entre varios casos de prueba, se pueden poner en configuración y desmontaje para hacerlo.

De estas cuatro fases las que deben existir son Ejecución y Afirmación. Piénselo también, si no lo implementa, no tiene objetivos, entonces, ¿qué más medir? Sin afirmar no hay expectativas y correr también es una pérdida de tiempo. La parte de limpieza probablemente no estaría ahí si no estuviera involucrada alguna liberación de recursos. Para algunas pruebas sencillas no se requiere ninguna preparación especial.

Un viaje (A-TRIP)

Con una comprensión básica de la estructura de la prueba, vayamos un paso más allá y veamos cómo medir si una prueba se realiza bien. Alguien resumió las características de una buena prueba en una sola afirmación: A-TRIP . En realidad, se trata de una abreviatura de cinco palabras, a saber:

  • Automático, automatización;

  • Minucioso, integral;

  • Repetible, repetible;

  • Independiente, independiente;

  • Profesional, profesional.

¿Qué significa? Expliquemos cada uno por separado.

Automático, automático. Después de la explicación de la conferencia anterior, deberías haber entendido este punto fácilmente. En comparación con las pruebas tradicionales, la mejora principal de las pruebas automatizadas radica en la automatización. Es por eso que la prueba debe tener una afirmación, porque solo cuando hay una afirmación , la máquina puede ayudarnos a juzgar si la prueba es exitosa .

Minucioso, integral. En realidad, este es un requisito de prueba y las pruebas deben usarse para cubrir varios escenarios tanto como sea posible. No importa qué tipo de prueba automatizada, su esencia son las pruebas. Hablamos antes sobre aprender de los evaluadores. El punto clave es que esto nos ayuda a escribir pruebas más completas. Hay otro ángulo que debe comprenderse de manera integral, es decir, la cobertura de la prueba . Ya hemos visto en la sesión de combate real cómo usar la herramienta de cobertura de prueba para ayudarnos a encontrar lugares en el código que no están cubiertos por la prueba.

Repetible, repetible. Requiere que las pruebas se puedan ejecutar repetidamente y que los resultados siempre sean los mismos. Esta es la premisa para garantizar que la prueba sea sencilla y fiable. Es necesario garantizar la idempotencia de las pruebas unitarias .

Las pruebas realizadas en memoria generalmente son repetibles. El principal factor que afecta la repetibilidad de una prueba son los recursos externos. Los recursos externos comunes incluyen archivos, bases de datos, middleware, servicios de terceros, etc. Si estos recursos externos se encuentran durante la prueba, tenemos que encontrar una manera de restaurar estos recursos a su apariencia original una vez finalizada la prueba. Ya has visto cómo manejar archivos en combate real y también hablaremos sobre cómo manejar bases de datos en capítulos de aplicaciones posteriores. En pocas palabras, una vez ejecutada la prueba, los datos se pueden revertir.

Hay otro ángulo para entender la repetibilidad, es decir, un lote de pruebas también debe ser repetible. Esto requiere que las pruebas no dependan unas de otras, que es otra característica de las pruebas que analizaremos a continuación.

Independiente, independiente. **** No debería haber dependencias entre prueba y prueba . ¿Qué es la dependencia? Es decir, una prueba depende de los resultados de otra ejecución de prueba. Por ejemplo, ambas pruebas dependen de la base de datos: la primera prueba escribe algunos datos en la base de datos cuando se ejecuta y la segunda prueba usa estos datos cuando se ejecuta. En otras palabras, la segunda prueba debe ejecutarse después de ejecutar la primera prueba, lo que se denomina dependencia.

La repetibilidad y la independencia están muy relacionadas. Porque solemos pensar que la repetibilidad es que las pruebas se ejecutan en un orden aleatorio y los resultados son los mismos, lo que depende de que las pruebas sean independientes. Y una vez que la prueba no es independiente y tiene dependencias, también viola la repetibilidad desde el punto de vista de una prueba única.

Profesional, profesional. Este punto falta en la mente de muchas personas: el código de prueba también es código y debe mantenerse de acuerdo con el estándar del código. Esto significa que su código de prueba también debe estar escrito claramente, como buenos nombres, escritura de funciones pequeñas, refactorización e incluso abstracción de la biblioteca básica de la prueba y el modo de la prueba.

Cómo escribir código comprobable

Si no se puede garantizar la calidad de cada pieza de material utilizado para construir un edificio, ¿te atreves a pedir al final un edificio de alta calidad?

Este es el escenario vergonzoso al que se enfrentan muchos equipos: cada módulo no ha sido verificado, solo se sabe que el sistema puede funcionar cuando está integrado. Entonces, una vez que un sistema funciona, lo mejor que se puede hacer es dejarlo en paz. Sin embargo, hay una gran cantidad de nuevos requisitos en cola.

En consecuencia, para un sistema bien comprobable, cada módulo debe poder probarse de forma independiente.

Para mejorar la capacidad de prueba del software, la clave es mejorar el diseño del software y escribir código comprobable.

Escribe código componible. De esta señal, sacamos dos inferencias:

  • No cree objetos dentro de los componentes;

     static class A {
      private AdCampaignStateMachineTest b=new AdCampaignStateMachineTest();
    
    //  public A(AdCampaignStateMachineTest b) {
    //   this.b = b;
    //  }
    
      public static void main(String[] args) {
          //推荐
    //   A a = new A(new AdCampaignStateMachineTest());
    //   a.xx();
          //不推荐
    //   A a = new A();
    //   ReflectUtil.setFieldValue(a, "b", new AdCampaignStateMachineTest());
    //   a.xx()
      }
     }
    
  • No escriba métodos estáticos.

    Mockito no puede burlarse de métodos estáticos. La ventaja de utilizar el método de objeto es que es conveniente para Spring DI.

Al no escribir métodos estáticos, podemos deducir:

  • no utilice el estado global;

  • No utilice el patrón Singleton.

En el trabajo real, además de escribir código comercial, también encontrará integración de terceros:

  • Para el caso de llamar a la biblioteca, podemos definir la interfaz y luego implementar la llamada a la biblioteca de terceros para lograr el aislamiento del código;

  • Si nuestro código es llamado por el marco, entonces el código de devolución de llamada es solo una capa delgada, responsable de reenviar desde el código del marco al código comercial.

marco simulado

La prueba no es fácil de probar, la clave es el problema del diseño del software. Un buen diseño puede aislar muchos detalles de implementación del código comercial (como el uso de DDD).

Una razón importante para el aislamiento es que estos detalles de implementación no son tan controlables. Por ejemplo, si confiamos en una base de datos, debemos asegurarnos de que solo se utilice una prueba en el entorno de la base de datos al mismo tiempo. En teoría, esto no es imposible, pero el coste será muy elevado. Por poner otro ejemplo, si confiamos en un servicio de terceros, entonces no podemos controlarlo para que nos devuelva el valor esperado. De esta manera, es posible que no podamos probar muchos escenarios de error.

La lógica básica del marco Mock es muy simple: crear un objeto simulado y configurar su comportamiento es principalmente para dar qué tipo de retroalimentación al llamar con qué parámetros. Aunque la lógica del marco simulado en sí es muy simple, ha pasado por un largo período de desarrollo en la etapa inicial. Diferentes marcos simulados dan diferentes respuestas sobre qué se puede simular y cómo realizar el simulacro.

La discusión de hoy se basa en el marco Mockito, que también es el marco Mock más utilizado en la comunidad Java.

Para aprender el marco simulado, debes dominar sus dos puntos centrales: ****Configurar el objeto simulado y verificar el comportamiento del objeto .

Establecer el objeto simulado

Para configurar un objeto simulado, primero cree un objeto simulado. En combate real, lo hemos visto.

TodoItemRepository repository = mock(TodoItemRepository.class);

El siguiente paso es establecer su comportamiento, los siguientes son dos ejemplos tomados del combate real.

when(repository.findAll()).thenReturn(of(new TodoItem("foo")));
when(repository.save(any())).then(returnsFirstArg());

La API de una buena biblioteca debe ser muy expresiva, como los dos códigos anteriores, aunque no lo explique, puedes saber lo que hace mirando la declaración misma.

El núcleo de la configuración del objeto de simulación son dos puntos: cuál es el parámetro y cuál es el procesamiento correspondiente.

La configuración de parámetros es en realidad un proceso de coincidencia de parámetros. La pregunta principal que debe responderse es juzgar si los parámetros reales dados cumplen con las condiciones establecidas aquí. Como en el código anterior, la redacción de guardar significa que cualquier parámetro está bien y también podemos establecerlo en un valor específico, como el siguiente.

when(repository.findByIndex(1)).thenReturn(new TodoItem("foo"));

De hecho, también es un proceso de coincidencia de parámetros, pero aquí se cometen algunas omisiones y el método de escritura completo debería ser el siguiente.

when(repository.findByIndex(eq(1))).thenReturn(new TodoItem("foo"));

Si tiene un proceso de coincidencia de parámetros más complejo, incluso puede implementar un proceso de coincidencia usted mismo. Pero le recomiendo encarecidamente que no haga esto, porque la prueba debería ser sencilla. En general, los dos usos de igualdad y parámetros arbitrarios son suficientes en la mayoría de los casos.

Después de configurar los parámetros, el siguiente paso es el procesamiento correspondiente. Ser capaz de configurar el procesamiento correspondiente es la clave para incorporar la controlabilidad de los objetos simulados. En el ejemplo anterior, hemos visto cómo establecer el valor de retorno correspondiente y también podemos generar excepciones para simular escenarios de excepción.

when(repository.save(any())).thenThrow(IllegalArgumentException.class);

De manera similar a configurar parámetros, el procesamiento correspondiente también puede ser muy complicado de escribir, pero también recomiendo no hacer esto, la razón es la misma, la prueba debe ser simple. Saber cómo establecer el valor de retorno y cómo generar una excepción es suficiente en la mayoría de los casos.

Comprobar el comportamiento del objeto

Otro comportamiento importante del objeto simulado es verificar el comportamiento del objeto, es decir, saber si un método se llama como se esperaba. Por ejemplo, podemos esperar que se llame a la función guardar durante la ejecución.

verify(repository).save(any());

Esto solo verifica que se llamó al método guardar, también podemos verificar cuántas veces se llamó a este método.

verify(repository, atLeast(3)).save(any());

De manera similar, la verificación también tiene muchos parámetros que se pueden configurar, pero tampoco te recomiendo que la uses demasiado complicada, incluso te sugiero que no la uses demasiado para verificarla sola .

El uso de verificar dará a las personas una sensación de seguridad, por lo que hará que las personas tiendan a usarlo más, pero esto es una ilusión. Cuando hablé del marco de prueba, dije que verificar es en realidad una especie de afirmación. Afirmación significa que este es el comportamiento que debe tener una función y es un contrato de comportamiento.

Una vez que se establece la verificación, en realidad restringe la implementación de la función. Pero el objeto de la restricción de verificación es el componente subyacente, que es un detalle de implementación. En otras palabras, el resultado del uso excesivo de verificar es sofocar los detalles de implementación de una función.

El uso excesivo de verificar le dará una sensación de logro al escribir código. Sin embargo, cuando se trata de modificar el código, la persona en su totalidad no es buena. Debido a que los detalles de implementación están bloqueados por verificar , una vez que se modifica el código, esta verificación puede hacer que la prueba falle fácilmente.

Las pruebas deben probar el comportamiento de la interfaz, no la implementación interna . Por tanto, aunque verificar es bueno, se recomienda utilizarlo con moderación. Si hay algunos escenarios en los que no hay nada que afirmar sin verificar, aún así se debe utilizar la verificación.

Según el modo de prueba, el comportamiento de configuración del objeto simulado debe considerarse como Stub, y el método para verificar el comportamiento del objeto es simulado. Según el patrón, a menudo deberíamos usar Stub y menos Mock.

Cómo escribir pruebas unitarias

No soy un gran programador; Solo soy un buen programador con grandes hábitos.

No soy un gran programador, sólo un buen programador con buenos hábitos.

——Kent Beck

Muchos equipos escriben menos pruebas unitarias debido a diversas razones (como un diseño deficiente). Pero para mejorar la calidad del código y localizar problemas con mayor precisión, deberíamos escribir más pruebas unitarias.

Las pruebas unitarias se escriben mejor junto con el código de implementación para reducir la complejidad de las pruebas complementarias posteriores . Si desea escribir una buena prueba, la clave es hacer un buen trabajo de descomposición de tareas; de lo contrario, ante una gran demanda, nadie sabe cómo escribir una prueba unitaria para ella.

El proceso de redacción de pruebas unitarias es en realidad un proceso de desarrollo de tareas . Completar un código de tarea no es solo escribir el código de implementación, sino también pasar la prueba correspondiente . En términos generales, el desarrollo de tareas debe diseñar primero la interfaz correspondiente, determinar su comportamiento, luego diseñar los casos de prueba correspondientes de acuerdo con esta interfaz y, finalmente, instanciar estos casos de uso en pruebas unitarias específicas una por una.

Un problema común con las pruebas unitarias es que tan pronto como se refactoriza el código, la prueba unitaria falla. Esto se debe en gran medida a la estricta dependencia de las pruebas de los detalles de implementación. En general, las pruebas unitarias están mejor diseñadas para el comportamiento de la interfaz , ya que este es un requisito más amplio. De hecho, muchos detalles en la prueba también se pueden considerar más amplios, como la configuración del objeto simulado, la configuración del servidor simulado, etc.

cobertura de prueba

La cobertura de prueba es una métrica que se refiere a la proporción de código que se ejecuta cuando se ejecuta un conjunto de pruebas. Una de sus funciones principales es decirnos cuánto código se prueba. De hecho, hablando más estrictamente, la cobertura de prueba debería llamarse cobertura de código, pero en la mayoría de los casos se usa en escenarios de prueba, por lo que en las discusiones de muchas personas no se hace una distinción estricta.

Dado que la cobertura de la prueba es un indicador de medición, necesitamos saber qué indicadores específicos existen. Los indicadores comunes de cobertura de la prueba son los siguientes:

  • Cobertura de funciones: cuántas funciones definidas en el código se llaman;

  • Cobertura de declaraciones: cuántas declaraciones en el código se ejecutan;

  • Cobertura de sucursales (cobertura de sucursales): cuántas sucursales en la estructura de control se ejecutan (como la condición en la declaración if);

  • Cobertura de condición (cobertura de condición): si la subexpresión de cada expresión booleana se ha verificado para detectar diferentes casos de verdadero y falso;

  • Cobertura de línea: cuántas líneas de código se prueban.

Tomando la tasa de cobertura de funciones como ejemplo, si definimos 100 funciones en el código y solo ejecutamos 80 después de ejecutar la prueba, entonces su tasa de cobertura de funciones es 80/100 = 0,8, que es 80%.

Estos indicadores básicamente saben lo que está pasando de un vistazo, lo único que es un poco más complicado es la cobertura de condiciones, porque prueba todos los valores verdaderos y falsos de cada subexpresión en una expresión booleana. en el código siguiente.

if ((a || b) && c) {
  ...
}

Es una situación aparentemente tan simple, porque involucra tres subexpresiones de a, b y c, y se deben probar los valores verdadero y falso de cada subexpresión, por lo que hay 8 situaciones.

imagen

05d61b4eedb1d0fe5d1a04e6e4bf1fc4-1662966759_copia

En el caso de que la condición sea relativamente simple, la cobertura de la condición es en realidad muy complicada. Si las condiciones aumentan aún más, la complejidad aumentará aún más y no es fácil cubrir completamente las condiciones en la prueba. Esto también nos da una pista de codificación: minimizar las condiciones tanto como sea posible. De hecho, en proyectos reales, muchas condiciones son innecesariamente complicadas y algunas condiciones complejas pueden dividirse regresando temprano.

JaCoCo: una herramienta de cobertura de pruebas de Java

A continuación, tomaré a Jacoco como ejemplo para hablar sobre cómo utilizar realmente una herramienta de cobertura de prueba.

JaCoCo es una herramienta de cobertura de prueba comúnmente utilizada en la comunidad Java. El nombre es la abreviatura de Java Code Coverage a primera vista. El equipo que lo desarrolló desarrolló originalmente un complemento de Eclipse llamado EclEmma, ​​​​que a su vez se utiliza para la cobertura de pruebas. Sin embargo, más tarde, el equipo descubrió que, aunque hay muchas implementaciones de cobertura de prueba en la comunidad de código abierto, la mayoría de ellas están vinculadas a herramientas específicas, por lo que decidieron iniciar el proyecto JaCoCo como una implementación independiente que no está vinculada a herramientas específicas. convirtiéndola en una técnica estándar en el entorno JVM.

Ya sabemos que existen muchos indicadores diferentes para la cobertura de pruebas, para aprender una herramienta de cobertura de pruebas específica lo principal es hacer un indicador correspondiente y saber cómo configurar el indicador correspondiente.

En JaCoCo, el concepto correspondiente al índice es contador. Qué indicadores queremos utilizar en la cobertura, es decir, qué diferentes contadores especificar.

Cada contador proporciona diferentes configuraciones, como el número de cobertura (COVEREDCOUNT), el número de no cobertura (MISSEDCOUNT), etc., pero a nosotros solo nos importa una cosa: la cobertura (COVEREDRATIO).

Con el contador y la configuración seleccionados lo siguiente que hay que determinar es el rango de valores, es decir, los valores máximo y mínimo. Por ejemplo, aquí nos centramos en cuál debe ser el valor de cobertura, y generalmente es el valor mínimo (mínimo) para configurarlo.

La cobertura es un ratio, por lo que su valor oscila entre 0 y 1. Podemos configurarlo según las necesidades de nuestros propios proyectos. Según la introducción anterior, si requerimos que la cobertura de la línea alcance el 80%, podemos configurarlo así.

counter: "LINE", value: "COVEREDRATIO", minimum: "0.8"

Bien, ahora tienes un conocimiento básico de JaCoCo. Pero generalmente en el proyecto, rara vez lo usamos directamente, sino que lo combinamos con el proceso de automatización de nuestro proyecto.

Uso de la cobertura de prueba en su proyecto

Éste es el valor de la inspección automatizada. En circunstancias normales, siempre que hagas un buen trabajo, funcionará silenciosamente debajo y no te afectará. Una vez que olvides algo por alguna negligencia, saltará para recordártelo.

Ya sea Ant, Maven o Gradle, las principales herramientas de automatización de la comunidad Java brindan soporte para JaCoCo y podemos configurarlas de acuerdo con las herramientas que elijamos. En la mayoría de los casos, configúrelo una vez y todo el equipo podrá usarlo.

Configuración de Maven Jacobo

   <plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>${jacoco.version}</version>
    <executions>
     <execution>
      <id>default-prepare-agent</id>
      <goals>
       <goal>prepare-agent</goal>
      </goals>
     </execution>
     <execution>
      <id>default-report</id>
      <phase>test</phase>
      <goals>
       <goal>report</goal>
      </goals>
     </execution>
     <!--                    <execution>-->
     <!--                        <id>default-check</id>-->
     <!--                        <goals>-->
     <!--                            <goal>check</goal>-->
     <!--                        </goals>-->
     <!--                        <configuration>-->
     <!--                            <rules>-->
     <!--                                <rule>-->
     <!--                                    <element>BUNDLE</element>-->
     <!--                                    <limits>-->
     <!--                                        <limit>-->
     <!--                                            <counter>INSTRUCTION</counter>-->
     <!--                                            <value>COVEREDRATIO</value>-->
     <!--                                            <minimum>0.8</minimum>-->
     <!--                                        </limit>-->
     <!--                                        <limit>-->
     <!--                                            <counter>BRANCH</counter>-->
     <!--                                            <value>COVEREDRATIO</value>-->
     <!--                                            <minimum>0.8</minimum>-->
     <!--                                        </limit>-->
     <!--                                        <limit>-->
     <!--                                            <counter>COMPLEXITY</counter>-->
     <!--                                            <value>COVEREDRATIO</value>-->
     <!--                                            <minimum>0.8</minimum>-->
     <!--                                        </limit>-->
     <!--                                        <limit>-->
     <!--                                            <counter>LINE</counter>-->
     <!--                                            <value>COVEREDRATIO</value>-->
     <!--                                            <minimum>0.8</minimum>-->
     <!--                                        </limit>-->
     <!--                                        <limit>-->
     <!--                                            <counter>METHOD</counter>-->
     <!--                                            <value>COVEREDRATIO</value>-->
     <!--                                            <minimum>0.8</minimum>-->
     <!--                                        </limit>-->
     <!--                                    </limits>-->
     <!--                                </rule>-->
     <!--                            </rules>-->
     <!--                        </configuration>-->
     <!--                    </execution>-->
    </executions>
   </plugin>

El punto clave aquí es vincular la cobertura de la prueba con el proceso de envío. En el combate real, debemos ejecutar el proceso de inspección antes de enviar, y la inspección de cobertura de prueba está en este proceso. De esta forma, se garantiza que no es una existencia independiente, y no solo juega un papel en nuestro proceso de desarrollo, sino que también juega un papel en el proceso de integración continua.

En el desarrollo diario, lo que realmente enfrentamos es cuando falla la cobertura de la prueba. Por ejemplo, en nuestro combate real, cuando ejecutamos un script para verificar el código, si la cobertura de la prueba no es suficiente, obtendremos el siguiente mensaje.

Rule violated for package com.github.dreamhead.todo.cli.file: lines covered ratio is 0.9, but expected minimum is 1.0

Los errores que se informarán aquí dependen de cuántos contadores hayamos configurado. Según mis hábitos habituales, configuraré todos los contadores para poder encontrar más problemas.

Sin embargo, este mensaje simplemente nos dice que la cobertura de la prueba no es suficiente, pero si no es suficiente, debemos verificar el informe de cobertura de la prueba. En términos generales, el informe de cobertura de la prueba se configura cuando lo integramos con la herramienta. JaCoCo puede proporcionar varios tipos de informes: XML, CSV, HTML, etc. Según los hábitos de uso generales, preferiría utilizar informes HTML, para que puedan abrirse y leerse directamente con un navegador. También puedes configurar un formato diferente si tienes herramientas que requieren informes en otros formatos.

La ubicación del informe generado también es configurable. Lo configuro en el directorio buildDir/reports/jacoco en el proyecto real. Aquí, $buildDir se refiere al directorio de cada producto de compilación de módulo. En términos generales, es la tabla de contenido de compilación. Entonces, cada vez que veo un error de compilación debido a la cobertura de la prueba, puedo abrir el archivo index.html en este directorio y le brindará una descripción general de toda la cobertura de la prueba para este módulo.

imagen

imagen

En el proyecto de combate real, el requisito de cobertura de nuestra configuración es del 100%, por lo que podemos encontrar fácilmente dónde está el lugar descubierto, es decir, el lugar en rojo. Luego podemos rastrear hasta el final, encontrar la clase específica, encontrar el método específico y finalmente ubicar la declaración específica. El siguiente es el problema que hemos localizado en el combate real.

imagen

imagen

Después de encontrar una cobertura de prueba insuficiente específica, el siguiente paso es encontrar formas de aumentar la tasa de prueba. Generalmente, estos escenarios se pueden cubrir en casos simples agregando o ajustando algunas pruebas. Pero también hay algunos que no son tan fáciles de cubrir. Por ejemplo, en el combate real, vemos que se lanza IOException en la API de Jackson.

Sin embargo, cómo resolver este problema específicamente, habrá sus propias soluciones para diferentes estudiantes. La parte realmente controvertida de este lugar es por qué la cobertura de la prueba está establecida en el 100%.

En proyectos reales, muchas personas que no quieren escribir pruebas esperan que cuanto menor sea el número, mejor, pero de hecho también sabemos muy bien que no tiene ningún sentido establecer este número demasiado bajo.

Pruebas de integración

En comparación con las pruebas unitarias, que sólo se centran en el comportamiento de la unidad, las pruebas de integración se centran en el rendimiento de múltiples componentes trabajando juntos. Una es la integración entre códigos y la otra es la integración de códigos y componentes externos.

Para la integración entre códigos, por un lado, debemos considerar cómo funcionan juntas las distintas unidades que escribimos;

Por otro lado, en el caso de utilizar varios frameworks, se considera la integración con el framework. Si tenemos pruebas unitarias, esta integración se ocupa principalmente de la fluidez del enlace, por lo que, en términos generales, solo necesitamos ensamblar códigos relacionados para realizar pruebas a lo largo de una ruta de ejecución.

Si se trata de un marco, es mejor poder integrarlos juntos. Un marco mejor diseñado tiene mejor soporte para las pruebas (como Spring Boot), lo que nos permite realizar pruebas fácilmente.

Para la integración de componentes externos, la dificultad radica en cómo controlar el estado de los componentes externos. La base de datos tiene una solución relativamente madura a este respecto: use una base de datos separada y retroceda una vez finalizada la prueba.

Pero la mayoría de los sistemas no tienen una solución tan buena, especialmente los servicios de terceros. En este momento, tenemos que ver si existen alternativas adecuadas. Para la mayoría de las API REST, podemos utilizar un servidor simulado para simular el servicio.

Algunos códigos no son fáciles de cubrir en escenarios de automatización debido a problemas de infraestructura, por lo que enfatizamos que el código combinado con el marco debe ser delgado, para que el impacto de este código sea el menor posible. Esto también está reduciendo el esfuerzo para cubrir las pruebas de nivel superior.

Cómo realizar pruebas unitarias en el proyecto Spring

Sin embargo, antes de la aparición de Spring Boot, era precisamente por la incapacidad de deshacerse del modelo de empaquetado e implementación que todavía era difícil desarrollarlo en base a este camino, se puede decir que el problema no ha cambiado fundamentalmente. Pero el concepto de desarrollo liviano de Spring es la fuerza impulsora detrás de todo el camino hacia adelante. Dado que el servidor web no se podía abandonar en ese momento, Spring simplemente eligió otra forma: comenzar con soporte de prueba.

Entonces Spring proporciona una forma de realizar pruebas, lo que nos permite verificar completamente el código que escribimos localmente antes del empaquetado final. Has visto cómo usar Spring para realizar pruebas en la sesión práctica. En pocas palabras, se trata de utilizar pruebas unitarias para construir un núcleo empresarial estable y utilizar la infraestructura proporcionada por Spring para las pruebas de integración.

Estrictamente hablando, construir un núcleo empresarial estable en realidad no depende de Spring, pero Spring proporciona una infraestructura para ensamblar componentes, es decir, un contenedor de inyección de dependencia (DI). Por lo general, usamos contenedores DI para completar nuestro trabajo, y es precisamente porque los contenedores DI son fáciles de usar que a menudo conduce a un  uso indebido de los contenedores DI, lo que dificulta las pruebas.

Por lo tanto, la clave para las pruebas unitarias en un proyecto Spring es garantizar que el código se pueda combinar, es decir, mediante inyección de dependencia . Se podría decir que todos usamos Spring, por lo que el código debe combinarse. Esto no es necesariamente cierto: algunas prácticas incorrectas dañarán la inyección de dependencia, lo que a su vez provocará dificultades en las pruebas unitarias.

No utiliza inyección basada en campo.

Un error típico es la inyección basada en campo, como la siguiente. Cuando se usa set para inyectar propiedades simuladas, se debe usar la reflexión

@Service
public class TodoItemService {
  @Autowired
  private TodoItemRepository repository;

}

@Autowired es una característica muy útil, le indicará a Spring que inyecte automáticamente los componentes correspondientes por nosotros. Agregar Autowired a un campo es un código fácil de escribir, pero es muy hostil para las pruebas unitarias, porque el valor de este campo debe establecerse de manera engorrosa, como mediante la reflexión.

¿Qué pasa si no utiliza la inyección basada en el campo? De hecho, es muy simple, simplemente proporcione un constructor y coloque @Autowired en el constructor, como se muestra a continuación.

@Service
public class TodoItemService {
  private final TodoItemRepository repository;

  @Autowired
  public TodoItemService(final TodoItemRepository repository) {
    this.repository = repository;
  }
  ...
}

De esta manera, al escribir pruebas, solo necesitamos probarlas como objetos ordinarios . Si no recuerdas el método específico, puedes revisar el enlace de combate real.

Generalmente, podemos usar las teclas de acceso directo del IDE para generar este tipo de constructor, por lo que este código no es una carga pesada para nosotros. Si aún no le gusta la redundancia de este tipo de código, también puede usar la Anotación de Lombok (Lombok es una biblioteca que nos ayuda a generar código) para simplificar el código, como se muestra a continuación.

@Service
@RequiredArgsConstructor
public class TodoItemService {
  private final TodoItemRepository repository;
  private @NotNull Bean1 bean1;
  ...
}

No depende de ApplicationContext

Otro error típico al usar Spring es obtener objetos dependientes a través de ApplicationContext, como el siguiente.

@Service
public class TodoItemService {
  @Autowired
  private ApplicationContext context;
  
  private TodoItemRepository repository; 
  
  public TodoItemService() {
    this.repository = context.getBean(TodoItemRepository.class);
  }
  ...
}

Es una práctica completamente incorrecta que ApplicationContext aparezca en el código comercial principal. Por un lado, rompe el diseño original del contenedor DI y, por otro lado, también hace que el código comercial principal dependa del código de terceros (es decir, ApplicationContext).

Veámoslo desde el punto de vista del diseño: la aparición de ApplicationContext hace que sea necesario introducir ApplicationContext cuando probamos este código. Para obtener los componentes correspondientes en el código, debe agregar los componentes correspondientes al ApplicationContext en la prueba, lo que complicará una prueba originalmente simple.

Verá, una prueba normal es muy simple, pero debido a la introducción de Spring, muchas personas la harán mal. La mayor ventaja de Spring es que no depende de Spring a nivel de código, pero el enfoque incorrecto es confiar profundamente en Spring.

Cómo realizar pruebas de integración en proyectos Spring

prueba de base de datos

Hoy en día, la base de datos casi se ha convertido en la configuración estándar de todos los proyectos comerciales, por lo que Spring también proporciona un buen soporte para las pruebas de bases de datos. Hemos dicho antes que una buena prueba debe ser repetible, poner esta oración en la base de datos es para asegurar que la base de datos antes de la prueba sea la misma que la base de datos después de la prueba. ¿Como hacer esto?

configuración de prueba

Por lo general, existen dos métodos: uno es utilizar una base de datos de memoria integrada, es decir, después de ejecutar la prueba, los datos en la memoria se pierden de una vez. Otra forma es utilizar una base de datos real. Para garantizar que la base de datos sea coherente antes y después de la prueba, utilizaremos el método de reversión de transacciones y no enviaremos datos a la base de datos.

Un punto clave de nuestras pruebas es que no podemos modificar el código a voluntad. Recuerde, el código no se puede modificar según las necesidades de la prueba. Si cambia, tal vez sea el diseño lo que deba cambiarse, no solo el código.

Si bien el código no se puede modificar, podemos proporcionar diferentes configuraciones. Siempre que proporcionemos información de conexión de base de datos diferente a la aplicación, se conectará a bases de datos diferentes. Spring nos brinda la oportunidad de proporcionar diferentes configuraciones, siempre que declaremos una configuración de propiedad diferente en la prueba, el siguiente es un ejemplo.

@ExtendWith(SpringExtension.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource("classpath:test.properties")
public class TodoItemRepositoryTest {
  ...
}

En este código, proporcionamos una configuración de prueba, que es una configuración proporcionada por @TestPropertySource. Esto es para reemplazar nuestra configuración predeterminada (es decir, nuestra base de datos real) con la configuración en el archivo test.properties en el classpath.

Base de datos integrada en memoria

Como dijimos anteriormente, debemos garantizar la repetibilidad de la base de datos de dos maneras: base de datos en memoria integrada y reversión de transacciones. Para utilizar una base de datos integrada en memoria, debemos proporcionar una configuración de base de datos integrada en memoria. En el mundo Java, las bases de datos de memoria integrada comunes incluyen H2, HSQLDB, Apache's Derby, etc. Solo necesitamos configurar una dependencia de prueba, tomando H2 como ejemplo, de la siguiente manera.

testImplementation "com.h2database:h2:$h2Version"

Luego, proporcione una configuración correspondiente, como la siguiente.

jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create

Con un poco de suerte, tus pruebas deberían ejecutarse sin problemas. Sí, con un poco de suerte.

La razón por la que el desarrollo de software es tan serio y serio se atribuye a la suerte, por lo que tenemos que hablar del uso de bases de datos de memoria integrada.

Por lo tanto, la tecnología de la base de datos de memoria integrada se ve hermosa, pero no la uso mucho en proyectos reales y uso más la reversión de transacciones.

reversión de transacciones

En cuanto a la reversión de transacciones, nuestra configuración es casi la misma que la configuración de la aplicación estándar, la siguiente es la configuración que usamos en el combate real.

spring.datasource.url=jdbc:mysql://localhost:3306/todo_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=todo
spring.datasource.password=geektime
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

En términos generales, para evitar conflictos de datos entre el proceso de prueba y el proceso de desarrollo, crearemos dos bases de datos diferentes, que en MySQL son dos declaraciones SQL.

create database todo_dev;
create database todo_test;

De esta manera, uno se usa para pruebas manuales y el otro para pruebas automatizadas. Puede ver la diferencia entre los dos en el nombre del sufijo de la base de datos. Por cierto, la popularidad general de esta práctica en la industria se debe a Ruby on Rails (un marco de desarrollo web Ruby), que trajo una gran interrupción a toda la industria en las prácticas de desarrollo de software.

Con este enfoque, nuestro código se enfrenta al mismo motor de base de datos, por lo que no tenemos que preocuparnos por las incompatibilidades de SQL.

La reversión de transacciones de la que estamos hablando se refleja en @DataJpaTest, lo que hace que la reversión de la base de datos sea la configuración predeterminada, por lo que podemos obtener esta capacidad sin hacer nada.

Como ocurre con la mayoría de las pruebas, al probar la integración con una base de datos, también debemos hacer cierta preparación. Lo que hay que preparar a menudo son algunos datos, que se insertan de antemano en la base de datos. Podemos utilizar la infraestructura (TestEntityManager) preparada por Spring para completar este trabajo en la base de datos, aquí hay un ejemplo.

@ExtendWith(SpringExtension.class)
@DataJpaTest
public class ExampleRepositoryTests {
  @Autowired
  private TestEntityManager entityManager;

  @Test
  public void should_work() throws Exception {
    this.entityManager.persist(new User("sboot", "1234"));
    ...
  }
}

Si no está utilizando JPA sino otros métodos de acceso a datos, Spring también nos proporciona @JdbcTest, que equivale a una configuración más básica, porque puede funcionar bien siempre que haya un DataSource, que es adecuado para absolutamente la mayoría de los Casos de prueba. En consecuencia, el trabajo con datos también es más directo. Utilice SQL. El siguiente es un ejemplo.

@JdbcTest
@Sql({"test-data.sql"})
class EmployeeDAOIntegrationTest {
  @Autowired
  private DataSource dataSource;
  
  ...
}

Pruebas de interfaz web

Además de la base de datos, otra cosa que hoy casi se ha convertido en estándar es la Web. Spring también proporciona muy buen soporte para pruebas web.

Si trabajas de la misma manera que yo trabajo en el combate real, encontrarás que en el paso de escribir la interfaz web, básicamente hemos completado casi todo el trabajo y solo necesitamos darle al mundo exterior una interfaz para conectarlo a nuestro sistema. En el combate real anterior, utilizamos el método de integración general para probar el sistema. El punto clave aquí es @SpringBootTest, que conecta todos los componentes.

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class TodoItemResourceTest {
  ...
}

Cuando hablo de pruebas de integración, una vez dije que hay dos tipos de pruebas de integración, una es una prueba que integra todos los códigos y la otra es la integración de componentes externos. Desde el punto de vista del código, esta última prueba es solo para pruebas unitarias, por lo que tiene las características tanto de pruebas unitarias como de pruebas de integración. De hecho, también existe un método integrado similar a las pruebas unitarias para probar interfaces web, que utiliza @WebMvcTest.

@WebMvcTest(TodoItemResource.class)
public class TodoItemResourceTest {
  ...
}

Como puede ver en este código, aquí especificamos el componente TodoItemResource a probar. En esta prueba, no integrará todos los componentes, solo las partes relacionadas con TodoItemResource, pero todo el proceso web estará completo.

Si se considera una prueba unitaria, el código detrás de la capa de servicio es externo y podemos usar objetos simulados para controlarlo dentro de un rango controlable, y en este momento MockBean comienza a desempeñar un papel.

@WebMvcTest(TodoItemResource.class)
public class TodoItemResourceTest {
  @MockBean
  private TodoItemService service;
  
  @Test
  public void should_add_item() throws Exception {
    when(service.addTodoItem(TodoParameter.of("foo"))).thenReturn(new TodoItem("foo"));
    ...
  }
}

Aquí, el objeto simulado TodoItemService marcado por @MockBean participará en el proceso de ensamblaje del componente y se convertirá en parte de TodoItemResource, y podemos establecer su comportamiento. Si la interfaz web tiene una interacción más compleja con la capa de servicio, entonces este enfoque puede manejarla bien. Por supuesto, como hemos estado diciendo, no recomiendo complicar demasiado las cosas aquí.

@WebMvcTest, que se inclina hacia las pruebas unitarias, se ejecuta más rápido que @SpringBootTest, que integra todos los componentes. Entonces, si aumenta la cantidad de pruebas, usar @WebMvcTest tendrá ciertas ventajas.

Hay un punto clave más para comprender las pruebas de interfaz web. Como dije en el contenido anterior, Spring eliminó la mayor parte de su dependencia del servidor de aplicaciones en ese entonces, pero la Web no se deshizo de él. Por lo tanto, cómo realizar mejores pruebas sin depender del servidor web es el problema al que se enfrenta Spring. La respuesta es que  Spring proporciona un entorno web simulado.

Específico de nuestra prueba, es el papel que desempeña el objeto MockMvc. Repasemos su uso con el siguiente código.

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class TodoItemResourceTest {
    @Autowired
    private MockMvc mockMvc;
    ...

    @Test
    public void should_add_item() throws Exception {
        String todoItem = "{ " +
                "\"content\": \"foo\"" +
                "}";
        mockMvc.perform(MockMvcRequestBuilders.post("/todo-items")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(todoItem))
                .andExpect(status().isCreated());
        assertThat(repository.findAll()).anyMatch(item -> item.getContent().equals("foo"));
    }
}

La clave aquí es @AutoConfigureMockMvc, que configura MockMvc por nosotros, y el resto es que usamos este entorno configurado para acceder.

Desde el punto de vista de la implementación, es el entorno web simulado. El llamado entorno simulado se debe a que no inicia ningún servidor web real, sino que llama directamente a nuestro código, omitiendo el proceso de solicitar un viaje en la red. Pero el procesamiento principal después de que la solicitud ingresa al servidor está ahí, por lo que el procesamiento correspondiente está ahí (ya sea el procesamiento de varios filtros o la conversión del cuerpo de la solicitud al objeto de la solicitud). Ahora debes comprender que MockMvc es una parte importante del desarrollo liviano de Spring.

Spring ha hecho grandes esfuerzos para respaldar el desarrollo liviano, por lo que verificamos la mayor parte del contenido antes de integrar todo el sistema. Lo que presento aquí es solo el uso más típico. La prueba de Spring es definitivamente un tesoro que vale la pena explorar. Puedes leer su documentación para descubrir usos más interesantes.

Ahora tenemos una comprensión básica de cómo hacer un buen trabajo en pruebas unitarias y pruebas de integración en proyectos reales, pero en proyectos reales, ¿cómo deben combinarse los diferentes tipos de pruebas? Esto es lo que discutiremos en la próxima conferencia.

Varias relaciones de prueba

Características de la prueba

imagen

6ae19b91c63bb20ae0b16d5f0db3d411-1662966760_copia

Bueno, hasta ahora ya comprende las características comunes de las pruebas. A continuación, echemos un vistazo a los diferentes modelos de coincidencia de pruebas.

modelo de coincidencia de prueba

Las llamadas proporciones de prueba diferentes son en realidad qué tipo de pruebas escribir más. Y decidir qué tipo de prueba escribir más se debe principalmente a los diferentes puntos de partida de diferentes personas. Algunas personas piensan que una prueba debería cubrir un rango lo más amplio posible, por lo que se deberían escribir más pruebas del sistema. Algunas personas piensan que las pruebas deberían considerar la velocidad y el costo, por lo que se deberían escribir más pruebas unitarias.

Debido a que existen diferentes puntos de partida, existen dos modelos de relación de prueba típicos en la industria, uno es el modelo de cono de helado y el otro es el modelo de pirámide de prueba .

Primero veamos el modelo de cono de helado, como se muestra en la siguiente figura.

imagen

imagen

En esta figura, la prueba unitaria está en la parte inferior, lo que indica que es la capa inferior; luego el nivel aumenta gradualmente y la prueba del sistema, es decir, la prueba de un extremo a otro en la figura, es la prueba de alto nivel. , en la cima. Todas las pruebas automatizadas forman la parte del cono, mientras que la parte exterior del helado es la prueba manual.

El ancho de cada capa aquí indica el número de pruebas. No es difícil ver en la figura que se espera la proporción de pruebas: una pequeña cantidad de pruebas unitarias y una gran cantidad de pruebas del sistema.

El punto de partida del cono de helado es considerar la cobertura de una sola prueba, siempre que unas pocas pruebas del sistema sean suficientes para cubrir la mayor parte del sistema. Por supuesto, para aquellos escenarios que no pueden cubrirse mediante pruebas del sistema, se requiere cooperación de pruebas de bajo nivel, como pruebas de integración y pruebas unitarias. En el modelo de cono de helado, la fuerza principal es la prueba de alto nivel, y la prueba de bajo nivel es solo un complemento de la prueba de alto nivel.

Después de comprender el modelo de cono de helado, veamos la pirámide de prueba: la siguiente imagen muestra la pirámide de prueba.

imagen

imagen

En términos de expresión, la pirámide de prueba y el modelo de cono de helado son consistentes: la parte inferior representa la prueba de bajo nivel, cuanto mayor es el nivel de prueba, mayor es el nivel de prueba y el ancho de cada capa indica el número de pruebas.

Artículo del artículo Testing Pyramid de Martin Fowler. Por la forma general del diagrama, no es difícil ver que la pirámide de pruebas es lo opuesto al cono de helado: su objetivo es escribir más pruebas unitarias, mientras que el número de pruebas en la capa superior disminuye capa por capa.

El punto de partida de la pirámide de pruebas es que las pruebas de bajo nivel tienen un bajo costo, una velocidad rápida y una amplia cobertura general, por lo que se debe escribir más . Debido a que las pruebas de bajo nivel cubren casi todas las situaciones, las pruebas de alto nivel solo pueden cubrir algunas áreas grandes para garantizar que no haya problemas con la cooperación entre diferentes componentes. En este modelo, la fuerza principal es la prueba unitaria y la prueba de alto nivel se utiliza como complemento.

Bien, ahora que entendemos el modelo de coincidencia de pruebas, la siguiente pregunta que debemos responder es cómo utilizar estos dos modelos .

Desde la perspectiva de las mejores prácticas de la industria, la pirámide de pruebas ya es la mejor práctica de la industria . La pirámide de pruebas se basa en pruebas unitarias. Debido a su bajo costo y alta velocidad, las pruebas unitarias nos permiten obtener comentarios rápidamente durante el proceso de desarrollo. La pirámide de pruebas también es más fácil de seguir para un equipo que quiere escribir pruebas.

De hecho, lo que utilizamos en el combate real es el modelo piramidal de prueba, que se basa principalmente en pruebas unitarias, con una pequeña cantidad de pruebas de integración o pruebas de sistemas. Por lo tanto, si va a iniciar un nuevo proyecto, es mejor utilizar el modelo piramidal de prueba, y el método específico que hemos visto en el enlace de combate real es escribir pruebas capa por capa. Cada vez que se completa una función, el código y las pruebas siempre se escriben sincrónicamente y el código siempre se verifica, para que podamos avanzar de manera constante.

Dado que la pirámide de pruebas se ha convertido en una de las mejores prácticas de la industria, ¿por qué necesitamos comprender el modelo del cono de helado? Porque no todos los artículos son nuevos.

Por diversas razones históricas, muchos proyectos heredados no se prueban. Después de que el proyecto se desarrolló por un período de tiempo, el equipo comenzó a prestar atención a la calidad del producto, por lo que todos comenzaron a complementar la prueba.

En este caso, se espera que las pruebas complementarias establezcan rápidamente una red de seguridad, que debe comenzar rápidamente con las pruebas del sistema. Siempre que se escriban algunas pruebas de alto nivel, se pueden cubrir la mayoría de las funciones del sistema, lo que pertenece a la práctica de "baja inversión y resultados rápidos". Esta es también una razón importante por la que a muchas personas les gusta el modelo de cono de helado.

Sin embargo, debemos saber que en el caso de pruebas complementarias no supone ningún problema realizarlas. Si lo tomamos como norma para el desarrollo, entonces algo anda mal. Esto es como la relación entre el tratamiento médico y el fitness. Aunque ir al hospital puede solucionar rápidamente ciertos problemas en un corto periodo de tiempo, no puedes ir al hospital sólo porque no tienes nada que hacer. Sólo haciendo más ejercicio de forma regular. Diariamente puedes reducir el número de veces que vas al hospital.

Entonces, para el modelo de cono de helado, es el punto de partida para escribir pruebas para proyectos heredados. Después de tener un resultado final de red de seguridad, todavía tenemos que avanzar hacia la dirección de la pirámide de pruebas, con las pruebas unitarias como base del todo . El código recién escrito debe organizar las pruebas de acuerdo con la pirámide de pruebas, que es una dirección sostenible. Específicamente cómo escribir pruebas en sistemas heredados, este es el tema que discutiremos en la próxima conferencia.

Mejores prácticas

Junit+Mockito

Tutorial de Junit: https://www.baeldung.com/junit

Mockito: https://www.baeldung.com/mockito-series

Groovy+Spock

Tutorial maravilloso: https://www.baeldung.com/groovy-language

Tutorial de Spock: https://www.baeldung.com/groovy-spock https://zhuanlan.zhihu.com/p/399510995

Tutorial de Spock-Spring: https://www.baeldung.com/spring-spock-testing

SonarQube

SonarQube es una plataforma de gestión de calidad de código fuente abierto que ayuda a los equipos de desarrollo a monitorear y gestionar la calidad del código para mejorar la calidad del software. Estas son algunas de las cosas que SonarQube puede hacer:

  1. Análisis estático de código: SonarQube puede analizar estáticamente el código para encontrar posibles defectos, lagunas, código duplicado y otros problemas en el código. El análisis estático es el análisis del código en tiempo de compilación sin ejecutarlo realmente.

  2. Evaluación de la calidad del código: SonarQube puede evaluar la calidad del código según un conjunto de criterios. Estos criterios incluyen complejidad del código, mantenibilidad, legibilidad, cobertura de pruebas, etc. Los resultados de la evaluación pueden ayudar al equipo de desarrollo a comprender la calidad del código y tomar las medidas adecuadas para mejorarlo.

  3. Seguimiento de la calidad del código: SonarQube puede ayudar al equipo de desarrollo a rastrear los cambios en la calidad del código para que los problemas de calidad del código puedan descubrirse y resolverse de manera oportuna. El equipo de desarrollo puede utilizar SonarQube para monitorear la calidad del código base y encontrar y resolver problemas a tiempo cuando el código cambia.

  4. Integración continua: SonarQube se puede integrar con herramientas de integración continua comunes (como Jenkins, Travis CI, etc.), lo que hace que el análisis de código y la inspección de calidad formen parte de la integración continua. Esto ayudará al equipo a encontrar y solucionar problemas en las primeras etapas del ciclo de desarrollo.

  5. Verificación de cumplimiento del código: SonarQube puede realizar verificaciones de cumplimiento del código para garantizar que cumpla con los estándares y las mejores prácticas de la industria. Esto ayuda a los equipos a escribir código más consistente y fácil de mantener.

  6. Gestión de defectos: SonarQube puede rastrear defectos de código y asignarlos a los desarrolladores correspondientes. Los desarrolladores pueden utilizar SonarQube para gestionar su lista de defectos y resolver problemas en el código.

TDD

El ritmo de TDD: refactorización rojo-verde.

imagen

imagen

prueba automatizada

https://hellosean1025.github.io/yapi/

test de presión

https://jmeter.apache.org/

https://mimeter.be.mi.com/scenes-info?scene=0&apiProtocol=1

Pruebas de rendimiento

https://github.com/openjdk/jmh

Recomendaciones de especificaciones de prueba única

  • De conos de helado a pirámides de prueba

  • El proceso de prueba único está conectado a la tubería CICD.

  • ¡No te saltes la prueba de compilación local!

  • Al modificar un determinado fragmento de código, complemente la prueba unitaria. Si no es fácil escribir la prueba unitaria, refactorice primero y la tasa de cobertura de la prueba unitaria alcanzará más del 80%.

  • La prueba única está escrita en el paquete correspondiente del directorio de prueba y el nombre de la clase es xxxTest

CampaignGateway.class --> CampaignGatewayTest.class

consejos de prueba de chatgpt

Para proyectos heredados de Java, escribir pruebas unitarias puede resultar difícil porque estos proyectos pueden carecer de una buena arquitectura y diseño, y el código también puede ser complejo y difícil de probar. A continuación se ofrecen algunas sugerencias que le ayudarán a escribir pruebas unitarias en proyectos heredados de Java:

  1. Elija el marco de prueba unitario adecuado: elija un marco de prueba unitario que se adapte a su proyecto, como JUnit o TestNG. Estos marcos cuentan con documentación extensa y soporte comunitario para ayudarlo a comenzar rápidamente y comenzar a escribir pruebas unitarias.

  2. Identifique los puntos de prueba: debe identificar los puntos críticos que desea probar para asegurarse de que está probando las piezas más críticas. Puede seleccionar piezas con menor cobertura para las pruebas o dar prioridad a las piezas de prueba que tengan un mayor impacto en la estabilidad y el rendimiento del sistema.

  3. Agregue pruebas de forma incremental: dado que los proyectos heredados pueden carecer de una buena arquitectura y diseño, puede resultar difícil escribir casos de prueba completos. Se recomienda comenzar con un módulo pequeño e independiente y agregar pruebas gradualmente mientras se mejora la estructura y el diseño del código para que las pruebas sean más fáciles.

  4. Uso de simulacros y apéndices: el código heredado puede depender de bibliotecas o sistemas externos, lo que puede dificultar las pruebas unitarias. En este caso, puede utilizar Mock and Stub para simular dependencias externas y asegurarse de que sus pruebas no se vean afectadas por el entorno externo.

  5. Integración continua y pruebas automatizadas: para garantizar que sus pruebas sean confiables y repetibles, se recomienda utilizar una herramienta de integración continua, como Jenkins o Travis CI, para ejecutar automáticamente sus pruebas y ejecutarlas después de cada confirmación de código.

En conclusión, escribir pruebas unitarias requiere habilidad y experiencia, requiere algo de tiempo y esfuerzo, pero puede ayudarlo a comprender mejor su código y garantizar su calidad. Al escribir pruebas unitarias, siga siempre buenas prácticas de codificación, como el principio de responsabilidad única y el principio de inversión de dependencia, para garantizar que sus pruebas sean legibles, mantenibles y extensibles.

  1. Refactorice su código para que sea más fácil de probar: cuando escribe pruebas unitarias, puede encontrar que su código es difícil de probar, lo que puede deberse a la falta de una buena arquitectura y diseño del código. En este caso, podría considerar refactorizar su código para que sea más fácil de probar. Algunas técnicas de refactorización comunes incluyen métodos de división, extracción de interfaces, eliminación de código duplicado, etc.

  2. Utilice herramientas de cobertura de código: el uso de herramientas de cobertura de código puede ayudarle a comprender si sus casos de prueba cubren suficiente código. Estas herramientas pueden ayudarle a encontrar áreas no cubiertas por sus casos de prueba y ayudarle a escribir casos de prueba más completos.

  3. Escriba casos de prueba mantenibles: es importante escribir casos de prueba mantenibles. Esto significa que sus casos de prueba deben ser fáciles de entender y modificar, y deben describir claramente el propósito de su prueba. Puede utilizar anotaciones y convenciones de nomenclatura para ayudarle a escribir casos de prueba que sean fáciles de mantener.

  4. Aprenda a manejar casos extremos: al escribir pruebas unitarias, debe considerar varios casos, incluidos casos extremos y excepciones. Estos casos pueden hacer que su código falle o se comporte mal, por lo que debe asegurarse de que sus casos de prueba cubran estos casos.

  5. Colaborar con los miembros del equipo: escribir pruebas unitarias es responsabilidad de todo el equipo. Trabaje con los miembros de su equipo para discutir estrategias y enfoques de prueba para garantizar que sus casos de prueba cubran puntos críticos y sean efectivos. Al escribir pruebas unitarias, también puede colaborar con otros desarrolladores y evaluadores para garantizar que sus casos de prueba sean completos y satisfagan las necesidades del sistema.

  6. Utilice pruebas basadas en datos: las pruebas basadas en datos son un enfoque de prueba que separa los datos de prueba del código de prueba para que pueda agregar, eliminar y modificar datos de prueba más fácilmente. Puede utilizar pruebas basadas en datos para probar diferentes aspectos de su sistema y asegurarse de que su código pueda manejar diversas situaciones de datos.

  7. Elija las afirmaciones correctas: elegir las afirmaciones correctas puede ayudarlo a probar la exactitud de su código. Los marcos de pruebas unitarias como JUnit y TestNG proporcionan una variedad de métodos de afirmación, incluidos métodos para comparar objetos, matrices, valores booleanos y más. Puede elegir el método de afirmación adecuado según sus necesidades.

  8. Utilice un nivel de prueba apropiado: la prueba unitaria es un nivel de prueba que prueba un solo componente o un solo método de su código. Además de las pruebas unitarias, existen otros niveles de pruebas, incluidas las pruebas de integración y las pruebas de un extremo a otro. Al escribir pruebas unitarias, debe centrar sus pruebas en el nivel más bajo de su código y agregar niveles adicionales de prueba según sea necesario.

  9. Mantenga los casos de prueba independientes: los casos de prueba deben ser independientes entre sí, lo que significa que no deben depender de otros casos de prueba ni del orden de las pruebas. Los casos de prueba independientes pueden garantizar la confiabilidad de los resultados de las pruebas y hacer que los casos de prueba sean fáciles de mantener y modificar.

  10. Registre y analice los resultados de las pruebas: al escribir pruebas unitarias, debe registrar los resultados de las pruebas y analizarlos. Esto puede ayudarle a identificar por qué falla un caso de prueba y encontrar las partes del código que necesitan mejorar. Puede utilizar herramientas de análisis y generación de informes de pruebas para ayudarle a registrar y analizar los resultados de las pruebas.

En conclusión, escribir pruebas unitarias es muy importante para mantener y mejorar proyectos heredados. Al escribir pruebas unitarias, debe elegir un marco de prueba adecuado, identificar puntos de prueba, refactorizar código, escribir casos de prueba fáciles de mantener y más. Al escribir pruebas unitarias sólidas y completas, puede mejorar la calidad del código, reducir errores y reducir los costos de mantenimiento.

Finalmente: el video tutorial completo de prueba de software a continuación se ha organizado y subido, y los amigos que lo necesiten pueden obtenerlo ellos mismos [Garantizado 100% gratis]

Documentación de la entrevista de prueba de software

Debemos estudiar para encontrar un trabajo bien remunerado. Las siguientes preguntas de la entrevista son los últimos materiales de entrevista de empresas de Internet de primer nivel como Ali, Tencent y Byte, y algunos jefes de Byte han dado respuestas autorizadas. Termine este conjunto Los materiales de la entrevista Creemos que todos pueden encontrar un trabajo satisfactorio.

Supongo que te gusta

Origin blog.csdn.net/weixin_50829653/article/details/132714440
Recomendado
Clasificación