Cómo hacer un buen trabajo en pruebas unitarias en proyectos de pruebas de software

prefacio

Como se menciona en el libro "Pruebas unitarias", las pruebas unitarias de aprendizaje no solo deben permanecer en el nivel técnico, como su marco de prueba favorito, una biblioteca simulada, etc., las pruebas unitarias son mucho más que "escribir pruebas", debe siga trabajando duro Maximizar el retorno del tiempo invertido en las pruebas unitarias, minimizar el esfuerzo que pone en las pruebas y maximizar los beneficios que brindan las pruebas no es fácil.

Al igual que los problemas que encontramos en el desarrollo diario, no es difícil aprender un idioma y dominar un método, la dificultad es maximizar el retorno del tiempo invertido. La prueba unitaria tiene muchos conocimientos y marcos básicos. Puede encontrar muchos de ellos cuando busca en Google. También hay muchas metodologías de mejores prácticas. Este artículo no va a discutir estos temas, sino a discutir cómo usar las pruebas unitarias. en nuestro trabajo diario esta arma.

Definición de prueba unitaria

¿Qué es la prueba unitaria? de Baidu

La prueba unitaria se refiere a comprobar y verificar la unidad comprobable más pequeña en el software. En cuanto al significado de [unidad], en términos generales, el significado específico debe determinarse de acuerdo con la situación real, como una unidad en Java se refiere a una clase, etc.

En términos humanos, la prueba unitaria es una prueba para verificar la precisión de una clase. Es diferente de las pruebas de integración y las pruebas del sistema. Es una prueba mínima dirigida por un desarrollador.

Algunos estudiosos también han dibujado la siguiente figura después de las estadísticas:

El 85% de los defectos se generan en la etapa de diseño del código ;

·  Cuanto más avanzada sea la etapa de descubrimiento de errores, mayor será el costo, que aumenta exponencialmente.

Desde este punto de vista, la escritura del código de prueba unitaria tiene un impacto extremadamente importante en la calidad de la entrega y el costo de la mano de obra.

errores comunes

Pérdida de tiempo y afecta la velocidad de desarrollo.

Las curvas de tiempo de desarrollo y prueba de diferentes proyectos son diferentes. Debe considerar exhaustivamente el ciclo de vida de su código, su capacidad para depurar y cuánto tiempo suele dedicar a revisar el código problemático. A medida que avanza el proyecto, estos tiempos aumentarán.Si desea que el código que escribe se use para siempre y para evitar que las personas se quejen de lo que escribió, las pruebas unitarias son muy necesarias.

Probar debe ser el trabajo de probar

El desarrollo es la primera persona responsable del código, y la persona que está más familiarizada con el código edita las pruebas unitarias durante la fase de diseño, lo que no solo le permite entregar con más confianza, sino que también reduce la aparición de problemas de prueba. Al mismo tiempo, sus propias capacidades full-stack también han mejorado.

Yo no escribí el código, no entiendo

A menudo nos quejamos de que el código anterior es difícil de entender o carece de CR. De hecho, el proceso de escribir pruebas unitarias también es un proceso de CR y aprendizaje, y tiene una comprensión profunda del flujo principal, los límites, las excepciones, etc. del código. Al mismo tiempo, también es un proceso de autoexamen de las especificaciones, la lógica y el diseño del código. Sugiero escribir pruebas unitarias en la refactorización y refactorizar en la escritura de pruebas unitarias, que se complementan entre sí.

Cómo escribir buenas pruebas unitarias

En términos de metodología, existe el principio de AIRE, que no se sentirá como aire, a saber, Automático (automatización), Independiente (independencia) y Repetible (repetible).

Mi entendimiento personal es:

1. Operación automática, a través de la integración de CI, para garantizar que la prueba unitaria pueda ejecutarse automáticamente y para garantizar el resultado de verificación de la prueba unitaria a través de afirmar en lugar de imprimir la salida. Asegúrese de que las pruebas unitarias se puedan ejecutar automáticamente sin intervención manual.

2. Las pruebas unitarias deben ser independientes, no pueden llamarse entre sí y no pueden tener un orden dependiente. Se garantiza que los paquetes sean independientes entre cada caso de prueba.

3. No puede verse afectado por el entorno operativo, la base de datos, el middleware, etc. Al escribir una prueba de unidad, debe simular las dependencias externas.

En términos de especificaciones de cobertura, existen muchos estándares tanto dentro de Alibaba como en la industria.

La tasa de cobertura de estados de cuenta alcanza el 70 %; la tasa de cobertura de estados de cuenta y la tasa de cobertura de sucursales del módulo principal alcanzan el 100 %. --- "Manual de desarrollo Java de Alibaba"

Referencia de calificación de cobertura de prueba única

Nivel 1: el proceso normal está disponible, es decir, cuando una función ingresa los parámetros correctos, tendrá la salida correcta

Nivel 2: el proceso de excepción puede generar excepciones lógicas, es decir, cuando los parámetros de entrada son incorrectos, no se pueden generar excepciones del sistema, pero las excepciones lógicas definidas por uno mismo se pueden usar para notificar el error a la capa superior del código de llamada.

Nivel 3: los casos extremos y los datos de límite están disponibles, y las condiciones de límite de los parámetros de entrada deben probarse por separado para garantizar que la salida sea correcta y válida

Nivel 4: pasa la lógica de todas las ramas y bucles, y no puede haber ningún proceso que no se pueda probar

Nivel 5: toda la verificación de campo de los datos de salida, para la salida con una estructura de datos compleja, asegúrese de que cada campo sea correcto

Del extracto anterior, tanto la cobertura de declaraciones como la cobertura de sucursales tienen requisitos numéricos y metodológicos, entonces, ¿cuál es la práctica en el trabajo real?

El autor una vez tuvo una tasa de cobertura incremental integral del código enviado en el trabajo que alcanzó casi el 100% en un trimestre. Puedo hablar de mi experiencia y práctica.

Se puede lograr muy fácilmente una tasa de cobertura de prueba única de alrededor del 60 %, pero para lograr una tasa de cobertura de más del 95 %, es necesario cubrir varias ramas y excepciones de código, e incluso métodos de configuración e inicialización de beans. efectos marginales enormes, pero decrecientes. Quiero probar toString, los métodos como getter/setter tampoco tienen sentido. Cuánto es apropiado, no creo que haya un estándar fijo. Un alto porcentaje de cobertura de código no indica éxito ni implica alta calidad de código. La parte que debe descartarse se ignora audazmente.

Mejores prácticas

Este título es un poco una fiesta de títulos. Hay innumerables libros y artículos de ata relacionados con las pruebas unitarias. Mi supuesta "mejor práctica" es algunos de los hoyos que he pisado en el trabajo real de Ali, o algunos puntos importantes que personalmente creo que son importantes. Si hay errores , Bienvenido a discutir.

1. Valor de límite de prueba oculto

public ApiResponse<List<Long>> getInitingSolution() {
      List<Long> solutionIdList = new ArrayList<>();
      SolutionListParam solutionListParam = new SolutionListParam();
      solutionListParam.setSolutionType(SolutionType.GRAPH);
      solutionListParam.setStatus(SolutionStatus.INIT_PENDING);
      solutionListParam.setStartId(0L);
      solutionListParam.setPageSize(100);
      List<OperatingPlan> operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      for(; !CollectionUtils.isEmpty(operatingPlanList);){
          /*
              do something
              */
          solutionListParam.setStartId(operatingPlanList.get(operatingPlanList.size() - 1).getId());
          operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      }
      return ResponsePackUtils.packSuccessResult(solutionIdList);
  }

¿Cómo escribir una prueba unitaria para el código anterior?

Naturalmente, cuando escribimos una prueba unitaria, simularemos la consulta de la base de datos y encontraremos la información. Pero si el contenido de la consulta supera los 100, porque el bucle for entra una vez, la cobertura automática de jacoco no puede encontrarlo. De hecho, este caso límite no está cubierto y estas condiciones límite solo pueden manejarse a través de los hábitos del desarrollador.

¿Cómo lidiar con estos valores límite ocultos? Los desarrolladores no pueden confiar en las pruebas de integración o el código CR. Deben tener esto en cuenta al escribir pruebas unitarias ellos mismos, para evitar que el futuro personal de mantenimiento caiga en la trampa.

2. No use @Transactional y opere bases de datos reales en pruebas de springboot

El contexto de las pruebas unitarias debe ser limpio, y la intención original de diseñar transaccionales es para pruebas de integración (como se presenta en el sitio web oficial de Spring):

Aunque es más fácil verificar la corrección de la capa DAO operando directamente la base de datos, también es fácil contaminarse con datos sucios en la base de datos fuera de línea, lo que resulta en la falla de la prueba única. El autor solía encontrar un solo código de prueba conectado directamente a la base de datos y, a menudo, cambiaba el código durante 5 minutos y limpiaba los datos sucios en la base de datos durante una hora. La segunda es que la prueba de integración necesita iniciar el contenedor de toda la aplicación, lo que viola la intención original de mejorar la eficiencia.

Si realmente desea probar la corrección de la capa DAO, puede integrar la base de datos integrada H2. Hay muchos tutoriales en línea, así que no los repetiré aquí.

3. Contenidos relacionados con el tiempo en la prueba única

El autor una vez se encontró con un caso extremo en el trabajo. Un CI por lo general funciona normalmente, pero una vez que se lanzó tarde en la noche, el CI no funcionó. Más tarde, después de verificar al día siguiente, se descubrió que alguien había tomado la hora actual en la prueba única La lógica contiene lógica nocturna (no se envían mensajes nocturnos), lo que hace que CI no pase. Entonces, ¿cómo lidiar con el tiempo en una sola prueba?

Al usar Mockito, puede usar mock(Date.class) para simular el objeto de fecha y luego usar when(date.getTime()).thenReturn(time) para establecer la hora del objeto de fecha.

¿Cómo obtienes la hora actual si usas calendar.getInstance()? Calendar.getInstance() es un método estático y Mockito no puede burlarse de él. Necesita introducir powerMock o actualizar a mockito 4.x para admitir:

@RunWith(PowerMockRunner.class)
  @PrepareForTest({Calendar.class, ImpServiceTest.class})   
  public class ImpServiceTest {
      @InjectMocks
      private ImpService impService = new ImpServiceImpl();
      @Before
      public void setup(){
          MockitoAnnotations.initMocks(this);
          Calendar now = Calendar.getInstance();
          now.set(2022, Calendar.JULY, 2 ,0,0,0);
          PowerMockito.mockStatic(Calendar.class);
          PowerMockito.when(Calendar.getInstance()).thenReturn(now);
      }
  }

4. Pruebas unitarias para clases finales, clases estáticas, etc.

Al igual que el ejemplo del calendario mencionado en el punto 3, el simulacro de la clase estática necesita la versión de mockito4.x. De lo contrario, se debe introducir powermock.Powermock no es compatible con mockito 3.x y mockito 4.x. Dado que la aplicación anterior introdujo muchas versiones de mockito3.x, el uso directo de mockito4.x para simular clases finales y estáticas requiere empaquetado. En la práctica, [url=] JUnit [/url], Mockito y Powermock tienen problemas de compatibilidad entre los números de versión, y puede ocurrir java.lang.NoSuchMethodError. Debe elegir una versión para burlarse de acuerdo con la situación real.

Sin embargo, cuando se establece un nuevo proyecto, es necesario determinar la versión de mockito y junit que se utilizará, y si se deben introducir marcos como powermock para garantizar que el entorno sea estable y esté disponible. Se recomienda no cambiar las versiones de mockito y powermock a gran escala para proyectos antiguos, ya que es fácil organizar paquetes y dudar de la vida.

5. La aplicación se inicia e informa la excepción de No se puede cargar esta clase SDK falsa

Esto se debe a que tair y metaq de Ali se basan en el contenedor de pandora, y la clase de módulo de pandora carga el fake-sdk de manera predeterminada. El principio específico puede hacer referencia a la siguiente figura:

 

Solución 1, presente el entorno pandoraboot.

@RunWith(PandoraBootRunner.clase)

En realidad, esto reduce la velocidad de ejecución de la prueba individual, lo que viola el principio de eficiencia. Pero en comparación con la ejecución de todo el contenedor, el tiempo de ejecución del contenedor Pandora es de unos 10 s, lo que sigue siendo aceptable.

Entonces, ¿hay un método simulado puro para evitar que Pandoraboot arranque? Personalmente, creo que simulacro es más importante que ut, especialmente algunas dependencias externas, que a menudo se migran o se desconectan, pueden cambiar 1 línea de código y necesitan reparar 1 hora de casos de prueba. Tair, lindorm y otros middleware no tienen forma de burlarse del entorno local, y es muy poco elegante depender directamente de recursos externos.

Solución 2, simulacro directo

Tome tair como ejemplo:

@RunWith(PowerMockRunner.class)
  @PrepareForTest({DataEntry.class})
  public class MockTair {
      @Mock
      private DataEntry dataEntry;
      @Before
      public void hack() throws Exception {
          //solve it should be loaded by Pandora Container. Can not load this fake sdk class. please refer to http://gitlab.alibaba-inc.com/mi ... dora-boot/wikis/faq for the solution
          PowerMockito.whenNew(DataEntry.class).withNoArguments().thenReturn(dataEntry);
      }

      @Test
      public void mock() throws Exception {
          String value = "value";
          PowerMockito.when(dataEntry.getValue()).thenReturn(value);
          DataEntry tairEntry = new DataEntry();
          //值相等
          Assert.assertEquals(value.equals(tairEntry.getValue()));
      }
  }

6. Cómo escribir una sola prueba en metaq

Consulte 5 para el método simulado de MessageExt, pero cómo ejecutar un bean MetaPushConsumer y llamar al método de escucha en la prueba única. Entonces solo se puede iniciar el contexto de contexto. La forma de alojar SpringRunner.

@RunWith(PandoraBootRunner.class)
  @DelegateTo(SpringRunner.class)
  public class EventProcessorTest {
      @InjectMocks
      private EventProcessor eventProcessor;
      @Mock
      private DynamicService dynamicService;
      @Mock
      private MetaProducer dynamicEventProducer;
      @Test
      public void dynamicDelayConsumer() throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
          //获取bean
          MetaPushConsumer metaPushConsumer = eventProcessor.dynamicEventConsumer();

          //获取Listener
          MessageListenerConcurrently messageListener = (MessageListenerConcurrently)metaPushConsumer.getMessageListener();
          List<MessageExt> list = new ArrayList<>();

          //这个需要依赖PandoraBootRunner
          MessageExt messageExt = new MessageExt();
          list.add(messageExt);
          Event event = new Event();
          event.setUserType(3);
          String text = JSON.toJSONString(event);
          messageExt.setBody(text.getBytes());
          messageExt.setMsgId(""+System.currentTimeMillis());

          //测试consumeMessage方法
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          doThrow(new RuntimeException()).when(dynamicService).triggerEventV2(any());
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          messageExt.setBody(null);
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
      }
  }

Para resumir cuándo usar contenedores:

// 1. 使用PowerMockRunner
  @RunWith(PowerMockRunner.class)
  // 2.使用PandoraBootRunner, 启动pandora,使用tair,metaq等
  @RunWith(PandoraBootRunner.class)
  // 3. springboot启动,加入context上下文,可以直接获取bean
  @SpringBootTest(classes = {TestApplication.class})

7. Intenta usar ioc

El uso de IOC puede desacoplar objetos, lo que facilita las pruebas. A menudo sucede que una determinada clase de herramienta se usa en un determinado servicio, y los métodos en esta clase de herramienta son todos estáticos. En este caso, al probar el servicio, debe probarse junto con la clase de herramienta.

Por ejemplo, el siguiente código:

@Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

Verificamos la información de ip del usuario logueado a través de IpUtil, y si la usamos de esta manera, necesitamos probar el método de IpUtil, el cual viola el principio de aislamiento. Probar el método de inicio de sesión también necesita agregar más conjuntos de datos de prueba para cubrir el código de clase de herramienta, y el grado de acoplamiento es demasiado alto.

Si se modifica ligeramente:

 @Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

De esta forma, solo necesitamos probar la clase IpUtil y la clase LoginServiceImpl por separado. Al probar LoginServiceImpl, basta con simular IpUtil, que aísla la implementación de IpUtil.

8. No pruebe código sin sentido para la cobertura

Por ejemplo, toString, como getter y setter, son todos códigos generados por máquinas, y las pruebas individuales no tienen sentido. Si es para mejorar la cobertura general de la prueba, excluya esta parte del paquete en CI:

 9. Cómo probar métodos de vacío

· Si el método void provoca cambios en la base de datos, como insertPlan (plan plan), y la base de datos se ha operado a través de H2, entonces se puede verificar el cambio en el número de entradas en la base de datos para verificar la corrección del método void .

·  Si el método void llama a una función, puedes obtener el número de llamadas a través del método de verificación de verificación:

userService.updateName(1L,"qiushuo");
  verify(mockedUserRepository, times(1)).updateName(1L,"qiushuo");

·  Si el método void puede provocar que se produzca una excepción.

Dothrow puede burlarse de la excepción lanzada por el método simulado:

@Test(expected = InvalidParamException.class)
  public void testUpdateNameThrowExceptionWhenIdNull() {
     doThrow(new InvalidParamException())
        .when(mockedUserRepository).updateName(null,anyString();
     userService.updateName(null,"qiushuo");
  }

Por último, me gustaría agradecer a todos los que han leído detenidamente mi artículo. La reciprocidad siempre es necesaria. Aunque no es algo muy valioso, puedes quitártelo si lo necesitas:

Estos materiales deben ser el almacén de preparación más amplio y completo para los amigos [de pruebas de software]. Este almacén también ha acompañado a decenas de miles de ingenieros de pruebas a través del viaje más difícil, ¡y espero que pueda ayudarlos! 

Supongo que te gusta

Origin blog.csdn.net/OKCRoss/article/details/131379002
Recomendado
Clasificación