Pruebas manuales para desarrolladores, aislar el código temporal, y más
Antes de empezar a hablar de cómo construir un software de calidad, definámoslo. El software de calidad es robusto, mantenible, tiene un mínimo de errores y proporciona una buena UX.
La mayoría de los comentarios que siguen son conceptos generales que se aplican al desarrollo de software; sin embargo, algunos de ellos se aplican más específicamente al desarrollo de Android.
Poner un gran foco inicial en la calidad paga enormemente los dividendos en el futuro. Con la mejora de la calidad del código, puede aumentar el rendimiento de los desarrolladores, aumentar la capacidad y reducir el tiempo de entrega, lo que a su vez le dará más tiempo para reflexionar y mejorar su propio proceso.
Principios SOLID
Este acrónimo de 5 letras describe los principios generales de diseño de software que ayudan a que el código sea robusto. Lee más sobre ello aquí y aquí.
Son cosas que hay que tener en cuenta siempre que se escribe código. Estos principios ayudan a que tu código sea mantenible, lo que es extremadamente valioso a largo plazo.
Tener una comprensión “sólida” de todos los principios resultará muy valioso.
Date tiempo para planificar
Cuando trabajes en una característica de gran envergadura, tómate el tiempo necesario para pensar en cómo diseñar la función de forma eficaz. Piensa en cómo se comunicarán todos los elementos.
¿Existen patrones de diseño que ya hayan resuelto el mismo problema o uno similar? Si esta función tiene que cambiar en el futuro, ¿cómo puedo construirla de manera que el cambio sea rápido y fácil? Es decir, que el efecto dominó sea mínimo al realizar el cambio.
Cuando construyo una característica medianamente grande, a veces tardo unas 4 horas en planificar la arquitectura. Si se trata de una función muy grande, puedo tardar un día entero o más en planificar un diseño robusto.
Paquete por característica
Las dos formas principales de organizar el código son el paquete por capa y el paquete por función. Con el paquete por capa, esencialmente colocas las clases en la capa arquitectónica a la que pertenecen. Por ejemplo, puedes poner todas tus clases de vistas personalizadas en un paquete, todas tus clases de repositorio en otro paquete, todas tus clases de modelo en otro paquete, etc.
Con el paquete por función colocas todas las clases que se requieren para una característica en el mismo paquete. Esto tiene muchas ventajas. Menor complejidad, mayor cohesión de las clases, menor acoplamiento entre paquetes. ¿Necesitas actualizar una característica o tal vez eliminar una característica? Todo lo que necesitas está en un solo lugar – ¡no necesitas buscar todas las clases que constituyen la característica!
Esto también hace que el proyecto sea más fácil de mantener porque los futuros desarrolladores también tendrán más facilidad para trabajar con la función porque, de nuevo, todo está en un solo lugar.
Hay, por supuesto, cosas como las clases de utilidad general que pueden ser más adecuadas fuera de un paquete de características.
No comparta componentes personalizados entre funciones
Por ejemplo, si crea una vista personalizada para la función A, generalmente es mejor si no reutiliza esa vista personalizada para la función B, aunque ésta necesite una vista similar (o la misma).
La razón es la siguiente. Supongamos que crea una vista de tarjeta personalizada que se usa en una lista en cuatro pantallas diferentes (característica A, B, C y D). En la actualidad, todas las vistas de tarjetas en las cuatro pantallas diferentes muestran la misma información. ¿Y si en el futuro los requisitos cambian sólo para las características A y B? ¿Qué hará entonces? Por lo general, una de dos cosas:
- Agregue algunas condiciones a la vista personalizada y/o más propiedades para que pueda adaptarse a todos los requisitos para todas las pantallas.
- Rompe la función y crea una nueva vista personalizada con los requisitos modificados.
La primera opción es probablemente una mala decisión porque al hacerlo aumentará la complejidad de la vista personalizada. Si sigues haciendo esto, con el tiempo, terminarás con una clase monolítica que está fuertemente acoplada a diferentes características. Ahora, si quieres refactorizar esa clase, te va a llevar mucho tiempo debido al estrecho acoplamiento con tantas características y al aumento de la complejidad. Podrías romper algo en una de las características. Además, hay que tener en cuenta que con el aumento de la complejidad se reduce el rendimiento del desarrollador.
La segunda opción es la mejor, aunque no habría sido necesario en primer lugar si hubieras creado vistas separadas para cada función.
Hay una advertencia. Si hay una vista personalizada muy pequeña que estoy muy seguro de que no va a cambiar, puedo ponerla en un paquete de vista común y utilizarla en todas las funciones. Esto podría ser algo como una pequeña vista de imagen de usuario personalizada que se utiliza dentro de cada vista de tarjeta. En cambio, la vista de tarjeta puede contener todo tipo de información, lo que hace más probable que se cambie.
Piense en casos extremos
Tómate el tiempo necesario para pensar en todos los casos extremos mientras trabajas en las características. Debes saber qué puede fallar en cada parte del código en el que trabajas.
Por ejemplo, ¿se gestionan correctamente los errores de la API? ¿Qué sucede si un usuario está en su teléfono y pierde la conectividad de la red? ¿Qué ocurre si un usuario cierra la aplicación en medio de una operación de larga duración y la reanuda más tarde? ¿Qué ocurre si un usuario pulsa un botón que realiza una solicitud de API de forma muy rápida y repetida?
Si no puedes abordar los casos extremos inmediatamente, asegúrate de hacer un seguimiento de ellos de alguna manera. Por ejemplo, puede dejar un comentario TODO. Sin embargo, ¡asegúrate de resolver el caso límite antes de considerar que la función está completa!
Android Studio/IntelliJ IDEA tiene una buena característica donde puedes ver todos los comentarios TODO dentro de un paquete.
IntelliJ Live Templates
Las Live Templates son una característica de Android Studio/IntelliJ IDEA que permite insertar rápidamente código boilerplate. Esta es una característica increíblemente poderosa que puede aumentar su productividad (así como reducir los errores de copiar/pegar).
Por ejemplo, puedes utilizarla cuando realices tareas repetitivas como la creación de vistas personalizadas, la configuración de la unión de vistas, la escritura de pruebas unitarias, etc. Las plantillas en vivo son personalizables usando variables.
Aclarar siempre la incertidumbre
Por lo general, los desarrolladores trabajan en tickets creados por los jefes de producto. El ticket tendría los detalles de una característica determinada en la que estás trabajando (junto con los criterios de aceptación).
Cuando trabajes en estos tickets de características, es mejor que aclares cualquier cosa que no esté clara dentro del ticket. No des por sentado. Ahorrarás tiempo si haces esto. Lo último que queremos es hacer una suposición equivocada y luego tener que rehacer algo que podría haberse aclarado antes.
En mi organización actual, tenemos toda la comunicación escrita para una característica particular que ocurre en un canal de tema específico dentro de Slack. Las personas relevantes para esa función formarían parte del canal de Slack (ingenieros de backend, ingenieros de frontend, jefes de producto, etc.).
Para mantenerme organizado, lo que generalmente hago es publicar todas las preguntas que tengo sobre un ticket en particular como un comentario separado. Cada pregunta puede ser respondida en un hilo para esa pregunta en particular. Utilizo las respuestas emoji de Slack para indicar si la pregunta ha sido respondida o no. Si quedan preguntas sin resolver, utilizo❓ para indicarlo. Si la pregunta se ha respondido completamente, utilizo ✅ para indicar ese estado.
No se apresure
Por muy importante que sea la función, es importante ir paso a paso y pensarlo todo bien. Aunque estés “atrasado” no dejes que te afecte. Si intentas ir lo más rápido posible, lo más probable es que te dejes algún fallo o te pierdas algún caso límite que podría haberse evitado.
Aunque estés “yendo rápido”, te llevará más tiempo en general volver a arreglar los problemas y las esquinas que cortaste. Mantener la calma y tomarse el tiempo necesario ahorrará tiempo a la larga. Es más barato hacer las cosas correctamente la primera vez que arreglarlas cinco veces después de descubrir algunos fallos.
Si te sientes estresado, tómate un descanso o vete a dar un paseo.
Aunque haya un fallo crítico en producción, tienes que ir paso a paso y entender lo que está pasando antes de abordar el problema. Si te lleva demasiado tiempo solucionar el problema, puedes volver a la versión anterior mientras trabajas en una solución.
Resolver primero los problemas difíciles/de bloqueo
Los puntos de integración son un ejemplo de bloqueador potencial. Supongamos que estás trabajando en una nueva característica del lado del cliente que requiere la integración de la API. Supongamos que se tardará un día en completar la API y 5 días en completar el trabajo del lado del cliente. Es una buena idea probar la API en las primeras etapas del proceso (por ejemplo, tan pronto como esté lista), incluso si no has terminado de construir la interfaz de usuario.
Si esperas a probar la API hasta que hayas terminado de crear la función o, peor aún, no la pruebas en absoluto y simplemente envías una versión al departamento de control de calidad, corres el riesgo de alargar el plazo de entrega de la función. Si te mantienes en estrecha comunicación con el equipo de backend y ayudas a probar la API antes de terminar de crear la función, estarás ayudando a reducir el plazo de entrega general.
Pruebas manuales del desarrollador
Cuando hayas terminado de crear la función, deberías intentar romperla. Haz algunas de tus propias pruebas manuales para asegurarte de que no has pasado por alto ningún caso límite y de que las cosas funcionan como esperabas. Esto ayudará a acortar el bucle de retroalimentación.
Imagina por un minuto que nos saltamos por completo nuestras propias pruebas de desarrollador y simplemente lanzamos la compilación al control de calidad. Entonces QA encuentra un error y lo devuelve para que lo arregles. Mientras tanto, tú ya has cambiado el contexto a otro ticket. Entonces vuelves y arreglas el fallo encontrado en QA. Imagina que esto sucede durante una ronda más entre QA y tú.
¿Puedes ver cómo este tipo de cosas podría alargar fácilmente el tiempo de desarrollo de un ticket en varios días (cambio de contexto + pasar más tiempo en el mismo ticket)? Aunque hacer algunas pruebas manuales del desarrollador antes de pasar una construcción a QA puede parecer tedioso, le ahorrará tiempo a largo plazo.
Pruebas unitarias
Idealmente, cada característica que usted construye debe tener pruebas unitarias. Si el código no es comprobable, entonces debe ser mejorado para que sea comprobable.
Incluso mejor si sus pruebas unitarias se ejecutan en cada confirmación de sus ramas de desarrollo/maestro a través de la integración continua.
Revisión del código
Las revisiones de código estándar son otra gran manera de detectar errores y posibles mejoras. Una revisión exhaustiva del código es mucho más valiosa que simplemente hacer clic en el botón de aprobación.
Supervisar los informes de fallos
Vigilar de cerca los informes de fallos justo después de un lanzamiento puede resultar útil. Es una gran sensación cuando puedes identificar y arreglar los errores antes de que sean demasiado grandes.
Haz que la reversión sea fácil
Etiquetar los commits que liberas te facilitará la vida en caso de que necesites volver a una versión anterior. Yo uso etiquetas como release/1.2.3-456 que indican el número de la versión.
Cuando necesites retroceder, puedes comprobar fácilmente el commit etiquetado, subir la versión y liberar la compilación. Este método funciona incluso si su proyecto utiliza submódulos.