Arquitectura software versatil, clean code y asequible a unit test o TDD

Es fácil entender lo que se busca cuando perseguimos tener el código bajo cobertura de unit test, así como es fácil entender porque aplicar el diseño dirigido por test (TDD – Test Driven design) es tan conveniente y sus ventajas. Sin embargo tratar de trasladar todo eso a la práctica en ocasiones no es tan fácil como se pinta.

En esta entrada doy algunos consejos sobre una arquitectura software versátil para afrontar estos y otros problemas. 

Diseño teórico de la arquitectura.

Para plantear la arquitectura que a continuación presento me he basado en una arqutiectura de N capas, donde se diferencian:

  • Controlador: Recibir llamadas, devolver resultados, y manejar excepciones con filtros globales.
  • Servicio: Manejar llamadas a la capa de dominio para a través de diferentes métodos llamados en el orden adecuado, satisfacer las reglas de negocio.
  • Dominio: Capa que contiene toda la lógica de negocio con principios SOLID, agrupando diferentes dominios con diferentes clases por responsabilidad única, existiendo para cada dominio por ejemplo clases de servicios de seguridad, servicios de mapeos, servicios de validaciones…
  • Acceso a datos: Se trata de repositorios agrupados en unidades de trabajo (Unit of work and repository pattern), en este caso se pueden diferenciar 2 niveles.
    • Interfaces de la capa de acceso a datos, ubicadas en la capa de dominio, desacopladas de la tecnología de acceso a datos elegida
    • Implementación de dichas interfaces, siendo la lógica del acceso a datos, soportada por una tecnología específica como Entity Framework por ejemplo.

Estos dos últimos puntos en los que se divide la capa de acceso a datos, se explican de forma extendida en mi entrada sobre Unit of work, Repository Pattern y Entity Framework Code First.

Modelo

Además se pueden distinguir distintos tipos de “modelos”.

  • Model: Se trata del modelo definido en nuestra capa de datos, el cual trasciende a la capa de dominio
  • DomainModel: En ocasiones necesitamos ciertos tipos de objetos para manejar información en nuestros servicios, sin que estos trasciendan al cliente ni a la capa de datos
  • ViewModel: Se trata del modelo que finalmente será ofrecido al cliente como respuesta a una petición concreta.

De esta forma, la arquitectura sería algo así:

Todas las capas se relacionan unas con otras a través de interfaces, con inyección de dependencias e inversión del control (DI e IoC), a través por ejemplo de Unity o Autofac, algo sobre lo que aún tengo pendiente escribir otra entrada.

De esta forma, podremos reemplazar cualquiera de las capas por una nueva implementación, sin que el resto de la arquitectura se vea afectada, ya que estarán desacopladas. Las nuevas implementaciones simplemente deberán respetar las interfaces. Así pues, nuestro repositorio podría pasar de implementar Entity Framework a ADO.Net sin que nuestro dominio tenga por qué sufrir un refactor.

Diseño estructural sobre un proyecto

¿Cómo he trasladado todo esto al “papel”, a un proyecto .net ya con funcionalidad? De la siguiente manera:

Aquí se pueden distinguir varias de las capas:

  • Controllers: Básicamente contiene los endpoints, un tratamiento de excepciones y los aspectos básicos de la autenticación.
  • Service: Es el unico sitio al que el controlador hace llamadas, de forma que le controlador no implementa absolutamente ninguna lógica, y a su vez aquí no se contiene la lógica en sí misma, si no el orden en que diferentes métodos contenedores de la lógica deben ser llamados para satisfacer las reglas de negocio. De esta forma cada uno de esos métodos podrá ser testeado de forma aislada, y en esta capa podremos testear que la cantidad de llamadas efectuadas a los diferentes métodos, y su orden, son los adecuados.
  • Domain: Aquí es donde realmente está contenida toda la lógica de la aplicación, y se puede dividir en varios apartados.
    • DomainModel: Ya definido anteriormente como objetos útiles para el desempeño de nuestras reglas de negocio, que no trascienden a la capa de datos ni al cliente.
    • Model: Nuestros objetos relacionados con la capa de datos.
    • ViewModels: Los objetos que se reciben o se envían hacia el cliente
    • Modules: Cada uno de los dominios con la suficiente entidad propia que hemos decidido separa en la aplicación, al tener operaciones que se sostienen en nuestra lógica de negocio por si mismas. Cada una de esas carpetas, contendrá una clase principal “DomainService” que coordinará cuantas operaciones sean necesarias entre las demás clases de ese dominio encargadas de la seguridad, los mapeos, las validaciones… siendo estas clases también contenidas en cada una de esas carpetas, por cada uno de los dominios.
    • IRepository o IUnitOfWork son las interfaces que conectarán la capa de dominio con la de datos, como ya se explica en la entrada que menciono anteriormente.
    • El resto de carpetas en la capa de dominio son menos significativas para esta entrada sobre arquitectura, solo para contener algunos helpers transversales en cross, o generación de eventos para service bus en arquitecturas de microservicios, son detalles en los que profundizaré en futuras entradas.
  • DataLayer: Aqui pueden observarse la implementación de nuestro respositorio genérico o clases de repositorio con extensiones, la unidad de trabajo (Unit of work) que dará paso a nuestro contexto de datos, y el los mapeos de Entity Framework para dar forma a nuestro modelo en la base de datos a través de Code First, sobre todo esto hablo en profundidad en la entrada que ya he comentado mas arriba, con ejemplos de código específicos.

Además de esas partes que ya se corresponden con el diseño teórico de la arquitectura, como todo proyecto, existen partes adicionales.

  • PresentationLayer: únicamente contiene algunos ficheros de recursos para control de ciertas partes del idioma desde FE, finalmente deje de considerar esta opción al manejarlo en cliente con Angular.
  • App_Data contiene algunos XML para documentación de la API con swagger.
  • App_Start lo de siempre, el WebApiConfig, la configuración básica de nuestra inyección de dependencias…
  • Y en migrations tenemos las diferentes migraciones de EF a las bases de datos basándonos en Code First.

Compatibilidad con Unit Testing

Pues bien, ¿qué hemos logrado? pues ahora toda la aplicación es perfectamente testeable bajo diferentes filosofías como se puede observar a continuación, expresando a la derecha el tipo de test que se perseguirá lograr en cada capa.

En la capa de controladores podremos hacer integration test, dado que solo se produce una llamada al servicio, si no aplicamos mock o stub a todas las capas inferiores, estaremos probando todo el servicio integramente.

En la capa de servicio, solo se producen llamadas de forma coordinada a la capa de dominio, por lo que aquí lo interesante es probar “qué” hacemos, y no “cómo se hace”, por lo tanto probaremos que las llamadas que pretendemos, su cantidad y su orden, son las adecuadas. Sobre esto hablo en varias entradas relacionadas con probar el flujo de nuestros servicios.

Flow expectation test como parte de tus Unit Test

Flow expectation test con Rhino Mocks – Parte 1

Flow expectation test con Rhino Mocks – Parte 2

Esto no son otra cosa que más Unit Test, pero la filosofía de lo que se busca testear cambia un poco, no es tanto los resultados producidos, si no mas bien las llamadas realizadas. Sobre los resultados producidos por cada uno de los métodos llamados nos preocuparemos en la siguiente capa.

En la capa de dominio si que probaremos que cada uno de los métodos de dominio responde a un resultado esperado ante un cierto input. Pero tendremos diferentes tipos de clases según sus responsabilidades, por lo que la filosofía para testear cada una de ellas puede variar ligeramente.

Por ejemplo, en un método de una clase de mapeo, ya que solo se encarga de mapear propiedades, habrá que probar que todas las propiedades mapeadas son las esperadas, en cambio en un método de una clase de validación, no habrá que validar todas las propiedades del objeto, solo aquellas implicadas en las validaciones y probar así que las pertinentes excepciones o mecanismos de control son disparados.

En la capa de datos en cambio, lo que habrá que probar será que las operaciones de persistencia no producen errores y se llevan a cabo de forma satisfactoria sobre un contexto, y sobre todo que las consultas producen los resultados esperados. Sobre como hacer unit test sobre unit of work y repositorios con entity framework y su DbContext (de forma aislada de la base de datos), hablo en esta entrada.

Test Driven Design

Conocida la información anterior, y dado que todas las capas están desacopladas gracias al uso de interfaces, usando estas interfaces podremos dar comienzo a los test incluso antes de las implementaciones de esas interfaces, ya que conoceremos su firma con parámetros de entrada y resultados devueltos o excepciones esperadas, siendo totalmente capaces de hacer TDD real bajo esta arquitectura.

En otra entrada profundizaré en como hacer DI e IoC para conectar las diferentes clases (y por tanto las capas) a través de unity.

Un saludo.

Deja un comentario