viernes, 25 de enero de 2008

Mock object con Rhino Mocks

Si están incursionando en el mundo de las pruebas de unidad, el desarrollo orientado por pruebas y en general en el mundo de la programación orientada a objetos, seguramente habrán escuchado hablar de los mock objects, hasta incluso habrán escuchado el término "moquear", este documento es acerca de mock objects u objetos simuladores.

Qué son los mock objects?

Éste concepto se encuentra muy ligado a las pruebas de unidad y voy a explicar por qué. Primeramente vamos a recordar algunos de los principios de las pruebas unitarias

-Deben ser automáticas
-Repetibles y reutilizables
-Independientes, no deben afectar a nada fuera de su ámbito

Ahora vamos a recordar algunas objetivos - ventajas que persigen las pruebas unitarias

-Separar el interfaz de la implementación
-Simplificar la integración
-Acotar el ámbito de los errores

Teniendo en cuenta estos conceptos es difícil pensar en una prueba que acceda a la base de datos por ejemplo, o a un archivo de texto, o que necesite una comunicación TCP, etc. Sin embargo esto es necesario en muchos casos, veamos un ejemplo

Ejemplo, cuenta bancaria

Imaginemos que tenemos un sistema en donde se puede consultar el estado de una cuenta bancaria, ya que nuestra implementación es sencilla, digamos que el registro de las cuentas se guarda en un archivo de texto separado por comas (horrible, en fin es un ejemplo), entonces tenemos

1- Una clase Cuenta, por supuesto, con un identificador y un saldo digamos
2- Una clase CuentaManager con la que vamos a interactuar, con un método GetCuenta que recupera un objeto Cuenta a partir del identificador
3- Por último una clase FileManager con la que CuentaManager va a poder manipular los archivos de texto, perfecto

Primero que nada para vamos implementar la clase CuentaManager porque el FileManager lo va  a hacer otro desarrollador, ya que se trata de un proyecto complejo, y además no nos gusta trabajar con archivos de texto por más FileHelpers que haya.

Probando el método GetCuenta de CuentaManager

El método GetCuenta va a recibir un identificador (un entero) y con él va a consultar a un objeto FileManager para que éste recupere la cuenta de donde sea; el problema de probar esto es que necesitamos que la clase FileManger nos devuelva una cuenta, pero no la tenemos y en caso de que la tuvieramos no es correcto utilizarla ya que un test no tiene que tocar nada más que lo que está probando, es aquí donde los mock objects vienen a ayudarnos.

Primer paso utilizar interfaces

Para que todo esto funcione de manera elegante necesitamos que nuestros objetos se comuniquen por interfaces, por lo tanto podemos escribir primero las interfaz y después implementarlas en una clase, o crear la clase y extraer la interface con alguna herramienta de refactor como el Visual Studio 2005, Reshaper, Eclipse, etc.

Inyección de dependencias

El segundo obstáculo que nos encontramos es que ibamos a instanciar la clase FileManager dentro de la clase CuentaManger porque nos parecía lo más adecuando, sin embargo esto nos va a impedir reemplazar la clase FileManager con cualquier IFileManager que querramos (porque deberíamos tocar el código interno de la clase CuentaManager), entonces vamos a escribir un constructor para la clase CuentaManager que acepte un objeto que implemente IFileManager y entonces utilizamos éste, a esto se le llama inyección de dependencia, ya que ahora la clase CuentaManager depende de otra para existir.

public CuentaManager(IFileManager<ICuenta> fileManager)

Perfecto, ahora vamos a crear la prueba de unidad

Assert.AreEqual(cuentaManger.GetCuenta(1), new Cuenta(1, 100), "No se recupera la cuenta con el cuenta manager correctamente"); 
Assert.AreEqual(cuentaManger.GetCuenta(2), new Cuenta(2, 200), "No se recupera la cuenta con el cuenta manager correctamente"); 

Bien, ahora necesitamos crear nuestra clase FileManagerMock (la clase que va a simular el comportamiento de la verdadera clase FileManager) que implemente la interfaz IFileManager y pasarla al objeto CuentaManager en el constructor

public class FileManagerMock: IFileManager<ICuenta>
{ 
 public ICuenta Get(int id) 
 { 
  if (id == 1) return new Cuenta(1, 100); 
  if (id == 2) return new Cuenta(2, 200); 
 } 
 public bool Save(ICuenta arg) 
 { 
  throw new Exception("The method or operation is not implemented."); 
 } 
} 

Bien, no es muy elegante pero sirve perfectamente, ahora el código de la prueba unitaria queda así:

IFileManager fileManagermock = new FileManagerMock(); 
ICuentaManger cuentaManger = new CuentaManager(fileManagermock); 
Assert.AreEqual(cuentaManger.GetCuenta(1), new Cuenta(1, 100), "No se recupera la cuenta con el cuenta manager correctamente"); 
Assert.AreEqual(cuentaManger.GetCuenta(2), new Cuenta(2, 200), "No se recupera la cuenta con el cuenta manager correctamente"); 

Perfecto y funciona que es lo importante, el problema lo habrán detectado, tenemos que escribir los mock objects nosotros, no vamos a decir que fue difícil, pero en una aplicación compleja con muchos test puede ser más que molesto, por suerte no todo está perdido

Ahora si, Rhino Mocks

Parece que siempre que existe un problema viene alguien con ganas de solucionarlo (en el software al menos) y esto es lo que hicieron los responsables de NMock, TypeMock.net, Rhino Mocks.
El tema es así, estos frameworks nos van a permitir crear los mock objects sin implementar una línea de código, cómo?, fácil

IMiInterfaz objetoMockeado = (IMiInterfaz) mocker.CreateMock(typeof(IMiInterfaz));

Antes que digan "esto lo hago con refection", esperen, hay algo más interesante. Podemos decirle al framework cómo debe comportarse cuando algunos de sus métodos sean invocados,  más o menos así:

Expect.Call(objetoMockeado.MetodoAInterceptar(1)).Return(new ObjectoRetorno())

O sea, cuando se llame al objecto que estoy moqueando a través de su método MetodoAInterceptar, con el parámetro 1, devolvé un ObjetoRetorno, aunque parezca mentira funciona, vamos a ver cómo es el código del ejemplo de las cuentas con RhinoMocks

[TestClass]
public class UnitTest
{
    Rhino.Mocks.MockRepository mocks;
    IFileManager<ICuenta> fileManagermock;
    ICuentaManger cuentaManger;
 
    [TestInitialize()]
    public void StartUp()
    {
        mocks = new Rhino.Mocks.MockRepository();
        fileManagermock = mocks.CreateMock<IFileManager<ICuenta>>();
        cuentaManger = new CuentaManager(fileManagermock);
    }
 
    [TestMethod]
    public void GetCuentaTest()
    {
        Rhino.Mocks.Expect.Call(fileManagermock.Get(1)).Return(new Cuenta(1, 100));
        Rhino.Mocks.Expect.Call(fileManagermock.Get(2)).Return(new Cuenta(2, 200));
        
        mocks.ReplayAll();
 
        ICuentaManger cuentaManger = new CuentaManager(fileManagermock);
 
        Assert.AreEqual<ICuenta>(cuentaManger.GetCuenta(1), new Cuenta(1, 100), "No se recupera la cuenta con el cuenta manager correctamente");
        Assert.AreEqual<ICuenta>(cuentaManger.GetCuenta(2), new Cuenta(2, 200), "No se recupera la cuenta con el cuenta manager correctamente");
 
        mocks.VerifyAll();           
    }
}

Mágico!, es verdad, como resultado tenemos:

- Pruebas unitarias que no interfieren código que no deben
- Mejor diseño del código (menos acoplamiento, más cohesión)
- Interfaces
- Pruebas más atómicas
- No dependemos de otros componentes
- En el momento de integrar el impacto es menor
- Creación de los componentes externos fácil.
- etc.

Todo muy lindo, sólo falta convencer a los gerentes que el tiempo insumido en esto es tiempo ganado y no perdido ;-), hasta la próxima.

Referencias

Hello word of Rhino Mocks
NMock
RhinoMocks
TypeMocks.net

9 comentarios:

josé M. Aguilar dijo...

Buen post, Leonardo. En muy poco espacio haces que sea posible obtener una idea clara de lo que puede conseguirse usando estos componentes.

Enhorabuena.

LgEaObNrAiReDlO dijo...

Gracias por el comentario José, sólo un pequeño aporte a la comunidad de habla hispana, de todos modos hay mucho más de Rhino mocks, esto es sólo una introducción.

Unknown dijo...

Hola Leonardo.

Antes que nada felicitarte por lo clarito que lo dejas todo y agradecerte personalmente un post como este (a mi me aclaró un par de conceptos bastante bien).

Aun así hay algo que no termino de entender y no es otra cosa que el uso de genéricos cada vez que usas el interfaz IFileManager. ¿Para que se usa en este caso? A ver, entiendo el uso de genéricos pero no entiendo la justificación que tiene en el ejemplo en concreto.

LgEaObNrAiReDlO dijo...

Gracias por el comentario Pablo, el tema de la interfaz IFileManager es representavivo de un repositorio de datos (en este caso en un file system) es genérico porque se supone que resuelve automáticamente la persistenacia, al estilo Active Record, pero más que nada por es un ejemplo :P , fue lo mejor que se me ocurrio :). Saludos.

Fernando Arancibia dijo...

Muchas gracias, creo que me queda clara la idea de lo que puedo hacer con mock object.

LgEaObNrAiReDlO dijo...

Gracias por dejar tu comentario Fernando, saludos!

Anónimo dijo...

Leonardo, muchas gracias por mostrar el tema en forma tan clara. Si me permitís un aporte, a mi me sirvió mucho ver que los métodos que se utilizan para setear las expectations, son en realidad extension methods que provee en este caso Rhino. Es solo un comentario y quizás es muy obvio, seguramente lo sabrás mejor que yo. Nuevamente gracias!

LgEaObNrAiReDlO dijo...

Gracias por tu comentario Raul, buen aporte, hace tiempo que dejé de usar Rhino para usar Moq

Walter dijo...

Me gusto mucho el pst, felicitaciones, Quisiera Saber como utilizar los objetos mock para simular el acceso a la Base de Datos.
de antemano mucha Gracias