Entrar en cualquier empresa nueva -con una cultura y unas prácticas de programación establecidas- puede ser una experiencia desalentadora. Cuando me uní al equipo de Ansible, decidí escribir las prácticas y principios de ingeniería de software que he aprendido a lo largo de los años y con los que me esfuerzo por trabajar. Se trata de una lista no definitiva y no exhaustiva de principios que deben aplicarse con sabiduría y flexibilidad.
Mi pasión son las pruebas, ya que creo que las buenas prácticas de pruebas pueden tanto asegurar un estándar mínimo de calidad (tristemente ausente en muchos productos de software), como guiar y dar forma al propio desarrollo. Muchos de estos principios están relacionados con las prácticas e ideales de las pruebas. Algunos de estos principios son específicos de Python, pero la mayoría no lo son. (Para los desarrolladores de Python, PEP 8 debe ser su primera parada para el estilo de programación y las directrices.)
En general, los programadores somos un montón de opiniones, y las opiniones fuertes son a menudo un signo de gran pasión. Con eso en mente, siéntete libre de estar en desacuerdo con estos puntos, y podemos discutirlos y debatirlos en los comentarios.
Mejores prácticas de desarrollo y pruebas
1. YAGNI: «You Aint Gonna Need It». No escribas código que crees que podrías necesitar en el futuro, pero que aún no necesitas. Esto es codificar para casos de uso futuros imaginarios, e inevitablemente el código se convertirá en código muerto o necesitará ser reescrito porque el caso de uso futuro siempre resulta funcionar de forma ligeramente diferente a como lo imaginaste.
Si pones código para un caso de uso futuro, lo cuestionaré en una revisión de código. (Puedes, y debes, diseñar APIs, por ejemplo, para permitir casos de uso futuros, pero ese es un tema diferente.)
Lo mismo ocurre con el código comentado; si un bloque de código comentado va a entrar en una versión, no debería existir. Si es código que puede ser restaurado, haz un ticket y haz referencia al hash del commit para la eliminación del código. YAGNI es un elemento central de la programación ágil. La mejor referencia para esto es Extreme Programming Explained, de Kent Beck.
2. Las pruebas no necesitan pruebas. La infraestructura, los frameworks y las librerías para las pruebas necesitan pruebas. No pruebes el navegador o las bibliotecas externas a menos que realmente lo necesites. Prueba el código que escribes, no el de otras personas.
3. La tercera vez que escribes el mismo trozo de código es el momento adecuado para extraerlo en un helper de propósito general (y escribir pruebas para él). Las funciones de ayuda dentro de una prueba no necesitan pruebas; cuando las separas y las reutilizas sí necesitan pruebas. Para la tercera vez que has escrito un código similar, sueles tener una idea clara de la forma del problema de propósito general que estás resolviendo.
4. Cuando se trata del diseño de la API (de cara al exterior y de objetos): Lo simple debe ser simple; lo complejo debe ser posible. Diseña para el caso simple primero, con preferiblemente cero configuración o parametrización, si eso es posible. Añade opciones o métodos adicionales de la API para casos de uso más complejos y flexibles (a medida que sean necesarios).
5. Fallar rápidamente. Compruebe la entrada y falle en la entrada sin sentido o el estado no válido tan pronto como sea posible, preferiblemente con una excepción o respuesta de error que deje claro el problema exacto a su llamador. Sin embargo, permita casos de uso «innovadores» de su código (es decir, no haga la comprobación de tipo para la validación de entrada a menos que realmente lo necesite).
6. Las pruebas unitarias prueban la unidad de comportamiento, no la unidad de implementación. Cambiar la implementación, sin cambiar el comportamiento ni tener que cambiar ninguna de tus pruebas es el objetivo, aunque no siempre es posible. Así que siempre que sea posible, trata tus objetos de prueba como cajas negras, probando a través de la API pública sin llamar a los métodos privados o juguetear con el estado.
Para algunos escenarios complejos -como probar el comportamiento en un estado complejo específico para encontrar un error oscuro- eso puede no ser posible. Escribir primero las pruebas realmente ayuda con esto, ya que te obliga a pensar en el comportamiento de tu código y cómo vas a probarlo antes de escribirlo. Probar primero fomenta la creación de unidades de código más pequeñas y modulares, lo que generalmente significa un mejor código. Una buena referencia para empezar con el enfoque de «probar primero» es Test Driven Development by Example, de Kent Beck.
7. Para las pruebas unitarias (incluyendo las pruebas de infraestructura de pruebas) todos los caminos del código deben ser probados. Una cobertura del 100% es un buen punto de partida. No puedes cubrir todas las posibles permutaciones/combinaciones de estado (explosión combinatoria), así que eso requiere consideración. Sólo si hay una muy buena razón debería dejarse sin probar las rutas de código. La falta de tiempo no es una buena razón y acaba costando más tiempo. Entre las posibles buenas razones se encuentran: que no se pueda probar de verdad (de forma significativa), que sea imposible acertar en la práctica o que se cubra en otra parte de una prueba. El código sin pruebas es un lastre. Medir la cobertura y rechazar los PRs que reduzcan el porcentaje de cobertura es una forma de asegurar que se avanza gradualmente en la dirección correcta.
8. El código es el enemigo: puede salir mal, y necesita mantenimiento. Escriba menos código. Elimine código. No escribas código que no necesites.
9. Inevitablemente, los comentarios del código se convierten en mentiras con el tiempo. En la práctica, poca gente actualiza los comentarios cuando las cosas cambian. Esfuércese por hacer que su código sea legible y autodocumentado a través de buenas prácticas de nomenclatura y de un estilo de programación conocido.
El código que no puede hacerse obvio-trabajando en torno a un error oscuro o una condición improbable, o una optimización necesaria- necesita comentarios. Comenta la intención del código, y por qué está haciendo algo en lugar de lo que está haciendo. (Por cierto, este punto en particular sobre las mentiras de los comentarios es controvertido. Sigo pensando que es correcto, y Kernighan y Pike, autores de The Practice of Programming, están de acuerdo conmigo.)
10. Escribe a la defensiva. Piensa siempre en lo que puede salir mal, en lo que ocurrirá con una entrada no válida y en lo que puede fallar, lo que te ayudará a detectar muchos bugs antes de que se produzcan.
11. La lógica es fácil de probar por unidades si no tiene estado y está libre de efectos secundarios. Divida la lógica en funciones separadas, en lugar de mezclar la lógica en el código lleno de estado y efectos secundarios. Separar el código con estado y el código con efectos secundarios en funciones más pequeñas hace que sea más fácil burlarse de ellas y realizar pruebas unitarias sin efectos secundarios. (Menos sobrecarga para las pruebas significa pruebas más rápidas.) Los efectos secundarios necesitan pruebas, pero probarlos una vez y burlarse de ellos en todas las demás partes es generalmente un buen patrón.
12. Los globales son malos. Las funciones son mejores que los tipos. Los objetos son probablemente mejores que las estructuras de datos complejas.
13. Usar los tipos incorporados de Python -y sus métodos- será más rápido que escribir tus propios tipos (a menos que estés escribiendo en C). Si el rendimiento es una consideración, trate de averiguar cómo utilizar los tipos estándar incorporados en lugar de objetos personalizados.
14. La inyección de dependencias es un patrón de codificación útil para tener claro cuáles son tus dependencias y de dónde vienen. (Haz que los objetos, métodos y demás reciban sus dependencias como parámetros en lugar de instanciar ellos mismos nuevos objetos). Esto hace que las firmas de la API sean más complejas, así que es una compensación. Terminar con un método que necesita 10 parámetros para todas sus dependencias es una buena señal de que tu código está haciendo demasiado, de todos modos. El artículo definitivo sobre la inyección de dependencia es «Inversion of Control Containers and the Dependency Injection Pattern», de Martin Fowler.
15. Cuanto más tienes que burlarte para probar tu código, peor es tu código. Cuanto más código tengas que instanciar y poner en marcha para poder probar una pieza específica de comportamiento, peor es tu código. El objetivo son pequeñas unidades comprobables, junto con pruebas funcionales y de integración de mayor nivel para comprobar que las unidades cooperan correctamente.
16. Las APIs orientadas al exterior son donde el «diseño por adelantado» -y la consideración de futuros casos de uso- realmente importa. Cambiar las APIs es un dolor para nosotros y para nuestros usuarios, y crear incompatibilidad hacia atrás es horrible (aunque a veces es imposible de evitar). Diseña las APIs de cara al exterior con cuidado, manteniendo el principio de «lo simple debe ser simple».
17. Si una función o método supera las 30 líneas de código, considere la posibilidad de dividirlo. Un buen tamaño de módulo máximo es de unas 500 líneas. Los archivos de prueba tienden a ser más largos que esto.
18. No hagas trabajos en constructores de objetos, que son difíciles de probar y sorprendentes. No pongas código en __init__.py (excepto importaciones para namespacing). __init__.py no es donde los programadores generalmente esperan encontrar código, por lo que es «sorprendente»
19. El DRY (Don’t Repeat Yourself) importa mucho menos en las pruebas que en el código de producción. La legibilidad de un archivo de prueba individual es más importante que la mantenibilidad (separar trozos reutilizables). Esto se debe a que las pruebas se ejecutan y se leen individualmente en lugar de formar parte de un sistema más amplio. Obviamente, la repetición excesiva significa que se pueden crear componentes reutilizables por conveniencia, pero es una preocupación mucho menor de lo que es para la producción.
20. Refactoriza siempre que veas la necesidad y tengas la oportunidad. La programación consiste en abstracciones, y cuanto más cerca estén tus abstracciones del dominio del problema, más fácil será entender y mantener tu código. A medida que los sistemas crecen orgánicamente, necesitan cambiar la estructura para su caso de uso en expansión. Los sistemas superan sus abstracciones y estructura, y no cambiarlos se convierte en una deuda técnica que es más dolorosa (y más lenta y con más errores) para trabajar. Incluya el coste de la eliminación de la deuda técnica (refactorización) en las estimaciones del trabajo de las características. Cuanto más tiempo se deje la deuda, mayor será el interés que se acumule. Un gran libro sobre refactorización y pruebas es Working Effectively with Legacy Code, de Michael Feathers.
21. Haz que el código sea correcto primero y rápido después. Cuando trabajes en problemas de rendimiento, haz siempre un perfil antes de hacer las correcciones. Por lo general, el cuello de botella no está exactamente donde usted pensaba que estaba. Escribir código oscuro porque es más rápido sólo vale la pena si has perfilado y probado que realmente vale la pena. Escribir una prueba que ejercite el código que estás perfilando con una temporización alrededor hace que saber cuándo has terminado sea más fácil, y puede dejarse en el conjunto de pruebas para evitar regresiones de rendimiento. (Con la nota habitual de que añadir código de temporización siempre cambia las características de rendimiento del código, haciendo que el trabajo de rendimiento sea una de las tareas más frustrantes.)
22. Las pruebas unitarias más pequeñas y con un alcance más ajustado dan una información más valiosa cuando fallan: te dicen específicamente lo que está mal. Una prueba que levanta la mitad del sistema para probar el comportamiento requiere más investigación para determinar lo que está mal. En general, una prueba que tarda más de 0,1 segundos en ejecutarse no es una prueba unitaria. No hay tal cosa como una prueba de unidad lenta. Con las pruebas unitarias estrechamente delimitadas que comprueban el comportamiento, tus pruebas actúan como una especificación de facto para tu código. Idealmente, si alguien quiere entender tu código, debería ser capaz de recurrir al conjunto de pruebas como «documentación» para el comportamiento. Una gran presentación sobre prácticas de pruebas unitarias es Fast Test, Slow Test, de Gary Bernhardt:
23. «Not Invented Here» no es tan malo como la gente dice. Si escribimos el código, entonces sabemos lo que hace, sabemos cómo mantenerlo, y somos libres de extenderlo y modificarlo como creamos conveniente. Esto sigue el principio YAGNI: tenemos código específico para los casos de uso que necesitamos en lugar de código de propósito general que tiene complejidad para cosas que no necesitamos. Por otro lado, el código es el enemigo, y poseer más código del necesario es malo. Considera la compensación al introducir una nueva dependencia.
24. La propiedad del código compartido es el objetivo; el conocimiento en silos es malo. Como mínimo, esto significa discutir o documentar las decisiones de diseño y las decisiones importantes de implementación. La revisión del código es el peor momento para empezar a discutir las decisiones de diseño, ya que la inercia de hacer cambios radicales después de que el código ha sido escrito es difícil de superar. (Por supuesto, sigue siendo mejor señalar y cambiar los errores de diseño en el momento de la revisión que nunca.)
25. ¡Los generadores son lo mejor! Son generalmente más cortos y más fáciles de entender que los objetos con estado para la iteración o la ejecución repetida. Una buena introducción a los generadores es «Generator Tricks for Systems Programmers», de David Beazley.
26. ¡Seamos ingenieros! Pensemos en el diseño y construyamos sistemas robustos y bien implementados, en lugar de cultivar monstruos orgánicos. Sin embargo, la programación es un acto de equilibrio. No siempre estamos construyendo un cohete. La sobreingeniería (arquitectura cebolla) es tan dolorosa para trabajar como el código infradiseñado. Merece la pena leer casi cualquier cosa de Robert Martin, y Clean Architecture: A Craftsman’s Guide to Software Structure and Design es un buen recurso sobre este tema. Design Patterns es un libro clásico de programación que todo ingeniero debería leer.
27. Las pruebas que fallan intermitentemente erosionan el valor de su conjunto de pruebas, hasta el punto en que eventualmente todo el mundo ignora los resultados de la ejecución de pruebas porque siempre hay algo que falla. Arreglar o eliminar las pruebas que fallan intermitentemente es doloroso, pero vale la pena el esfuerzo.
28. Por lo general, sobre todo en las pruebas, esperar a un cambio específico en lugar de dormir por una cantidad arbitraria de tiempo. Los sleeps vudú son difíciles de entender y ralentizan tu suite de pruebas.
29. Ve siempre que tu prueba falle al menos una vez. Pon un error deliberado y asegúrate de que falla, o ejecuta la prueba antes de que el comportamiento bajo prueba esté completo. Si no, no sabrás que realmente estás probando algo. Escribir accidentalmente pruebas que en realidad no prueban nada o que nunca pueden fallar es fácil.
30. Y por último, un punto para la gestión: La molienda constante de características es una forma terrible de desarrollar software. No dejar que los desarrolladores se enorgullezcan de su trabajo asegura que no vas a sacar lo mejor de ellos. No abordar la deuda técnica ralentiza el desarrollo y da lugar a un producto peor y con más errores.