Foto de Pavel Danilyuk
Asociar el Principio del 80/20 en el contexto de DDD (Domain-Driven Design) y TDD (Test-Driven Development) con la Ley de Pareto tiene sentido porque esta ley establece que, en muchos casos, aproximadamente el 80% de los resultados provienen del 20% de las causas. Aplicado al desarrollo de software, esto sugiere que dedicar la mayor parte de tu tiempo y esfuerzo (el 80%) a entender y modelar el dominio del problema (DDD) puede resolver la mayoría de los desafíos y asegurar que el software esté alineado con las necesidades del negocio.
Por otro lado, el 20% restante del tiempo se invierte en asegurar la calidad del código mediante TDD, lo que garantiza que las funcionalidades críticas estén bien probadas y que el código funcione según lo esperado. Este equilibrio permite una base sólida y bien comprendida del problema, lo que minimiza errores y malentendidos, mientras que las pruebas aseguran un desarrollo más robusto y eficiente. En esencia, el Principio del 80/20 aplicado a DDD y TDD optimiza el esfuerzo y los recursos, maximizando el impacto positivo en el desarrollo de software.
DDD es el 80% y TDD es el 20%
Aplicar la Ley de Pareto en este contexto no es de mi autoría, sino de un buen amigo y profesional que lleva haciendo testing desde hace más de 20 años, cuando en el 99% de las empresas de España no se hacía. La semana pasada hablamos sobre este tema y me pareció tan valiosa su aportación que decidí crear este artículo para que podáis continuar adquiriendo formación y puntos de vista basados en su amplia experiencia.
Primero, hablemos del principio del 80/20 en el contexto de DDD y TDD:
DDD (Domain-Driven Design)
- Enfoque en el Dominio del Problema: DDD se enfoca en entender y modelar el dominio del problema. El objetivo principal es capturar las reglas del negocio y las funcionalidades requeridas de manera que el código refleje fielmente estas reglas. Este enfoque asegura que el diseño del software esté alineado con las necesidades del negocio desde el principio.
- Alineación con las Necesidades del Negocio: Al dedicar el 80% de tu tiempo a DDD, te aseguras de que tienes una comprensión profunda del problema y cómo debe ser resuelto antes de siquiera escribir una línea de código. Esto previene malentendidos y errores costosos más adelante en el desarrollo.
TDD (Test-Driven Development)
- Escribir Pruebas Antes del Código Funcional: TDD se centra en escribir pruebas antes de escribir el código funcional. Esto ayuda a garantizar que el código sea probado y funcione según lo esperado.
- Claridad en las Funcionalidades a Probar: Sin una clara comprensión de las reglas del negocio y los requisitos (lo que se obtiene a través de DDD), podrías terminar escribiendo pruebas para funcionalidades que no son necesarias o que no están alineadas con el dominio. Al dedicar el 20% de tu tiempo a TDD, te aseguras de que estás cubriendo tu código con pruebas de manera eficiente, pero solo después de que hayas definido claramente qué es lo que necesitas probar.
Reglas del Ciclo Red, Green, Repeat
El ciclo de TDD se puede resumir en tres simples pasos:
- Red: Primero, escribes una prueba que falla. Esto se llama «prueba roja» porque aún no has escrito el código que hará que la prueba pase. El objetivo es definir lo que quieres que tu código haga.
- Green: Luego, escribes el código mínimo necesario para que la prueba pase. Aquí es donde haces que la prueba «pase a verde». No te preocupes por la calidad del código en esta etapa; solo asegúrate de que la prueba pase.
- Repeat (Refactor): Finalmente, mejoras tu código. Refactorizas para limpiar el código, hacerlo más claro y eficiente, sin romper la funcionalidad. Durante este paso, tus pruebas deben seguir siendo verdes.
Metodología Recomendada
Nuestra metodología o aproximación al TDD es abierta y flexible, no un dogma.
Reconocemos que puede tener detractores o gente muy purista, pero creemos que esta aproximación puede ser beneficiosa para muchos equipos de desarrollo:
- Alineación con el Negocio y DDD:
- Comprensión del Dominio: Dedica tiempo a entender profundamente el dominio del problema y las necesidades del negocio.
- Modelado del Dominio: Plantea la arquitectura del software alineada con esta comprensión, asegurando que el diseño refleje fielmente las reglas del negocio.
- Diseño de Dominio: Utiliza los principios del diseño de dominio para crear un modelo que represente adecuadamente las entidades y sus interacciones dentro del contexto del negocio.
- Desarrollo con TDD:
- Pruebas Unitarias como Parte del Desarrollo: Considera las pruebas unitarias como una parte integral del desarrollo. No las dejes como una tarea posterior.
- Escribir Pruebas Antes del Código: Antes de implementar una funcionalidad, escribe las pruebas que definirán cómo debe comportarse el código.
- Creación de Código Guiada por Test: Desarrolla el código necesario para pasar las pruebas, asegurando que cada nueva funcionalidad esté respaldada por pruebas que validen su correcto funcionamiento.
- Implementación y Paralelización:
- Contratos y Modelos: Durante la implementación de cada contrato y modelo, asegúrate de escribir las pruebas unitarias de manera paralela. Esto significa que mientras desarrollas el código, también estás desarrollando las pruebas correspondientes.
- Servicios: Aplica el mismo enfoque a los servicios. Implementa las funcionalidades del servicio y, al mismo tiempo, escribe las pruebas para asegurar que funcionen correctamente.
- Así con todas las capas.
-
Ciclo Red, Green, Repeat.
-
Revisión y Ajustes:
- Revisión Continua: Revisa constantemente las pruebas y el código para identificar áreas de mejora.
- Ajustes Basados en Feedback: Ajusta el código y las pruebas basándote en el feedback recibido, tanto de los usuarios como del equipo de desarrollo.
- Documentación y Comunicación:
- Documentación de Pruebas: Documenta las pruebas y su propósito para facilitar su comprensión y mantenimiento.
- Comunicación con el Equipo: Mantén una comunicación abierta con todos los miembros del equipo para asegurar que todos estén alineados con los objetivos y las prácticas de desarrollo.
Recuerda, la clave está en adaptarla a las necesidades y contextos específicos de tu equipo y proyecto. Cuantas más herramientas tengas, mejor respuesta puedes dar al cliente.
Ejemplo de código en .NET con xUnit
Se supone que ya has realizado el DDD y por tanto os dejo un test muy sencillo para que práctiqueis pasar de rojo a verde y continuar con la depuración:
// The code
public class PalindromeFinder
{
public List<string> FindPalindromes(string input)
{
var result = new List<string>();
if (IsPalindrome(input))
{
result.Add(input);
}
return result;
}
private bool IsPalindrome(string input)
{
var reversed = new string(input.Reverse().ToArray());
return input.Equals(reversed, StringComparison.OrdinalIgnoreCase);
}
}
// The Test
using System.Collections.Generic;
using Xunit;
public class PalindromeFinderTests
{
[Fact]
public void IdentifiesSingleWordPalindrome()
{
var finder = new PalindromeFinder();
var result = finder.FindPalindromes("sagas");
Assert.Equal(new List<string> { "madam" }, result);
}
[Fact]
public void IdentifiesNonPalindrome()
{
var finder = new PalindromeFinder();
var result = finder.FindPalindromes("saga");
Assert.Empty(result);
}
[Fact]
public void IdentifiesPalindromeIgnoringCase()
{
var finder = new PalindromeFinder();
var result = finder.FindPalindromes("SaGas");
Assert.Equal(new List<string> { "MadAm" }, result);
}
[Fact]
public void PrivateMethodIsPalindromeWorks()
{
var finder = new PalindromeFinder();
var result = finder.IsPalindrome("sagas");
Assert.True(result);
}
}
¿Es buena es mala esta aproximación?
El TDD fue popularizado por Kent Beck, un destacado ingeniero de software y uno de los pioneros de las metodologías ágiles. Beck presentó TDD como una parte integral de Extreme Programming (XP), una metodología de desarrollo de software que co-desarrolló.
Kent Beck introdujo TDD como una técnica para mejorar la calidad del código y el diseño del software mediante la escritura de pruebas antes del código de producción. La idea es que al escribir las pruebas primero, los desarrolladores se enfocan en los requisitos y la funcionalidad del software desde el principio, lo que lleva a un código más limpio, modular y fácil de mantener.
¿Por qué Kent Beck y otros autores nos advierten que TDD no es siempre la mejor opción?
Aunque Kent Beck sigue siendo un defensor de TDD, ha reconocido que no es una solución universal y que puede no ser apropiada para todos los contextos o tipos de proyectos. Aquí hay algunas razones por las que tanto él como otros expertos argumentan que TDD no siempre es la mejor opción:
- Limitaciones en Proyectos Grandes y Complejos: En proyectos muy grandes o complejos, TDD puede volverse difícil de manejar. Escribir y mantener un gran número de pruebas puede ser costoso y llevar mucho tiempo, lo que puede ralentizar el desarrollo.
- Pruebas Incompletas o Incorrectas: Si las pruebas están mal diseñadas o incompletas, pueden dar una falsa sensación de seguridad. En algunos casos, los desarrolladores pueden confiar demasiado en las pruebas unitarias y no realizar suficientes pruebas de integración o de sistema.
- Rigidez en el Diseño: TDD puede llevar a un diseño demasiado rígido si los desarrolladores se concentran demasiado en hacer que las pruebas pasen, en lugar de pensar en la arquitectura general del sistema. Esto puede resultar en un código que es difícil de cambiar o adaptar a nuevos requisitos.
- Enfoque en Pruebas Unitarias: TDD se centra en las pruebas unitarias, que son útiles para verificar pequeñas partes del sistema, pero pueden no capturar problemas que solo se manifiestan cuando los componentes interactúan entre sí (es decir, pruebas de integración o E2E).
- No Adaptable a Todos los Lenguajes: En lenguajes de tipado fuerte como Java o C#, algunas de las ventajas de TDD (como la detección temprana de errores) pueden ser menos pronunciadas debido a la ayuda proporcionada por el compilador. En cambio, en lenguajes de tipado débil como JavaScript o Python, TDD puede ser más beneficioso.
En el video de 2024, «TDD: Theme & Variations«, Kent Beck explora cuándo TDD puede no ser la herramienta adecuada, sugiriendo que no es universalmente aplicable y que en ciertos casos, otras estrategias de desarrollo pueden ser más efectivas. Esto incluye proyectos donde las necesidades de diseño evolucionan rápidamente o donde las pruebas iniciales no ofrecen un valor significativo.
Ya lo decía en:
- El libro «Test-Driven Development: By Example» (2003): «TDD es una técnica que puede ayudar en muchos contextos, pero no es una bala de plata. Hay situaciones donde puede no ser práctico o incluso necesario.» Esto deja claro que él no pretende que TDD sea la única forma de desarrollar software.
- La entrevista en el podcast «Ruby Rogues» (2013): «Hay sistemas donde las pruebas unitarias estrictas no siempre aportan el mayor valor, como en prototipos rápidos o exploración de ideas.» Reconoce que el costo de implementar TDD puede no justificarse en ciertos casos.
- La conferencia en GeeCON 2016: Kent Beck señaló que: «TDD es como cualquier herramienta: úsala donde se adapte mejor al problema. Hay casos en los que puedes optar por otro enfoque y está bien.» Aquí hace énfasis en la flexibilidad en lugar del dogmatismo.
- En discusiones sobre TDD vs Diseño Emergente. En varias conversaciones, incluida una entrevista con Martin Fowler y David Heinemeier Hansson (DHH), Beck ha discutido que TDD puede no ser ideal si interfiere con otros enfoques de diseño más rápidos o si el desarrollador ya tiene una visión clara del diseño. Por ejemplo, en un debate entre Beck y DHH sobre getters y setters, Beck mencionó que: «Si escribir pruebas obstaculiza el flujo creativo o aumenta innecesariamente la complejidad inicial, no lo fuerces.»
Me sorprende mucho haber escuchado recientemente en un foro que Kent Beck se estaba retractando. Simplemente, muchas personas han adoptado TDD como dogma cuando nunca lo fue. Beck mismo ha reiterado que TDD es una herramienta poderosa, pero no una solución única para todos los problemas de desarrollo de software. La clave está en entender cuándo y cómo usarlo de manera efectiva.
Conclusión: ¿Por qué es bueno dedicar un 80% al DDD y el resto al TDD?
La regla de «DDD es el 80% y TDD es el 20%» tiene sentido cuando se considera el contexto y las fortalezas de cada metodología:
- Profunda Comprensión del Problema (DDD):
- Alineación con el Negocio: DDD asegura que el software esté profundamente alineado con las necesidades y reglas del negocio. Al dedicar el 80% del tiempo a DDD, se garantiza que el desarrollo se basa en una sólida comprensión del dominio del problema, lo que reduce la probabilidad de errores fundamentales y malentendidos.
- Base Sólida para el Desarrollo: Un enfoque fuerte en DDD proporciona una base sólida para el desarrollo del software, haciendo que el diseño y la arquitectura sean más robustos y adaptables a futuros cambios.
- Calidad del Código (TDD):
- Pruebas Basadas en Requisitos Claros: Una vez que se tiene una comprensión clara y detallada del dominio (gracias a DDD), TDD puede ser aplicado de manera más efectiva. Esto asegura que las pruebas se alineen con las funcionalidades críticas y las reglas del negocio.
- Eficiencia en la Cobertura de Pruebas: Dedicando el 20% del tiempo a TDD después de DDD, se garantiza que el tiempo invertido en pruebas sea eficiente y enfocado. Las pruebas se escriben con un claro entendimiento de lo que se debe probar, minimizando el riesgo de pruebas innecesarias o mal direccionadas.
- Flexibilidad y Adaptabilidad:
- Enfoque No Dogmático: Como Kent Beck y otros expertos han señalado, TDD no es una solución universal. La combinación de un fuerte enfoque en DDD (80%) con un enfoque complementario en TDD (20%) permite un desarrollo más equilibrado y adaptativo. Esto proporciona flexibilidad para ajustar las estrategias en función del contexto y las necesidades específicas del proyecto.
Nota
Cuando desarrollamos primero el código y luego los tests, nos encontramos con que muchas clases en C# son privadas o internas (esto no debería ocurrir si aplicamos TDD), lo que impide realizar un test directo. En estos casos, solo podemos hacer pruebas indirectas, lo cual no siempre es ideal. Sin embargo, si aplicamos Test-Driven Development (TDD), este problema se minimiza.
¿Por qué no debería ocurrir con TDD?
En TDD, escribimos primero las pruebas antes de escribir el código funcional. Este enfoque nos obliga a pensar en la interfaz pública del código desde el principio, ya que queremos que nuestras pruebas sean capaces de interactuar con el código que vamos a desarrollar. Como resultado, las clases y métodos que necesitamos probar tienden a ser más accesibles (públicos o internos), evitando así la situación en la que nos encontramos con clases privadas que no podemos testear directamente.
Solución con [InternalsVisibleTo]:
Para aquellos casos donde, a pesar de aplicar TDD y necesitas acceder a miembros internos, puedes utilizar el atributo [InternalsVisibleTo]
. Este atributo permite que los miembros internos de un ensamblado sean accesibles a otro ensamblado específico, facilitando así las pruebas unitarias.
Ejemplo:
Supongamos que tienes dos ensamblados: MyProject
y MyProject.Tests
. En MyProject
, podrías tener una clase interna que necesitas probar en MyProject.Tests
. Para permitir el acceso a esa clase interna desde el ensamblado de pruebas, agregarías lo siguiente al archivo AssemblyInfo.cs
de MyProject
:
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyProject.Tests")]
Esto le indica al compilador que todos los miembros internos de MyProject
deben ser visibles para el ensamblado MyProject.Tests
.
Más Información:
Para más detalles sobre cómo usar [InternalsVisibleTo]
, puedes consultar la documentación oficial de Microsoft: InternalsVisibleToAttribute Class.