unit-testing embedded drivers

unit testing - Unidad de prueba de controladores de dispositivos



unit-testing embedded (6)

Tengo una situación en la que necesito escribir algunas pruebas unitarias para algunos controladores de dispositivos para hardware incorporado. El código es bastante antiguo y grande y, lamentablemente, no tiene muchas pruebas. En este momento, el único tipo de prueba que es posible es compilar completamente el sistema operativo, cargarlo en el dispositivo, usarlo en escenarios de la vida real y decir que ''funciona''. No hay manera de probar los componentes individuales.

Encontré un buen hilo aquí que discute las pruebas de unidad para dispositivos integrados de los cuales obtuve mucha información. Me gustaría ser un poco más específico y preguntar si alguien tiene "mejores prácticas" para probar los controladores de dispositivos en tal escenario. No espero poder simular ninguno de los dispositivos con los que la placa en cuestión está hablando y, por lo tanto, es probable que tenga que probarlos en el hardware real.

Al hacer esto, espero poder obtener datos de cobertura de pruebas unitarias para los controladores y persuadir a los desarrolladores para que escriban pruebas para aumentar la cobertura de sus controladores.

Una cosa que se me ocurre es escribir aplicaciones incrustadas que se ejecutan en el sistema operativo y ejercer el código del controlador y luego comunicar los resultados al arnés de prueba. El dispositivo tiene un par de interfaces que puedo usar para probablemente manejar la aplicación desde mi PC de prueba para que pueda ejercer el código.

Cualquier otra sugerencia o conocimiento sería muy apreciado.

Actualización: si bien puede que no sea una terminología exacta, cuando digo pruebas de unidad, me refiero a poder probar / ejercitar código sin tener que compilar todos los controladores OS + y cargarlos en el dispositivo. Si tuviera que hacer eso, lo llamaría integración / prueba del sistema.

El problema es que las piezas de hardware que tenemos son limitadas y los desarrolladores las utilizan a menudo para corregir errores, etc. Para mantener un servidor dedicado y conectado a la máquina donde se realizan las pruebas automatizadas y el servidor de CI puede ser un no, no. este escenario. Es por eso que estoy buscando formas de probar el controlador sin tener que compilarlo todo y cargarlo en el dispositivo.

Resumen

Basándome en las excelentes respuestas a continuación, creo que una manera razonable de abordar el problema sería exponer la funcionalidad del controlador utilizando IOCTL y luego escribir pruebas en el espacio de la aplicación del dispositivo integrado para ejercer realmente el código del controlador.

También tendría sentido tener un pequeño programa que reside en el espacio de la aplicación en el dispositivo que expone una API que puede ejercer el controlador a través de serie o USB para que la prueba de la unidad se pueda escribir en una PC que se comunique con el Hardware y ejecutar la prueba.

Si el proyecto recién comenzara, creo que tendríamos más control sobre la forma en que se aíslan los componentes para que las pruebas puedan realizarse principalmente a nivel de PC. Dado que la codificación ya está hecha y estamos tratando de adaptar el arnés y los casos de prueba al sistema, creo que el enfoque anterior es más práctico.

Gracias a todos por sus respuestas.


Vocabulario

No espero poder simular ninguno de los dispositivos con los que la placa en cuestión está hablando y, por lo tanto, es probable que tenga que probarlos en el hardware real.

Entonces, estás saliendo de las pruebas unitarias. Tal vez podrías usar una de estas expresiones en su lugar?

  • Pruebas automatizadas : las pruebas se realizan sin la intervención del usuario (a diferencia de las Pruebas manuales ).
  • Pruebas de integración : probando varios componentes juntos (al contrario de las pruebas unitarias ).
    En una escala más grande, si prueba un sistema completo y no solo algunos componentes juntos, se denomina Pruebas del sistema .

AGREGADO después de comentarios y actualizaciones en la pregunta:

  • Pruebas de componentes : como pruebas de integración o pruebas del sistema, pero a una escala aún menor.
    Nota: Las tres pruebas del sistema de integración de componentes comparten el mismo conjunto de problemas, en diferentes escalas. Por el contrario, la Prueba unitaria no (ver más abajo).

Ventajas de la prueba de unidad "real"

Con las pruebas de integración (o sistema o componente), es ciertamente interesante obtener algunos comentarios, como la cobertura de pruebas. Ciertamente es útil hacerlo.

Pero es muy difícil (lea "muy costoso") avanzar más allá de algún punto, así que
Le sugiero que use enfoques complementarios, como agregar algunas Pruebas Unitarias reales . ¿Por qué? :

  • Es muy difícil simular las condiciones de borde o error . (Ejemplos: el reloj de la computadora cruza un día o un año durante una transacción; el cable de red está desenchufado; la alimentación se apagó en algún componente o todo el sistema; el disco está lleno). Usando Unit Testing, porque simulas estas condiciones en lugar de intentar reproducirlas, es mucho más fácil. Unit Test es su única oportunidad de obtener una buena cobertura de código.
  • Las pruebas de integración llevan tiempo (debido al acceso a recursos externos). Podría ejecutar miles de pruebas de unidad durante la ejecución de una prueba de integración. Así que probar muchas combinaciones solo es posible con Pruebas Unitarias ...
  • Al requerir acceso a recursos específicos (hardware, licencia, etc.), las pruebas de integración a menudo están limitadas en tiempo o escala . Si los recursos son compartidos por otros proyectos, cada proyecto puede usarlos solo durante unas pocas horas por día. Incluso con acceso exclusivo, tal vez solo una máquina puede usarlo, por lo que no puede ejecutar pruebas en paralelo. O, su compañía puede comprar un recurso (Licencia o Hardware) para producción, pero no tenerlo (o lo suficientemente temprano) para desarrollo ...

El código que realmente depende del hardware (el nivel más bajo de la pila de controladores en una arquitectura en capas) no puede probarse en ninguna parte, excepto en el hardware, o en una simulación de alta calidad del hardware.

Si su controlador tiene algún componente de funcionalidad de nivel superior que no se basa directamente en el hardware (por ejemplo, un controlador de protocolo para enviar mensajes al hardware en un formato particular) y si esa parte está bien autocontenida en el código, entonces puede realizar una prueba de unidad por separado en un marco de prueba de unidad basado en PC.

Volviendo al nivel más bajo, si depende del hardware, entonces la plantilla de prueba debe incluir el hardware. Puede hacer una plantilla de prueba que incluya el hardware, el controlador y algún software de prueba. Lo principal, creo, es sacar el código de aplicación del producto normal de la prueba y poner un código de prueba en su lugar. El código de prueba puede probar sistemáticamente todas las características del controlador y los casos de esquina (que el código de la aplicación no puede), y también puede golpear al conductor de manera intensiva en un corto período de tiempo (lo que probablemente la aplicación no). Por lo tanto, es un uso más eficiente de su hardware limitado que solo ejecutar la aplicación y le brinda mejores resultados.

Si puede poner una PC en el bucle, entonces la PC podría ayudar con las pruebas. Por ejemplo, si está escribiendo un controlador de puerto serie para un dispositivo integrado, entonces podría:

  • Escriba el código de prueba para el dispositivo integrado que envía varios flujos de datos conocidos.
  • Conéctelo al puerto serie de una PC, ejecutando el código de prueba que verifica los flujos de datos transmitidos.
  • Lo mismo en la otra dirección: la PC envía datos; El dispositivo incorporado lo recibe y lo verifica, y notifica a la PC cualquier error.
  • Las pruebas pueden transmitir datos a toda velocidad y jugar con un rango de diferentes temporizaciones de bytes (una vez encontré un error de silicio UART en el microcontrolador que solo aparecía si los bytes se enviaban con un retraso de ~ 5 ms entre bytes).

Se podría hacer algo similar con un controlador de Ethernet, un controlador de Wi-Fi.

Si está probando un controlador de dispositivo de almacenamiento, como para un chip EEPROM o Flash, entonces la PC no podría involucrarse de la misma manera. En ese caso, su arnés de prueba podría probar todo tipo de condiciones de escritura (un solo byte, bloque ...) y verificar la integridad de los datos utilizando todo tipo de condiciones de lectura.


En los viejos tiempos, así era como probábamos y depurábamos los controladores de dispositivos. La mejor manera de depurar un sistema de este tipo era que los ingenieros usaran el sistema integrado como un sistema de desarrollo y, una vez que alcanzaran la madurez adecuada del sistema, ¡eliminen el sistema de desarrollo cruzado original!

Para su situación, varios enfoques vienen a la mente:

  • Agregue controladores ioctl: cada código ejerce una prueba de unidad en particular
  • Con la compilación condicional, agregue un main () al controlador que realiza pruebas de unidades funcionales en el controlador y envía los resultados a la salida stdout .
  • Para facilitar la depuración inicial, tal vez esto se pueda convertir en multiplataforma operable para que no tenga que depurar en el hardware de destino.
  • Quizás el código condicional también puede emular un dispositivo estilo loopback.

Lo recomendaría para pruebas basadas en aplicaciones. Incluso si los andamios pueden ser difíciles y costosos de construir, hay mucho que ganar aquí:

  • Falla solo una vez el proceso en lugar de un sistema
  • Capacidad de usar el conjunto de herramientas estándar (depurador, verificador de memoria ...)
  • superar la limitación de disponibilidad de hardware
  • Retroalimentación más rápida: no hay instalación en el dispositivo, solo compile y pruebe
  • ...

En lo que respecta a la denominación, esto puede denominarse prueba de componentes.

La aplicación puede inicializar el controlador del dispositivo de la misma manera que lo hace el sistema operativo de destino, o usar directamente los internos del controlador. El primero es más caro pero lleva a más cobertura. Luego, el enlazador le dirá qué funciones faltan, las aplastará, posiblemente utilizando apéndices explosivos .


Tuve esta tarea exacta hace apenas dos meses.

Déjeme adivinar: probablemente tenga "fragmentos" de código que hablan detalles de bajo nivel al dispositivo. Usted sabe que estos fragmentos funcionan, pero no puede obtener cobertura porque tienen una dependencia de los controladores del dispositivo.

Del mismo modo, no tiene sentido probar cada línea individualmente. Nunca se ejecutan de forma aislada, y su prueba de unidad terminaría pareciéndose a un reflejo del código de producción. Por ejemplo, si desea iniciar el dispositivo, necesita crear una conexión, pasarle un comando de reinicio de bajo nivel específico, luego inicializar una estructura de parámetros, etc. Y si necesita agregar una parte de la configuración, esto puede requerir para desconectarlo, agregue la configuración y luego en línea. Cosas como esas.

Usted NO quiere probar cosas de bajo nivel. Sus pruebas de unidad solo reflejarán cómo asume que el dispositivo funciona sin confirmar nada.

La clave aquí es crear tres elementos: un controlador, una abstracción y una implementación de adaptador de esa abstracción. En Cpp, Java o C #, crearía una clase base o una interfaz para representar esta abstracción. Asumiré que creaste una interfaz. Rompes los fragmentos en operaciones atómicas. Por ejemplo, crea un método llamado "inicio" y "agregar (parámetro)" en la interfaz. Pones tus fragmentos en el adaptador del dispositivo. El controlador actúa sobre el adaptador a través de la interfaz.

Identifique piezas de lógica dentro de los fragmentos que ha colocado en el adaptador. Luego debe decidir si esta lógica es de bajo nivel (detalles de manejo del protocolo, etc.) o si esta es la lógica que debe pertenecer al controlador.

Luego puede probar en dos etapas: * Tenga una aplicación de panel de prueba simple que actúe sobre el adaptador de concreto. Esto se utiliza para confirmar que el adaptador realmente funciona. Que comienza cuando presionas "start". Eso, por ejemplo, si presiona "desconectarse", "transmitir (192)" y "conectarse en línea" en secuencia, el dispositivo responde como se esperaba. Esta es tu prueba de integración.

Usted no prueba por unidad los detalles en el adaptador. Lo prueba manualmente porque el único criterio de éxito es cómo responde el dispositivo.

Sin embargo, el controlador está completamente probado. Solo tiene una dependencia de la abstracción, que se burla de su código de prueba. Por lo tanto, su código no depende del controlador de su dispositivo porque el adaptador concreto no está involucrado.

Luego, escribe pruebas unitarias para confirmar que, por ejemplo, el método "Agregar (1)" en realidad invoca "Desconectarse", luego "Transmitir (1)" y luego "Conectarse" en la abstracción simulada.

El desafío aquí es trazar la distinción entre el adaptador y el controlador. Que va a donde Lo que me funcionó fue crear el panel de prueba mencionado anteriormente y luego manipular el dispositivo a través de él.

El adaptador debe ocultar los detalles que solo tendrá que cambiar si el dispositivo cambia.

  1. Si el panel de control es incómodo de operar con muchas secuencias que deben repetirse una y otra vez, o si se requiere el conocimiento específico del dispositivo para operar el panel, tiene una granularidad demasiado alta y debería agrupar algunas de ellas. El panel de prueba debe tener sentido.

  2. Si los cambios en los requisitos del usuario final tienen un impacto en el código del adaptador, es probable que tenga una granularidad demasiado baja y que deba dividir las operaciones, de modo que los cambios en los requisitos puedan adaptarse al desarrollo impulsado por pruebas en la clase de controlador.


Tuve un problema similar hace dos o tres años. He portado un controlador de dispositivo de VxWorks a Integrity. Habíamos cambiado solo las partes dependientes del sistema operativo del controlador, pero era un proyecto crítico para la seguridad, por lo que todas las pruebas de unidad, las pruebas de integración se rehacieron. Hemos utilizado una herramienta de prueba automatizada llamada banco de pruebas LDRA para nuestras pruebas de unidad. El 99% de nuestras pruebas unitarias se realizan en máquinas Windows con compiladores de Microsoft. Ahora te explico como hacer esto

Bueno, primero que todo, cuando estás haciendo pruebas unitarias estás probando un software. Cuando incluye el dispositivo real en sus pruebas, también está probando el dispositivo. A veces puede haber problemas con el hardware o la documentación del hardware. Cuando está diseñando el software, si ha descrito claramente el comportamiento de cada función, es muy fácil realizar pruebas de unidad, por ejemplo, Piense en la función;

readMessageTime(int messageNo, int* time); //This function calculates the message location, if the location is valid, //it reads the time information address=calculateMessageAddr(messageNo); if(address!=NULL) { read(address+TIME_OFFSET,time); return success; } else { return failure; }

Bueno, aquí solo estás probando si readMessageTime está haciendo lo que se supone que debe hacer. No tiene que probar si calculaMessageAddr está calculando el resultado correcto o, lee lee la dirección correcta. Esa es la responsabilidad de algunas otras pruebas de unidad. Entonces, lo que debe hacer es escribir apéndices para calcularMessageAddr y leer (función del sistema operativo) y verificar si llama a las funciones con los parámetros correctos. Este es el caso si no está accediendo a la memoria directamente desde su controlador. Puede probar cualquier tipo de código de controlador sin ningún sistema operativo o dispositivo con esta mentalidad.

Si ha asignado la memoria del dispositivo directamente en su espacio de memoria y el controlador del dispositivo lee y escribe en la memoria del dispositivo, ya que es su propia memoria, se complica un poco. Usando herramientas de prueba automatizadas, ahora tiene que ver los valores de los punteros y definir los criterios de paso / caída de acuerdo con los valores de estos punteros. Si está leyendo un valor de la memoria, debe definir el valor esperado. Esto puede ser difícil en algunos casos.

También hay un problema más, los desarrolladores siempre confunden en las pruebas unitarias de controladores como:

readMessageTime(int messageNo, int* time); //This function calculates the message location, if the location is valid, //it does some jobs to make the device ready to read then //it reads the time information address=calculateMessageAddr(messageNo); if(address!=NULL) { do_smoething(); // Get the device ready to read! do_something_else() // do some other stuff so you can read the result in 3us. status=NOT_READY; while(status==NOT_READY) // mustn''t be longer than 3us. status=read(address+TIME_OFFSET,time); return success; } else { return failure; }

Aquí do_something y do_something_else hacen algunos trabajos en el dispositivo para que esté listo para leer. Los desarrolladores siempre se preguntan "¿Qué pasa si el dispositivo no se prepara para siempre y mi código tiene un punto muerto aquí?" Y tienden a probar este tipo de cosas en el dispositivo.

Bueno, tienes que confiar en el fabricante del dispositivo y el autor técnico. Si están diciendo que el dispositivo estará listo en 1-2us, no tiene que preocuparse por esto. Si su código falla aquí, debe informarlo al fabricante del dispositivo, no es su trabajo encontrar una solución para superar este problema. ¿Viste mi punto?

Espero que esto ayude….