Cuándo simular la prueba unitaria del código C / C ++

Foto de cabeza de Miroslaw Zielinski, gerente de producto de Parasoft

Por Miroslaw Zielinski

2 de junio de 2023

7  min leer

La prueba unitaria es el proceso de separar unidades y ejecutar pruebas independientes en cada una. Este artículo incluye una guía completa sobre cuándo simular pruebas unitarias y algunos consejos útiles para las pruebas unitarias de C y C++.

Las pruebas unitarias tienen que ver con probar unidades o funciones y operaciones aisladas. En esta publicación, analizamos varias oportunidades para simular, incluido el abordaje de algunas preguntas comunes para guiarlo de manera efectiva a través de Pruebas unitarias C y C++.

Comenzando con un ejemplo, a menudo recibo la pregunta: "¿Cuánto aislamiento de prueba unitaria necesitamos?" Esta es una pregunta recurrente e importante que suele debatirse cuando se desarrollan pruebas unitarias para C y C++.

No me refiero aquí al aislamiento del compañero desarrollador sentado junto a nosotros en el espacio abierto y tocando el ritmo de la música con sus auriculares, que, por cierto, también es muy importante cuando queremos crear contenido de buena calidad. código. Me refiero al aislamiento del código probado de su entorno circundante, sus supuestos colaboradores.

Antes de continuar, permítanme aclarar una cosa. Cuando se habla de stubing y mocking para lenguajes C y C++, generalmente se traza una línea entre C y C++ debido a las diferencias en la capa del lenguaje que se refleja en la complejidad, las capacidades y las expectativas con respecto a los marcos de trabajo de simulación típicos.

Con Parasoft C/C++test, la situación es ligeramente diferente porque la mayoría de las capacidades del marco están disponibles para ambos lenguajes. Por lo tanto, cuando discuta este tema, daré un ejemplo de prueba de unidad de C o C++ y, a menos que marque algo específicamente como compatible solo con C o C++, siempre debe asumir que se proporciona una funcionalidad específica para ambos idiomas.

¿Qué es burlarse y cómo funciona en las pruebas?

Se sabe que las definiciones son inconsistentes y han causado cierta confusión. Por lo tanto, esto amerita una breve explicación de lo que es un stub. Entonces explicar qué es un simulacro es más fácil.

El propósito de ambos es reemplazar una pieza de código o dependencia fuera de la unidad. La eliminación de todas las dependencias de una unidad o función permite que sus pruebas se centren en probar la calidad (seguridad, protección y confiabilidad) de la unidad.

El stubbing reemplaza las dependencias, pero es una implementación simple que devuelve valores enlatados. Los simulacros se centran en el comportamiento, por lo que son una implementación controlada por la prueba unitaria. Se pueden implementar con valores devueltos, verificar valores de argumentos y ayudar a verificar la funcionalidad de los requisitos comunes de seguridad y protección. Sin embargo, con toda honestidad, cuando estoy creando mis casos de prueba de unidad, no pienso en si estoy tropezando o burlándome. Solo estoy enfocado en probar la funcionalidad para determinar si satisface mis requisitos y si la implementación es sólida.

¿Aislar o no aislar?

Para algunas personas, el sentido común dicta que no debemos aislarnos a menos que tengamos una buena razón para ello. Probar mediante la inclusión de otros colaboradores o funcionalidades solo aumenta nuestra penetración en el código base. ¿Y por qué deberíamos dejar pasar la oportunidad de obtener una cobertura de código adicional y la posibilidad de encontrar errores fuera de la unidad? Bueno, hay buenas razones para ello.

Un probador de unidades ortodoxo argumentará que Las pruebas unitarias consisten en probar las unidades aisladas. y debe permanecer así. Si cada unidad individual es sólida, solo fortalece el todo. Además, las pruebas con colaboradores reales o la inclusión de otras funciones en la prueba se denominan pruebas de integración. Si incluimos todo el código, los casos de prueba se denominan pruebas del sistema.

Hay diferentes niveles de abstracción. Los desarrolladores y evaluadores deben realizar pruebas en estos diferentes niveles. El nivel más bajo se llama prueba unitaria y debe realizar el aislamiento de la unidad. Analicemos algunas razones para burlarse y las mejores prácticas para aislar unidades.

Razones para simular unidades en su proceso de prueba

1. Colaborador aún no implementado o aún en desarrollo

Esto es muy simple. No tenemos otra opción y necesitamos una implementación simulada. El diagrama a continuación ilustra este entorno típico de prueba unitaria (SUT - sistema bajo prueba, DOC - componente / colaborador dependiente):

2. Independencia del hardware

Para los desarrolladores que escriben aplicaciones de escritorio, esta clase de problemas puede parecer distante. Pero para los desarrolladores integrados, la independencia del hardware de las pruebas unitarias es un aspecto importante que permite la automatización y ejecución de pruebas de alto nivel sin necesidad de hardware.

Un buen ejemplo aquí sería una unidad bajo prueba que interactúa con el hardware GPS, esperando que se proporcione una cierta secuencia de coordenadas de localización para calcular la velocidad. Aunque es una buena idea que hagamos más ejercicio, no puedo imaginar a los probadores corriendo con un dispositivo para simular el movimiento, solo para generar las entradas de prueba requeridas, cada vez que se requiera una sesión de prueba unitaria. Con ese fin, este ejemplo ilustra qué tan tarde en el ciclo de vida de desarrollo sería la prueba de GPS de un dispositivo si la independencia del hardware no fuera posible durante el desarrollo.

3. Inyección de fallas

Inyectar errores a propósito es un escenario común en las pruebas. Esto podría usarse, por ejemplo, para probar si la asignación de memoria ha fallado o para ver si un componente de hardware ha fallado. Algunos desarrolladores intentan estimular al colaborador real en la fase de inicialización de la prueba para que responda con un error cuando se le llama desde el código probado. Para mí, esto no es práctico y suele ser demasiado complicado. Una implementación falsa específica de la prueba que simula una falla suele ser una opción mucho mejor.

Mejores prácticas para implementar la simulación en su proceso de prueba

Además de estos casos obvios en los que siempre se desea un colaborador burlado, existen otras situaciones más sutiles o mejores prácticas en las que burlarse o agregar colaboradores falsos es una buena opción. Además, si su proceso de prueba sufre alguno de los problemas enumerados a continuación, es una indicación de que se requiere un mejor aislamiento del código probado.

1. Cuando las pruebas unitarias no son repetibles

La transitoriedad puede ser un problema que dificulta la implementación de pruebas estables. Hay casos en que las unidades dependen de una señal externa para indicar el comportamiento. Un ejemplo clásico es una unidad que se basa en el reloj del sistema para su comportamiento. Por ejemplo, si una unidad reacciona de manera diferente en ciertos momentos, entonces la automatización es difícil de lograr. Una mejor práctica es simular la llamada al reloj del sistema y obtener un control total sobre los valores de entrada de tiempo.

2. Cuando los entornos de prueba son difíciles de inicializar

Inicializar el entorno de prueba puede resultar muy complejo. Simular a los colaboradores reales para que proporcionen entradas confiables al código probado puede ser una tarea desalentadora, si no imposible.

Los componentes a menudo están interrelacionados. Al intentar inicializar un módulo específico, podemos terminar inicializando la mitad del sistema. Reemplazar a los colaboradores reales con una implementación simulada o falsa reduce la complejidad de la inicialización del entorno de prueba.

3. Cuando el estado de la prueba es difícil de determinar

En muchos casos, la determinación del veredicto de la prueba requiere verificar el estado del colaborador después de que se ejecuta la prueba. Con colaboradores reales, a menudo es imposible porque no existe un método de acceso adecuado en la interfaz de colaborador real para consultar el estado después de la prueba.

Reemplazar a un colaborador real con un simulacro generalmente soluciona el problema. Podemos extender la implementación falsa con todo tipo de métodos de acceso para determinar el resultado de la prueba.

4. Cuando las pruebas son lentas

Hay casos en que una respuesta del colaborador real puede tomar una cantidad de tiempo considerable. No siempre está claro cuándo la demora se vuelve inaceptable y cuándo se requiere aislamiento. ¿Es aceptable o no un retraso de dos minutos en una prueba?

A menudo es deseable poder ejecutar conjuntos de pruebas lo más rápido posible, tal vez después de cada cambio de código. Los grandes retrasos debido a las interacciones con colaboradores reales pueden hacer que los conjuntos de pruebas sean demasiado lentos para ser prácticos. Los simulacros de estos colaboradores reales pueden ser más rápidos en varios órdenes de magnitud y llevar el tiempo de ejecución de la prueba a un nivel aceptable.

Ejemplo práctico de cuándo simular pruebas unitarias de código C/C++

Las interfaces simuladas pueden hacer que el trabajo de prueba sea mucho más fácil. En lugar de que su unidad llame a otros, puede llamar a una interfaz simulada. Su código de prueba puede interponerse en todos los lados de la unidad que desea probar y luego examinar todas las salidas y manejar todas las entradas.

Digamos que en el siguiente ejemplo de código, queremos probar la unidad/función de la barra y hacer que llame a una función foo falsa/simulada. Para hacerlo, necesitamos una forma de reemplazar y controlar la llamada a foo(). Hay varios enfoques de simulación para hacer esto y Parasoft puede automatizar gran parte de esto por usted. En este ejemplo y por motivos de simplicidad, se utiliza una macro (#define FOO) para controlar el colaborador foo().

#ifdef TEST
#define FOO mock_foo
#else
#define FOO foo
#endif
 
int mock_foo(int x)
{
	return x;
}
 
int bar(int x)
{
	int result = 0;
	for (int i = 0; i < 10; i++)
	{
		result += FOO(i + x);
	}
	return result;
}

Preguntas útiles para determinar si simular o no

Al escribir una nueva prueba unitaria de C o C++ y decidir si usar colaboradores originales o implementaciones simuladas, considere las siguientes cuatro preguntas.

  1. ¿El colaborador real es una fuente de riesgo para la estabilidad de mis pruebas?
  2. ¿Es difícil inicializar al colaborador real?
  3. ¿Es posible verificar el estado del colaborador después de la prueba, para decidir el estado de la prueba?
  4. ¿Cuánto tiempo tardará el colaborador en responder?

Si conocemos al colaborador lo suficiente como para responder a todas estas preguntas, entonces es una decisión fácil de una forma u otra. De lo contrario, sugeriría comenzar con el colaborador real e intentar responder las cuatro preguntas sobre la marcha. En la práctica, este es el enfoque que la mayoría de los profesionales del desarrollo basado en pruebas (TDD) aplican en su trabajo diario. Significa que debe cuidar debidamente sus casos de prueba y revisarlos de cerca hasta que se estabilicen.

En la mayoría de los casos, el aislamiento de las pruebas unitarias se complica por las dependencias de la unidad que se está probando. Hay casos claramente deseables en los que se necesita burlarse de un componente dependiente, pero también situaciones más sutiles. En algunos casos, no está bien definido y depende del riesgo y la incertidumbre que tenga una dependencia en el entorno de prueba.

Cómo agilizar las pruebas unitarias para sistemas integrados y críticos para la seguridad
Foto de cabeza de Miroslaw Zielinski, gerente de producto de Parasoft

Por Miroslaw Zielinski

Gerente de producto para las soluciones de prueba integradas de Parasoft, las especialidades de Miroslaw incluyen C / C ++, RTOS, análisis de código estático, pruebas unitarias, gestión de la calidad del software para aplicaciones críticas de seguridad y cumplimiento del software con los estándares de seguridad.

Reciba las últimas noticias y recursos sobre pruebas de software en su bandeja de entrada.