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.