Estoy en medio de una *BIG REFACTORY* del código fuente de yoprogramo.net, pero el motivo de este post no es contarles eso si no comentarles como estoy realizando mis tests para diseñar Servicios y Repositorios.
Lo primero, tenemos que tener algo en claro, para programar bien #TDD es necesario conocer algunas cosas, que ya ayer Carlos Peix explico muy bien en su WebCast. Para que nuestra capa de servicios sea optima para TDD es necesario que apliquemos el patrón Inyección de Dependencias y que utilicemos algún contenedor IoC para trabajar con ellas.
Si no tienes conocimientos de estas herramientas y patrones te invito a que veas mi video tutorial sobre contenedores IoC.
Usare como ejemplo, algo del mundo real –copiándole a José- que aplique en yoprogramo.net para diseñar mis servicios.
Diseñando el servicio IPostServicio:
Este servicio estará en la capa infraestructura de la aplicación:
namespace YoProgramo.Nucleo
{
public interface IPostServicio {
IList TraerTodos();
IList TraerTodosSinSpam();
IList TraerPostsParaHome(int? top);
IPost TraerPorId(long id);
IList TraerPorTag(ITag tag);
IList TraerPorUsuario(IUser usuario);
IList TraerFavoritosPorUsuario(IUser usuario);
IList TraerTodosLosPostsSinRespuestas();
IList GetByCategory(ICategoria categoria);
IPost TraerPorSlug(string slug);
void Crear(IPost obj);
void Actualizar(IPost obj);
void Borrar(IPost obj);
}
}
Luego de crear la interface, empecé con el diseño y creación de los tests. El primero que les mostraré es uno para probar el método TraerTodos(). Es necesario utilizar alguna librería de mock *yo utilizo moq* para poder mockear el repositorio que utiliza el servicio.
Se dice que estos tests son de diseño, porque en realidad la clase PostServicio no existe y luego de crear el test iremos refactorizando hasta lograr tener terminada la clase con todos sus métodos funcionando.
namespace YoProgramo.Infraestructura.Tests
{
[TestClass()]
public class PostServicioTest
{
public IList HacerListaDePosts()
{
var lista = new List();
IPost post = new Post();
lista.Add(post);
post = new Post();
lista.Add(post);
return lista;
}
[TestMethod()]
public void Traer_todos_y_comprobar_que_no_sea_nulo_y_que_sean_2()
{
var falso_repositorio = new Mock();
falso_repositorio
.Setup(x => x.TraerTodos()).Returns(HacerListaDePosts());
IPostServicio servicioPost = new PostServicio(falso_repositorio.Object);
var list_de_posts = servicioPost.TraerTodos();
Assert.IsNotNull(list_de_posts);
Assert.AreEqual(list_de_posts.Count(), 2);
}
}
}
Primero creamos un mock – un mock es un objeto proxy – para así luego modificar su comportamiento, en este caso indicamos que al llamar al método TraerTodos() retorne una lista de objetos Post. Luego este objeto mock lo utilizamos para enviarlo al servicio a través de su constructor.
Refactoring, refactoring…
Al terminar el test el mismo falla y aquí es donde comienza el trabajo verdadero en TDD, hay que refactorizar para lograr superar nuestras pruebas.
Mi clase PostServicio luego del refactoreo:
namespace YoProgramo.Infraestructura.Servicios
{
public class PostServicio : IPostServicio
{
protected IPostRepositorio _postRepositorio;
public PostServicio(IPostRepositorio postRepositorio)
{
_postRepositorio = postRepositorio;
}
#region IPostServicio Members
public IList TraerTodos()
{
return this._postRepositorio.TraerTodos();
}
public IList TraerTodosSinSpam()
{
throw new NotImplementedException();
}
public IList TraerPostsParaHome(int? top)
{
throw new NotImplementedException();
}
public IList TraerPorTag(ITag tag)
{
throw new NotImplementedException();
}
public IList TraerPorUsuario(IUser user)
{
throw new NotImplementedException();
}
public IList TraerFavoritosPorUsuario(IUser user)
{
throw new NotImplementedException();
}
public IList TraerTodosLosPostsSinRespuestas()
{
throw new NotImplementedException();
}
public IList GetByCategory(ICategoria categoria)
{
throw new NotImplementedException();
}
public IPost TraerPorSlug(string slug)
{
throw new NotImplementedException();
}
public void Crear(IPost obj)
{
_postRepositorio.Agregar(obj);
}
public void Actualizar(IPost obj)
{
_postRepositorio.Actualizar(obj);
}
public void Borrar(IPost obj)
{
_postRepositorio.Borrar(obj);
}
public IPost TraerPorId(long id)
{
return _postRepositorio.Get(id);
}
#endregion
}
}
Solo implemente los métodos que utilizo para estas pruebas especificas, los demás serán para otros tests.
Falsos repositorios:
No siempre podemos mockear, en ocasiones tendremos que diseñar falsos objetos a pata, lo feo de esto es que este código es solo para las pruebas, no se usa en otra parte. Puede hacernos perder tiempo pero es necesario para que nuestras pruebas sean óptimas. Ahora veamos este test…
[TestMethod()]
public void Actualizar_post()
{
var falso_repositorio = new FalsoPostRepositorio();
IPostServicio servicioPost = new PostServicio(falso_repositorio);
IPost post = new Post();
post.Id = 1;
post.Title = "Prueba";
servicioPost.Crear(post);
IPost postParaActualizar = servicioPost.TraerPorId(1);
postParaActualizar.Title = "Prueba actualizada";
servicioPost.Actualizar(postParaActualizar);
IPost postEsperado = servicioPost.TraerPorId(1);
Assert.IsNotNull(postEsperado);
Assert.AreEqual(postEsperado.Title, "Prueba actualizada");
}
Como ven, aquí no utilizo mock dado que para actualizar objetos necesitamos algo de interacción con el repositorio, de hecho estamos realizando dos pasos, uno para crear un objeto y otro para modificarlo.
Mi Falso Repositorio:
namespace YoProgramo.Infraestructura.Tests.FalsosRepositorios
{
public class FalsoPostRepositorio : IPostRepositorio
{
private Dictionary<long, IPost> posts;
public FalsoPostRepositorio()
{
posts = new Dictionary<long, IPost>;
}
#region IRepositorio Members
public IPost Get(long id)
{
return posts[id];
}
public IList TraerTodos()
{
throw new NotImplementedException();
}
public void Borrar(IPost entidad)
{
posts.Remove(entidad.Id);
}
public void Agregar(IPost entidad)
{
posts.Add(entidad.Id ,entidad);
}
public void Actualizar(IPost entidad)
{
posts[entidad.Id] = entidad;
}
public IList FindAll()
{
throw new NotImplementedException();
}
public IList FindAll(System.Linq.Expressions.Expression> expression)
{
throw new NotImplementedException();
}
public IQueryable Find()
{
throw new NotImplementedException();
}
public IQueryable Find(long id)
{
throw new NotImplementedException();
}
public IQueryable Find(System.Linq.Expressions.Expression> expression)
{
throw new NotImplementedException();
}
#endregion
}
}
Y esto es todo por hoy, nada más que decir sobre el tema. Con el webcast de ayer me entusiasme mas con esto y ahora ya es tarde para escaparme jeje.