Durante Octubre y Noviembre de 2022 tuve el enorme privilegio de asistir a los cursos sobre Test-Driven Development aplicados a embebidos que James W. Grenning ofrece en la actualidad desde su plataforma de aprendizaje. El siguiente post tiene como intención hacer una reseña de los mismos.
Grenning es firmante del manifiesto Ágil y autor del libro ‘Test-Driven Development for Embedded C'. Nunca me cansaré de decir que ese libro supuso un cambio radical tanto en mi vida profesional como personal. Si aún no lo has leído, te aconsejo encarecidamente lo hagas de inmediato.
En concreto me estoy refiriendo a las dos siguientes modalidades de formación:
Duración de 3 días consecutivos, a razón de 5h/día en vivo junto a James y el resto de participantes, más 2h/día adicionales de preparación previa de manera individual. El curso dispone de entregas cada pocos meses que se ajustan bastante bien tanto a horarios europeos (en España las sesiones empiezan a las 14h) así como a horarios ajustados a norte américa.
Estructura y contenido de los cursos
Ambos cursos poseen una estructura bastante similar. En ambas modalidades se utiliza la herramienta online Cyber-dojo para la ejecución de los ejercicios prácticos. La plataforma registra de manera automática el estado (rojo o verde) después de ejecutar los tests así como los cambios efectuados desde la anterior ejecución. Esta información permite de manera fácil y rápida al instructor conocer cuando alguien se está atascando así como conocer el motivo.
James dispone de su propio servidor de Cyber-dojo. En él, para pasar de Rojo a Verde, no solo habrá que hacer pasar el test que teníamos en fallo, sino que deberemos hacerlo con el mínimo código posible, puesto que de manera automática se comprueba la cobertura de código. Cualquier valor por debajo del 100% significa que hemos introducido más código de producción del necesario para hacer pasar los tests que existen hasta ese momento.
Escribir más código del necesario es uno de los errores más comunes al aplicar TDD, sobre todo cuando nos estamos inciando. Esta comprobación automática tiene como finalidad alertarnos cuando no estamos cumpliendo la primera y tercera de las leyes de TDD propuestas por Robert C. Martin, las cuales apareceran referenciadas varias veces a lo largo del curso:
1) No está permitido escribir código de producción si no es con el objetivo de hacer pasar un test previo en fallo
2) No está permitido escribir más código de test del necesario para provocar un fallo; los errores de compilación se consideran fallos
3) No está permitido escribir más código de producción del estrictamente necesario para hacer pasar el test en fallo
Estas tres reglas constituyen la implementación de Test-First bajo el paraguas de TDD. Su principal objetivo es construir una batería de especificaciones semánticamente estable o exhaustiva, que nos permita combatir desde el inicio, y a lo largo de las sucesivas etapas de refactoring, a nuesto enemigo público número uno: The Liar.
If you want fully tested code, do not write untested code.
La cobertura de código es una condición necesaria (aunque no suficiente) para asegurar la exhaustividad de nuestra batería de pruebas. Para mayor certidumbre, sería necesario aplicar Mutation Testing. Sin embargo, su ejecución tanto en C como en C++ puede resultar demasiado lenta como para incluir su comprobación automática dentro del microciclo de TDD. Con todo, dado que la cobertura de código es una condición necesaria y rápida de comprobar automáticamente, es una brillante idea el incluirla en nuestro bucle de feedback. De esa manera los asistentes se percatarán cuando están escribiendo código de más, pudiendo corregir rápidamente parte de sus hábitos adquiridos.
Toda la interactividad de las sesiones sucede dentro del Wingman Training Center. Es un entorno virtual basado en gather.town. James ha dispuesto en él diferentes espacios: un escenario (stage) donde lo que sucede en él es visto y oído por todos los asistentes, el Tiki Bar, que es una especie de sala de reuniones donde debatir conjuntamente las dudas que puedan surgir a un determinado grupo, así como las diferentes salas de trabajo desde las cuales trabajaremos por parejas los diferentes ejercicios prácticos.
Día 1.
El ejemplo a desarrollar durante este primer día es un CircularBuffer. Es un ejemplo excelente para ilustrar y experimentar el microciclo Red-Green-Refactor de TDD, así como los conceptos englobados en el acrónimo ZOMBIES, todo ello sin la complejidad añadida del uso de colaboradores.
Tras nuestra primera experiencia práctica con TDD, James explica como aplicarla de manera eficaz en el entorno de embebidos, mediante Dual-Target TDD y su implementación a través de las siguientes 5 etapas:
- Etapa 1: Microciclo de TDD en Host
- Etapas 2 - 3: Ejecutar tests unitarios en el Hardware de evaluación o Simulador
- Etapa 4: Ejecutar tests unitarios en el Hardware final
- Etapa 5: Ejecutar tests de Aceptación en el Hardware final
Posteriormente, se nos expone por primera vez al desarrollo evolutivo e incremental mediante una modificiación de los requerimientos iniciales que habíamos aplicado al CircularBuffer. Es en este momento cuando percibimos por primera vez que, si bien el primer beneficio que apreciamos es la reducción en el número de bugs, en realidad, el principal beneficio aportado por TDD es ser capaz de cambiar nuestro código con total libertad y seguridad.
Changing design is one of the main benefits of TDD
La sesión termina con un debrief, en el que se comparten las dudas e impresiones que los participantes han tenido al aplicar TDD por primera vez. En este momento James aprovecha para explicarnos cuales son las críticas más extendidas y cual es su opnión sobre ellas. Es una de las secciones del curso que me resultaron más interesantes. Voy a intentar matizar las que considero más importantes.
FALTA DE EXHAUSTIVIDAD DE LOS TEST
Una de las críticas que se achacan a TDD es que, dado que es una técnica de desarrollo y no de test, está orientada a incrementar funcionalidad, normalmente a través de happy-paths, que en cubrir todos los posibles corner-cases y sad-paths.
De hecho, algunos conocidos practicantes de TDD abogan abiertamente por esta aproximación, delegando el tratamiento de los sad-paths a otro tipo de aproximación Test-Later y, por tanto, bajo un bucle de feedback mucho más lento.
En realidad no hay motivo para que esto se así. Uno de los beneficios de TDD es poder centrarnos en una funcionalidad a la vez, independientemente de con qué flujo de ejecución, happy o sad, estemos trabajando. Además, ninguna técnica de desarrollo basada en Test-Later es capaz de trazar sin ambiguedad la causa-efecto de nuestro código (aquí un estudio sobre los efectos de Test-Later en un entorno de seguridad crítica).
Coincido plenamente con James en el hecho de que seguir las 3 reglas de TDD guiados por ZOMBIES es la mejor manera que conocemos en la actualidad para cubrir todos los escenarios posibles, incluídos los sad-paths, de manera iterariva e incremental.
MENOR VELOCIDAD DE DESARROLLO
La crítica más común y posiblemente la más disuasoria, es la que afirma que se necesita más tiempo en implementar una determinada funcionalidad siguiendo TDD que sin TDD. Aquí es importante que maticemos que significa realmente 'sin TDD'. ¿Nos estamos refiriendo a Test-Never o Test-Later?
1) Test-Never
Significa entregar código de producción sin haber realizado ningún tipo de test automatizado. En el mejor de los casos, se habrán hecho algunas pruebas manuales durante y/o tras haber desarrollado el código de producción (recuerdo con ninguna nostalgia aquellos prints en DEBUG).
If it doesn't have to work, i can get it done a lot faster
Entregar algo no significa que funcione o esté terminado. Cualquier velocidad de desarrollo extraída de una Definición de Completado deficiente o incompleta es directamente una mentira. Esto debería ser suficiente para descartar/abandonar esta aproximación al desarrollo profesional de software. Sin embargo, no es para nada vestigial en nuestro sector, así que desgranémosla un poco más.
Si seguimos esta filosofía, para evitar regresiones, en cada nueva iteración o cambio, vamos a necesitar volver a testear manualmente tanto el código nuevo como el que ya habíamos 'probado' o dado por bueno en la iteración anterior. La secuencia temporal suele ser la siguiente:
- El tiempo de test manual crece exponencialmente, incrementando el tiempo de entrega.
- Cada vez empleamos una mayor parte de ese tiempo en el debugger.
- Aparecen partes del código que son innaccesibles o muy complicadas de ejercitar.
- Empezamos a tomar atajos y acabamos abriendo la puerta a The Liar.
Entramos en una espiral en la que nos encontramos más tiempo corrigiendo bugs que creando funcionalidad nueva.
Es importante matizar que en el razonamiento anterior únicamente estamos considerando la calidad externa. En ningún momento se ha tenido en cuenta del tiempo asociado al coste de oportunidad; esto es, el coste asociado a cada una de las oportunidades perdidas de replantearnos, iterativamente, la mejora de la calidad interna (reduciendo su complejidad accidental y mejorando su diseño y arquitectura), gracias al bucle de feedback corto que proporciona TDD.
En definitiva, esta opción únicamente sería válida si fuésemos capaces de desarrollar la solución requerida en una única iteración, sin introducir bugs y siempre que no se tuviera que cambiar el código en el futuro. Todos deberíamos reonocer que las anteriores condiciones jamás se cumplen.
"You've been down there, Neo.
You already know that road.
You know exactly where it ends.
And I know that's not where you want to be"
Aquellos que hemos vivido en nuestras carnes la parálisis debida a tener que estar apagando fuegos contínuamente sabemos perfectamente cómo y dónde acaba ese camino.
2) Test-Later
En este caso si hay tests automatizados, con lo que el coste de volver a testear ya no crece exponencialmente. Sin embargo, no nos ofrece feedback contínuo sobre la solución y decisiones que estamos adoptando (tanto de test como producción). Esta falta de realimentación conduce a problemas cuyas consecuencias son similares a las que encontrábamos con Test-Never, como son:
James ha hecho ensayos interesantes con particiapantes en su curso. Dividió a los participantes en dos grupos. Ambos desarrollarían por primera vez un módulo, cuya interfaz pública se les proporcionaba. La definición de completado se lograba cuando se cumplieran ciertos tests de aceptación funcionales (calidad externa) que James tenía definidos.
Un primer grupo desarrollaría el módulo con TDD. Un segundo grupo con Test-Later. Resultó que ambos grupos tardaron de media lo mismo en completar la funcionalidad requerida.
¿Puede significar esto que desarrollar con TDD es igual de rápido que hacerlo con Test-Later (al menos para una primera versión de un producto)?
La respuesta es sí. Aunque la igualdad es cierta únicamente cuando los desarrolladores no están familiarizados con TDD. A medida que se tiene mayor dominio de la práctica este tiempo se reduce, y se acaba siendo más rápido con TDD que con Test-Later ya desde la primera iteración.
Esto coincide plenamente con mi experiencia y con la de aquellos que conozco que practican la técnica con el rigor adecuado. Solía razonar que TDD es más rápido porque no desperdiciamos tanto tiempo en debug. Aún siendo cierto, la verdad es que aún si fuésemos capaces de eliminar el cuello de botella del debug, Test-Later seguiría siendo más lento para alguien con las horas de vuelo suficientes con TDD, ya desde la primera iteración, donde los efectos de tener que lidiar con la complejidad accidental acumulada aún no se han hecho patentes.
Día 2
Los ejemplos de esta sesión giran alrederdor del desarrollo de un Planificador de Control de Luces (LighScheduler el cual también aparece en el libro). Durante está sesión, se introducen los dobles de prueba, principalmente Spies y Stubs, como mecanismo para controlar las dependencias con las que colaborará nuestro Subject Under Test (SUT) en el entorno de test.
El uso de dichos dobles de prueba no solo nos permitirá controlar las entradas indirectas y capturar el comportamiento deseado de a la salida del SUT para todos aquellos casos que contemplemos, sino también hacerlo con repetibilidad y rapidez, en un ciclo de pocos segundos.
Se sigue de nuevo la aproximación a la resolución del problema guiados por ZOMBIES, mientras cumplimos con las 3 reglas de TDD anteriormente descritas.
Por último se aborda la temática del mantenimiento y refactorización de los tests. Se hace énfasis en que cada test o especificación debe ser un pequeño microuniverso independiente del resto. En ellos debemos aplicar los mismos principios aplicables al código de producción para independizarlo de detalles de implementación (SOLID, Design Principles...) con la diferencia de que en este caso debe primar DAMP por encima de DRY.
En los ejercicios de BONUS podemos volver a vivir la experiencia de evolucionar nuestra solución con la red de seguridad que hemos tejido. Vuelve a reforzarse el auténtico objetivo de TDD visto en el Día 1: ser capaces de cambiar de opinión y evolucionar la solución de manera incremental a medida que conocemos más detalles, ya no del mapa, sino del propio territorio de la solución.
Día 3.
En esta última sesión del curso nos aproximamos al silicio, al hardware, mediante la implementación de un driver/controlador de memoria Flash.
En este caso haremos uso de un nuevo y seductor doble de prueba, el Mock, usando concretamente el framework CppUMock para ello. Nuestro Mock suplantará la interacción de nuestro SUT con el hardware, concretamente la interacción de las lecturas y escrituras en los registros especiales del driver.
Los Mocks son fáciles de usar una vez se entiende como funcionan. Esto les hace especialmente apetecibles. Sin embargo, tienen un lado oscuro, tal y como nos advirtió James hasta en cuatro ocasiones. Saben mucho (a veces demasiado) sobre cómo nuestro SUT interactúa con sus dependencias. Esto puede resultar muy beneficioso cuando las especificaciones están escritas en piedra, aunque esta sea de silicio, como es el caso de las especifiaciones contempladas en las hojas de características de los Circuitos Integrados o datasheets, donde tenemos que interactuar en un orden y secuencias concretos. En el resto de situaciones, ¡mucho cuidado con los Mocks!.
Aunque ya se ha ido introduciento en las anteriores sesiones, en esta última James realiza una demo centrada en las técnicas de Refactoring, en la cual expande, divierte para posteriormente contraer el código, con el objetivo de acabar acomodando un nuevo cambio que impactaba en la API pública de nuestro módulo. Dado que el refactoring es un tema de importancia capital dentro del diseño y desarrollo evolutivo e incremental, James también dispone de un curso íntegro sobre refactoring en C++.
You must slow down to be fast
Como despedida, se nos presentan algunas técncias para lidiar con Código Legado y como lograr emplazarlo bajo el entorno de test siguiendo baby steps, así como el 'algoritmo' automatizado que James ha bautizado como Crash To Pass. Al igual que sucede con las técncias de refactoring, dispone de un workshop sobre Código Legado.
En este caso, tanto el trabajo como los propios ejercicios prácticos están dispuesto de manera incremental, apoyados por material audiovisual, en el que se simula que estés programando en pareja con él.
Aún siendo a tu ritmo, James lo ha dividido de manera orientativa en 4 semanas, quedando de la siguiente manera:
Week 1 -- Module 1 -- Your first TDD
Tiene una correspondencia prácticamente directa con la primera sesión del Curso en Vivo descrita anteriormente
Week 2 -- Module 2 -- TDD and code with Dependencies
Tiene una correspondencia prácticamente directa con la segunda sesión del Curso en Vivo descrita anteriormente
Week 3 -- Module 3 -- Test Doubles and Mocking the Hardware
Week 4 -- Module 4 -- Refactoring Legacy Code
Estos dos módulos se corresponden en contenidos con los tratados en la tercera sesión del Curso en Vivo.
¿Valen la pena? ¿Incluso si ya he leído su libro?
¡Rotundamente sí! Me explico.
Vengo aplicando TDD en mi trabajo diario e impartiendo formación sobre la misma desde 2016. Aún así, cada vez que leo de nuevo su libro (voy por la cuarta lectura ahora mismo) desvelo un nuevo e importante detalle que me había pasado desapercibido. Por desgracia para mí, muchos de esos detalles los he tenido que aprender a base de ensayo y error a lo largo de estos años. Por suerte, he de decir que siempre he sometido a juicio tanto la propia técnica como mi aproximación a la misma. Este último punto ha sido crucial para no haber desistido. Es curioso descubrir como, una vez entiendes cual era el problema subyacente y le encuentras una solución, cuando vuelves a leer el libro compruebas que, la mayoría de las veces, la respuesta ya estaba allí, de alguna manera esperándote... esperando que tu nivel de conocimiento fuese el suficiente como para entender las implicaciones de ese importante detalle aplicado en el contexto adecuado.
Acompañar la lectura del libro con uno de estos cursos puede transformar esos años en meses, eliminando la probabilidad de abandono debido al desconocimiento inconsciente. Supone pues una vetaja competitiva, con un retorno de la inversión difícil de mejorar.
En mi opinión, para sacarle todo el juego al Curso en Vivo, es mejor haber leído antes el libro. De esta manera podrás preguntar directamente todas las dudas que tengas a James. El nivel de detalle de sus explicaciones es tan amplio que, no solo te resolverá esa duda, sino que te desbloqueará el empezar a pensar en la siguiente.
En el caso de la modalidad de Curso a tu Ritmo; creo que, si dispones del tiempo suficiente, puedes organizarte la lectura del libro y la ejecución del curso en paralelo. James ha establecido un camino de aprendizaje en el que simula perfectamente el estar a tu lado, tanto mediante el desarrollo guiado del código como si estuviérais programando en parejas, así como con material audiovisual y demostraciones. Además, si aún así hay algo no te queda claro o tienes alguna duda nueva, siempre tienes la opción de preguntarle dudas desde la propia plataforma.
Qué aprenderás
Una manera de desarrollar código embebido iterativa e incremental, más rápida, segura, de mayor calidad y a un ritmo sostenido. Todo ello gracias a un proceso de desarrollo basado en un ciclo de feedback extremadamente corto (menor del minuto).
It's easier to keep a system working than to fix it after you break it
Puede que los conceptos básicos de TDD sean sencillos de entender. Sin embargo, los importantes detalles que se esconden detrás de cada una de sus fases y sus respectivas implicaciones, no son para nada inmediatas de anticipar. Por experiencia propia y por lo que suelo escuchar y leer, la inmensa mayoría de la gente que dice haber intentado seguir TDD y ha acabado abandonando no ha llegado a aplicar TDD correctamente (casi siempre el fallo está en no conocer las verdaderas implicaciones de la etapa de Refactor).
Con James-In-The-Loop aprenderás de manera progresiva todos los recovecos que suelen pasar desapercibidos y cuyo conocimiento te llevará a una correcta aplicación de TDD, de manera guiada, rápida y eficaz.
Conclusión
La inmensa mayoría de organizaciones responsables de la formación de los futuros desarrolladores de software, sea este embebido o no, no incluyen en su currículo el estudio y aplicación de práctica técnica alguna focalizada en posibilitar un diseño y desarrollo evolutivo e incremental.
Por desgracia, según mi experiencia, la de muchos colegas, y tal y como respaldan varios estudios, tampoco parece existir una masa crítica de desarrolladores con experiencia suficiente sobre dichas prácticas técnias dentro en las empresas. En los sectores de embebidos en particular, donde mayores beneficios aporta su aplicación, la situación parece, irónicamente, incluso peor.
Es por ello que las posibilidades de que nuevos desarrolladores tengan la oportunidad de conocer y analizar con datos objetivos enfoques de desarrollo opuestos al tradicional o Cascada, bien durante su formación académica o bien durante su experiencia laboral, son desgraciadamente muy bajas.
I can't convince you to use TDD. you have to convince yourself
¿Qué hacer entonces?
James Grenning lleva desde 1999 aplicando técnicas propias de la Programación Extrema (XP) al mundo de embebidos, así como enseñando a otros cómo hacerlo. Como él, muchos creemos que es absolutamente necesario modernizar la manera en la que desarrollamos software y, más concretamente, software embebido. Dudo que haya mejores maneras de empezar el camino en esta apasionante manera de entender el desarrollo que de la mano del maestro James Grenning.
Por último y no menos importante, quiero agradecer profundamente a James el haberme invitado a saborear estas experiencias de aprendizaje tan brutales. He podido aprender aspectos nuevo y comprobar, con profunda satisfacción, como he convergido a enfoques y soluciones muy similares a las suyas, tanto en la manera de desarrollar cómo en la manera en la que las intentamos trasladar en nuestras respectivas formaciones.
¡Gracias James por aportar tanta luz a nuestras embebidas penumbras!