Rhino Mock ¿Por qué esta librería?

El mundo del unit testing presenta una gran cantidad de librerías que puedes utilizar para abordar tus tests. Entre las mas utilizadas están las nativas de Microsoft, NUnit, Moq, Rhino Mock…

Tras haber usado en mi entorno laboral librerías nativas de Microsoft, Moq, Rhino Mock y Nunit, me he decidido por combinar estas dos ultimas con mayor frecuencia, y en esta entrada te explico por qué ha sido así.

Habitualmente las librerías de mocking que puedes usar para hacer test, te facilitan eso, el mocking o la creación de stub. Si aún no sabes qué es esto, puedes leer acerca de ello en ¿Qué es un mock y un stub?.

Cuando pretendes probar un método de tu código, que se relaciona con otros métodos y obtiene respuestas de ellos, es habitual que hagas un stub de estos métodos para obtener una respuesta, mockeando la clase a la que estos métodos pertenecen.

Esto es normal cuando quieres probar “cómo” se está comportando el código, y la gran mayoría de fuentes a las que he recurrido para aprender a hacer unit test, se basan en esto, probar el “cómo”.

Si lo que quieres probar es el “qué” hace tu código, resulta mas difícil encontrar fuentes que te hablen de ello, de hecho aquí la lista de librerías que faciliten este tipo de test es mas reducida.

A que me estoy refiriendo con probar el “cómo” o probar el “qué”. A continuación os presento un par de ejemplos.

Probando el “cómo”

Un patrón de diseño que he encontrado muy útil, y me sirve para este ejemplo, el repository pattern, sobre este patrón junto a unit of work y entity framework code first hablo en esta entrada.

Con este patrón, todo acceso a la capa de datos queda englobada en clases de repositorio, y esto es muy útil para hacer unit test sobre la capa de dominio, o los servicios de lógica de negocio, desacoplándolos de la capa de acceso a datos, siendo independiente de la tecnología que utilices.

Suponte que tienes un servicio expuesto en tu api para obtener un objeto setting de un usuario de la base de datos. El controlador de tu API donde el servicio está expuesto, accederá a una capa de servicio, donde está el SUT (System Under Test), y este no será el responsable de acceder a la base de datos, si no que será el repositorio. Por tanto tu método de servicio necesita una respuesta de un método de repositorio. A continuación presento un ejemplo.

En este ejemplo puedes ver que nuestro SUT (GetUserSetting), tiene dependencia de la respuesta del método “FindUserSettings” del repositorySettings, el cual es una interfaz que recibe en su constructor. En este caso no debería importante qué hace ese método del repositorio, ya que tendrá sus propios unit test. Únicamente te debe interesar saber que devuelve un objeto Setting.

Por tanto lo que te interesa es crear un mock de tu repositorySettings, y un stub del método FindUserSettings de forma que condiciones su respuesta, y tu método sometido a test tenga algo que devolver o con lo que poder trabajar.

Aquí podemos ver un ejemplo de test para ver “cómo” se comporta este método.

Como puedes ver se están pasando 3 test.

  • Cómo se comporta nuestro método ante entradas de parámetros null, validando la excepción generada.
  • Cómo se comporta nuestro método cuando el repositorio devuelve un objeto null
  • Cómo se comporta nuestro método cuando el repositorio devuelve un objeto Setting que no es null.

En las primera línea del segundo y tercer test estamos simulando la respuesta de la llamada que el método hará al repositorio, de forma que realmente nuestro método nunca llega a usar el método de repositorio real que acabaría accediendo a la base de datos.

Muchas librerías son capaces de hacer esto con mayor o menor elegancia. Rhino mock ofrece muchas posibilidades en esta primera linea que aplica el Stub, en este caso los parámetros de entrada usados en la llamada al respositorio dan igual, por eso ni me molesto en introducir algo distinto de null, porque en el siguiente método decimos que los ignoramos (IgnoreArguments), ya que sean cuales sean devolveré lo que indica el return.

Podría no haber ignorado estos argumentos de entrada, y en caso de tener diferentes entradas, crear stubs que ofrezcan diferentes salidas. Estas entradas no tienen porque ser coincidencias exactas, pudiendo aplicar lógica que nos permita indicar que una entrada sea “distinta de”, mientras que otra sea “cualquier string” (por ejemplo). En otra entrada sobre uso de stub con Rhino mock hablaré de estas opciones.

Probando el “qué”

Al margen de lo anterior, lo que realmente me ha llevado a usar Rhino mock es el poder probar “qué” hace mi test y el “orden” en que lo hace.

Imagina que tu método de servicio hace cinco llamadas a los métodos 1, 2, 3, 4 y 5, y te interesa verificar que se hacen en ese orden específico, y que no hay ninguna nueva llamada entre ellos.

Librerías como Moq son capaces de verificar que un método es llamado, incluso que cantidad de veces es llamado, e incluso verificar que la sequencia en la que se llama a los métodos que son llamados es correcta, pero hasta donde yo se, no es capaz de detectar que hay llamadas en medio de esa secuencia que podrían no ser deseadas. Siempre se pueden buscar complejas formas de hacerlo, pero rhino mock incorpora esta funcionalidad de forma sencilla.

Te pongo en situación, tu método hace llamadas [1, 2, 4, 5], (falta el 3), quieres probar que hace justo eso, y no ninguna otra cosa en ningún otro orden, los test que podrías hacer son:

  • Verificar que algunos métodos son llamados. Podrías verificar que llamas a [5, 1, 2], y este test sería cierto, y daría verde, en cambio no sería representativo para testear “qué” hace tu método, ya que falta el 4 y no respeta el orden.
  • Verificar que todos los métodos son llamados. Podrías verificar que llamas a [1, 2, 5, 4], también sería cierto pero no estas verificando el orden, por lo que tampoco es representativo de “qué” estas haciendo.
  • Verificar que todos los métodos son llamados respetando una secuencia. [1, 2, 4, 5]. ¡Ya lo tienes! ¿Pero que pasa si alguien inserta el método 3 [1, 2, 3, 4, 5]? Tu test seguirá dando verde y en cambio habrá dejado de ser representativo sobre “qué” hace tu test.

Todos estos pasos anteriores son capaces de hacerse con librerías como por ejemplo Moq. Pero en el ultimo punto tienes un problema o una incertidumbre que con Moq te va a ser dificil detectar. En cambio Rhino mock es capaz de hacer que tu test falle y de un resultado negativo, rojo, ante esta situación, de forma que es un test muy estricto, y muy débil (esto no tiene porque ser negativo).

Se dice que un test es débil si pasa de un estado exitoso a uno negativo con facilidad tras aplicar cierto cambios en el código, en otras palabras, se puede romper con facilidad. Esto en cambio se entiende como una ventaja, pues asegura que cuando introduces cambios debes revisar y actualizar tu batería de test para que se mantengan consistente con tu código.

Si alguien edita tu método introduciendo la llamada 3, tu test fallará, y esa persona sabrá que tiene que refactorizar el test roto para que vuelva a ser representativo de la funcionalidad real.

Aquí presento un ejemplo:

En el método que te presento, un usuario pretende editar los roles de otro usuario en una empresa a la que pertenece.

En este caso te debe dar igual cómo se comporta cada uno de los métodos de roleDomainService que estas viendo. Solo debería importarte que se llaman en una secuencia, y quieres verificar que se trata de esa secuencia, y no otra. El código puede autodocumentarse leyéndolo, pero aún así lo voy a diseccionar fácilmente en lo que a llamadas a roleDomainService se refiere.

  • GetUser: Se obtiene el perfil del usuario logueado
  • GetUserRoles: Se obtienen los roles del usuario logueado en la empresa activa
  • FindUserRolesByBusiness: Se obtienen los roles del usuario que se quiere actualizar en la empresa
  • RolesCanBeUpdatedByUser: El usuario logueado puede actualizar los roles que se ven modificados entre los recibidos para el usuario destino, y los que el usuario destino tiene actualmente.
  • ValidationBusinessUserRole: Llegados a este punto se valida el objeto VM recibido (las validaciones de seguridad han pasado).
  • UpdateRoles: Llegados a este punto se realizan las actualizaciones de roles para el usuario destino (las validaciones de negocio han pasado).
  • SaveContext: La información modificada se persiste.

No querrás que las validaciones de negocio se realicen con anterioridad a las de seguridad,  para no devolver información a usuarios que no puedan accederla. No podrás llamar al método de seguridad sin conocer los roles del usuario destino y logueado, ni puedes llamar para obtener los roles del usuario logueado sin obtener el perfil de dicho usuario. Y obviamente no debes actualizar información sin pasar las validaciones, por lo que querrás validar justo ese orden, y si alguien lo modifica o añade pasos, querrás que el test se rompa para crear la obligación de arreglarlo (supervisándolo). Otra opción es que sepas que el método debe ser modificado, así que modificas el test esperando que falle, para aplicar TDD, refactorizar el código, y así arreglar el test que acabas de actualizar y daba rojo.

Te muestro un ejemplo de este test de “qué” se hace con rhino mock, independientemente de cómo se comporte cada uno de esos métodos, desacoplando nuestra capa de servicio de la capa de dominio.

Gracias a que la clase roleBusinessService recibe en su constructor la interfaz de IRoleDomainService, puedes generar un mock sobre esta interfaz (MockRepository.GenerateStrictMock<IRoleDomainService>();), puedes usar el mock en el constructor de la clase sometida a test, y en el test, puedes usar GetMockRepository().Ordered() para especificar todas las llamadas que esperas que se realicen (con Expect), y en la secuencia que esperas que sucedan.

El test en este caso no tiene Assert, en su lugar hay un VerifyAllExpectations sobre la clase roleDomainService. Si un nuevo método se introduce en la secuencia, o la secuencia falla, esta verificación no se cumplirá y el test fallará.

Conclusiones

Si desarrollas una arquitectura, donde tengas una capa de servicio que simplemente documente tu funcionalidad en pasos claros, para poder probar “qué” estas haciendo, y luego otra capa que implemente todas estas llamadas, pudiendo probar el “cómo” haces cada unos los pasos, tendrás un buen comienzo de una aplicación robusta y fácilmente mantenible de cara al futuro, siendo posible incluso reducir tiempo de adaptación a nuevos desarrolladores.

Deja un comentario