Cómo afectan los Mock frameworks nuestras pruebas unitarias

Una de las características más importante que debe seguir cualquier código y que es particularmente importante en las pruebas de cualquier tipo es la claridad. Una prueba debe entenderse a la primera sin demasiado esfuerzo, por eso es que debe ser breve, clara y desprovista en el mayor grado posible de elementos ‘ruidosos’.

Uno de esos factores ruidosos son los frameworks de aislamiento, los mal llamados Mocking Frameworks. Es por esto que limitar su uso a aquellas ocasiones verdaderamente justificables es algo que recomiendo. Sí, eso dije: hay que limitar el uso de frameworks de aislamiento por varias razones y entre ellas, porque ensucian las pruebas. Veamos:

Las dos pruebas siguientes son idénticas en propósito salvo que la primera utiliza un stub creado por mí mientras que la segunda utiliza Rhino Mock Isolation Framework:

[TestMethod]
public void Should_retrive_an_user_from_the_repository()
{
var userManager = new UserManagementService(new RepositoryStub());
var user = userManager.GetUserById(id: 1);

Assert.AreEqual("001 - Lucas", user.Code, "The returned Code is wrong");
}

[TestMethod]
public void Should_retrive_an_user_from_the_repository()
{
var repositoryStub = MockRepository.GenerateStub<IUserRepository>();
repositoryStub
.Expect(repository=> repository.GetUserById(id: 1))
.Return(User.CreateFromName(id: 1, name: "Lucas"));

var userManager = new UserManagementService(repositoryStub);
var user = userManager.GetUserById(id: 1);

Assert.AreEqual("001 - Lucas", user.Code, "The returned Code is wrong");
}

La diferencia es clara, a la larga la primera alternativa seguirá siempre siendo sencilla y entendible, es decir mantenible, mientras que la segunda, la que utiliza un framework de aislamiento, puede requerir más complejidad para por ejemplo, actuar en función de sus parámetros, soportar muchas llamadas, contemplar el orden de las llamadas, etc.

Aparte de esto, la primera opción es más rápida, no requiere que nos aprendamos las diferencias entre todos los tipos de Stubs y Mocks de cada frameworks y sus sintaxis particulares. ¿Quién que no conozca Rhino mocks podría configurar el stub para que en una llamada, cuando el id es 5, 6 ó 7, lance una exception?. Veamos un último ejemplo de esto:

[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException)]
public void Should_check_the_bounds()
{
var userManager = new UserManagementService(new RepositoryStub());
var user = userManager.GetUserById(id: -12);

Assert.Fail("An expected exception is missing.");
}

[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException)]
public void Should_check_the_bounds()
{
var repositoryStub = MockRepository.GenerateStub<IUserRepository>();
repositoryStub
.Expect(repository => repository.GetUserById(
Arg<int>.Matches(!(Is.LessThanOrEqual(0) && Is.GreaterThanOrEqual(2^16)))))
.Throw(new ArgumentOutOfRangeException("user id must be between 0 and 2^16") );

var userManager = new UserManagementService(repositoryStub);
var user = userManager.GetUserById(id: -12);

Assert.Fail("An expected exception is missing.");
}

Las dos pruebas son nuevamente idénticas en propósito y resultados obtenidos pero puede verse que la segunda prueba parece más complicada, y de hecho lo es. Ahora, respecto al RepositoryStub….. Cuán complejo es esta clase?

public class RepositoryStub : IUserRepository 
{
public User GetUserById(int id)
{
if(id < 0 || id > (2^16))
throw new ArgumentOutOfRangeException("user id must be between 0 and 2^16");
return User.CreateFromName(1, "Lucas");
}
}

Entre estas dos alternativas, la de usar un frameowrk y la de escribir nuestras propias clases, existe una tercera alternativa a tener en cuenta: crear nuestros stubs/mocks utilizando delegados para poder especificarles  aquello que queremos que hagan o retornen al ser invocados. Esta es una opción intermedia en cuanto al ruido que introducen. Vamos modificar la clase RepositoryStub para demostrar cómo se hacen. Aquí la tenemos:

class RepositoryStub : IUserRepository 
{
public Func<int, User> GetUserByIdFunc;

public RepositoryStub(Func<int, User> getUserFunc )
{
GetUserByIdFunc = getUserFunc;
}

public User GetUserById(int id)
{
return GetUserByIdFunc(id);
}
}

Con este patrón podemos hacer que nuestras pruebas sean tan sencillas como si hubiesemos creado un stub específico y no configurable. Así queda nuestra prueba ahora:

[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Should_check_the_bounds()
{
var repository = new RepositoryStub(getUserFunc: ReturnLucasOrThrowAnExceptionIfIdIsOutOfBounds);
var userManager = new UserManagementService(repository);
var user = userManager.GetUserById(id: -12);

Assert.Fail("An expected exception is missing.");
}

private User ReturnLucasOrThrowAnExceptionIfIdIsOutOfBounds(int id)
{
if(id < 0 || id > (2^16))
throw new ArgumentOutOfRangeException("user id must be between 0 and 2^16");
return User.CreateFromName(1, "Lucas");
}

Es posible crear pruebas más sencillas, más legibles, más breves, más fáciles de entender, sin dependencias externas a frameworks complicados y difíciles de aprender y con total control de lo que pasa y deja de pasar en nuestro código. Solo se necesita resistir la tentación (o la moda) de utilizar frameworks de aislamiento en donde no son absolutamente necesarios.

Sin categoría

3 thoughts on “Cómo afectan los Mock frameworks nuestras pruebas unitarias

  1. Esto se llama sindrome NIH. El hecho de que en este ejemplo sea más “sencillo” no implica que se deba usar. Hablas de mantenimiento. Es más mantenimiento tener ese codigo generado a mano que usar un framework. Además, la claridad es mayor cuando se describe el comportamiento dentro de la misma prueba no escondido en otra clase.

    El hecho de no querer entender (que se tarda un par de horas como mucho) lo que un framework puede ofrecer no es motivo para hacerse uno propio.

    Realmente cuando se enfrente uno a situaciones complejas se ve cuanto se ahorra con estos frameworks. Combinar uno hecho a mano y un framework como Rhino Mocks para casos “simples” y “complejos” no tiene sentido honestamente

  2. Lucas buenas, my 2 cents

    tengo que darle la razón a Pedro. Por lo gral los pasos que se siguen son los siguientes: desacoplar -> crear stubs/mocks personalizados -> ver como se complica el codificar todos los stubs/mocks -> elegir un producto (Moles, Rhino, etc) utilizarlo y happy ending ^^

    En casos muy simples como este, efectivamente es más rápido escribir una stub propio que utilizar otro producto, pero cuando hay más complejidad o en otros escenarios, estos productos valen la pena.

    Eso sí, si en tu caso es más rápido/fácil hacer todo “a mano”, go for it !!! que luego siempre puedes cambiar cuando lo necesites (y te de el presupuesto, of course)

    Salu2

  3. Hola gente: bueno, como sabemos, esto que describo no algo nuevo o algo que quiera inventar yo, de hecho es como se hacian las cosas antes de que existiesen los frameworks de aislamiento. Yo no considero que esto sea un NIH porque, al contrario de los pasos que comentas Bruno, yo he dado los pasos al reves: luego de un año y medio aproximadamente de usar Rhino y mucho reniegue y complicaciones, me puse a buscar otras opciones hasta que me he decidido por lo más sencillo. Además ahora cualquiera puede entender las pruebas y modificarlas.

    Mis pasos han sido los siguiente: Rhino Mocks pimero. Luego, gracias a Uncle Bob, comencé a hacer mis Stubs/Mocks “a mano”. Y por último, gracias a los capos de Moles, empecé a crear mis Stubs/Mocks “a mano” pero con delegados (como en el último ejemplo). Y la vida me sonrie realmente 🙂 Nunca he tenido nunca casos en los que “se complica” algo al usar estas alternativas.

    Pero creo que debo aclarar que jamás he dicho que esto sea para casos simples y que los frameworks sean para casos complejos! eso sí que es ridículo. Tampoco esto es “hacer un framework propio”, a menos que sobrecarguemos un poco más a esa palabra para contemplar ahora también la creación de estas clases falsas.

    Yo creo que cuando las cosas se complican es porque el código que queremos probar está muy acoplado y eso nos obliga a crear una docena de stubs, eso probablemente se deba a que estamos haciendo test-last y tenemos código duro de probar. Nuevamente la culpa es nuestra. En estos casos no sé si la solución es usar un frameworks de aislamiento o desacoplar un poco el código.

    Bueno, para terminar, para mí no solo me resulta más rápido/fácil y claro hacerlo así sino que además me resulta igualmente rápido por lo que el presupuesto no cambia en nada Bruno.

    Bueno gente, un abrazo y saludos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *