En anteriores entradas he presentado mecanismos para hacer Unit Test sobre métodos de lógica genéricos, que suelen ubicarse en una capa de dominio, y Flow Expectation Test sobre la capa de servicio. Ahora voy a presentarte la manera de hacer Unit Test sobre tu capa de datos.
En esta entrada presento una arquitectura que usa un patrón de diseño con Unit Of Work y Reposity Pattern. En esta otra entrada escribí sobre Rhino Mock como una de mis librerías favoritas para Unit Testing. Ahora voy a combinar ambas para hacer unit test a tus métodos de Unit Of Work, pudiendo simular una colección de datos en memoria como si de un acceso a base de datos se tratase, de forma que puedas probar tus métodos con consultas linq y testear así su resultado, siendo capaz de aproximarte cada vez al valioso TDD del que hablo en esta entrada.
En primer lugar he aislado la creación de un contexto mock en una clase que devuelve dicho contexto. En todo momento me voy a basar en mi entrada sobre una arquitectura con esté patrón. Esta clase puede quedar de forma parecida a la siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class EFMock { private UnitOfWork context; public EFMock() { this.context= CreateCustomerDataContextTestDouble(); } public UnitOfWork Context { get { return context; } } private UnitOfWork CreateCustomerDataContextTestDouble() { var dataContextTestDouble = MockRepository.GenerateMock<UnitOfWork>(); return dataContextTestDouble; } private IDbSet<T> GetDbSetTestDouble<T>(IQueryable<T> data) where T : class { IDbSet<T> dbSet = MockRepository.GenerateMock<IDbSet<T>>(); ((IQueryable<T>)dbSet).Expect(m => m.Provider).Return(data.Provider); ((IQueryable<T>)dbSet).Expect(m => m.Expression).Return(data.Expression); ((IQueryable<T>)dbSet).Expect(m => m.ElementType).Return(data.ElementType); ((IQueryable<T>)dbSet).Expect(m => m.GetEnumerator()) .Return(null) // will be ignored but still the API requires it .WhenCalled((methodInvokation) => methodInvokation.ReturnValue = data.GetEnumerator()); return dbSet; } } |
La clave de esta clase es su método genérico GetDbSetTestDouble<T>. Este método es capaz de poblar un IDbSet con los datos contenidos en data, de tipo IQueryable, de forma que todas tus consultas de linq serán capaces de hacer querys sobre estos datos.
Para que lo anterior sea posible, en tu clase UnitOfWork los DbSet deben ser definidos como «public virtual IDbSet».
Ahora lo único que necesitas es definir conjuntos de datos IQueryables por cada objeto de modelo que utilices, y asignarlos como resultado «Return» por cada stub de cada uno de los DbSet contenidos en tu UnitOfWork.
Si tuvieses por ejemplo los siguientes modelos:
- IdentityRole
- UserRole
Tu UnitOfWork tendría:
1 2 |
public virtual IDbSet<IdentityRole> IdentityRoles { get; set; } public virtual IDbSet<UserRole> UserRoles { get; set; } |
Podrías tener unos objetos IQueryables como:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
private IQueryable<IdentityRole> IdentityRoleStubData() { var identityRoles = new List<IdentityRole> { new IdentityRole { Id = "roleIdSuperAdmin", Name = "SuperAdmin" }, new IdentityRole { Id = "roleIdOwner", Name = "Owner" }, new IdentityRole { Id = "roleIdAdministrator", Name = "Administrator" }, new IdentityRole { Id = "roleIdEmployee", Name = "Employee" }, new IdentityRole { Id = "roleIdUser", Name = "User" } }.AsQueryable(); return identityRoles; } private IQueryable<UserRole> UserRoleStubData() { var identityRoles = new List<UserRole> { new UserRole { RoleId = "roleIdSuperAdmin", GUID_User = "userGUID1" }, new UserRole { RoleId = "roleIdOther", GUID_User = "userGUID1" }, new UserRole { RoleId = "roleIdSuperAdmin", GUID_User = "userGUID2" }, new UserRole { RoleId = "roleIdOther", GUID_User = "userGUID2" } }.AsQueryable(); return identityRoles; } |
Ahora en tu método CreateCustomerDataContextTestDouble() debes crear los stub de cada uno de los DbSet, pasando el conjunto de datos al método GetDbSetTestDouble que será usado en el Return de cada stub, quedando como sigue a continuación.
1 2 3 4 5 6 7 8 9 |
private SecurityContext CreateCustomerDataContextTestDouble() { var dataContextTestDouble = MockRepository.GenerateMock<SecurityContext>(); dataContextTestDouble.Stub(x => x.IdentityRoles).Return(GetDbSetTestDouble(IdentityRoleStubData())); dataContextTestDouble.Stub(x => x.UserRoles).Return(GetDbSetTestDouble(UserRoleStubData())); return dataContextTestDouble; } |
El resultado final sería este:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
public class EFMock { private SecurityContext securityContext; public EFMock() { this.securityContext = CreateCustomerDataContextTestDouble(); } public SecurityContext SecurityContext { get { return securityContext; } } private SecurityContext CreateCustomerDataContextTestDouble() { var dataContextTestDouble = MockRepository.GenerateMock<SecurityContext>(); dataContextTestDouble.Stub(x => x.IdentityRoles).Return(GetDbSetTestDouble(IdentityRoleStubData())); dataContextTestDouble.Stub(x => x.UserRoles).Return(GetDbSetTestDouble(UserRoleStubData())); return dataContextTestDouble; } private IDbSet<T> GetDbSetTestDouble<T>(IQueryable<T> data) where T : class { IDbSet<T> dbSet = MockRepository.GenerateMock<IDbSet<T>>(); ((IQueryable<T>)dbSet).Expect(m => m.Provider).Return(data.Provider); ((IQueryable<T>)dbSet).Expect(m => m.Expression).Return(data.Expression); ((IQueryable<T>)dbSet).Expect(m => m.ElementType).Return(data.ElementType); ((IQueryable<T>)dbSet).Expect(m => m.GetEnumerator()) .Return(null) // will be ignored but still the API requires it .WhenCalled((methodInvokation) => methodInvokation.ReturnValue = data.GetEnumerator()); return dbSet; } private IQueryable<IdentityRole> IdentityRoleStubData() { var identityRoles = new List<IdentityRole> { new IdentityRole { Id = "roleIdSuperAdmin", Name = "SuperAdmin" }, new IdentityRole { Id = "roleIdOwner", Name = "Owner" }, new IdentityRole { Id = "roleIdAdministrator", Name = "Administrator" }, new IdentityRole { Id = "roleIdEmployee", Name = "Employee" }, new IdentityRole { Id = "roleIdUser", Name = "User" } }.AsQueryable(); return identityRoles; } private IQueryable<UserRole> UserRoleStubData() { var UserRole = new List<UserRole> { new UserRole { RoleId = "roleIdSuperAdmin", GUID_User = "userGUID1" }, new UserRole { RoleId = "roleIdSuperAdmin", GUID_User = "userGUID2" }, }.AsQueryable(); return UserRole; } } |
Ya está todo listo, ahora solo queda hacer el test del método que deseas probar. Supongamos que en tu clase de contexto tienes un método que consulta diferentes tablas para obtener un dato, por ejemplo si un usuario tiene un determinado rol por el nombre de dicho rol. Necesitas consultar la tabla IdentityRole y UserRole. Hay muchas formas de abordar esto, pero yo he optado por hacerlo fuera de los repositorios, de forma que estos se mantengan con métodos solo para una tabla, y usar estos métodos con linq en el contexto, en UnitOfWork.
1 2 3 4 5 6 7 8 9 10 |
public UserRole FindUserRoleByRoleEnum(string userId, Const.Roles role) { List<UserRole> userRoles = (from ur in UserRoles join ir in IdentityRoles on ur.RoleId equals ir.Id where ir.Name == role.ToString() && ur.GUID_User == userId select ur).ToList(); return userRoles.FirstOrDefault(); } |
Ahora los siguientes test, aplicarán esta query sobre el contexto el cual tiene los DbSet como Mock gracias a Rhino Mocks, de forma que podrás probar si tus consultas tienen el resultado esperado ante un conjunto de datos que tu has definido, sin necesidad de acceder a la base de datos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
[TestFixture] public class ContextUnitTest { private EFMock efMock; UnitOfWork context; [SetUp] public void Setup() { efMock = new EFMock(); context= efMock.Context; } #region FindUserRoleByRoleEnum [Test] public void FindUserRoleByRoleEnum_WhenUserRoleExists_ShouldReturnUserRole() { UserRole actual = context.FindUserRoleByRoleEnum("userGUID1", Const.Roles.SuperAdmin); Assert.IsNotNull(actual); } [Test] public void FindUserRoleByRoleEnum_WhenUserRoleNotExists_ShouldReturnNull() { UserRole actual = context.FindUserRoleByRoleEnum("userGUID3", Const.Roles.SuperAdmin); Assert.IsNull(actual); } #endregion } |
Si quisieses aplicar Includes en tus querys, por tener objetos anidados como podría ser este caso, necesitarás que en tu conjunto de datos, tu objeto padre incluya al objeto hijo definido dentro. Un ejemplo podría ser el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 |
private IQueryable<UserRole> UserRoleStubData() { var UserRole = new List<UserRole> { new UserRole { RoleId = "roleIdSuperAdmin", GUID_User = "userGUID1", IdentityRole = new IdentityRole(){ Id = "roleIdSuperAdmin", Name = "SuperAdmin" } }, new UserRole { RoleId = "roleIdSuperAdmin", GUID_User = "userGUID2" }, }.AsQueryable(); return UserRole; } |
Pues otra capa más añadida a tu Code Coverage por Unit Test. Aún estarían sin probar los repositorios contenidos en tu UnitOfWork, pero dado que se trata de una implementación genérica, he preferido probar los repositorios de formas aislada para una entidad concreta, lo cual ya servirá para probar dicha implementación, y en cambio en UnitOfWork aplicar Stub de los repositorios como ya he mencionado en otras entradas, considerándolos un elemento aislado dentro de tu capa de datos que tiene la suficiente entidad por si mismo como para tener sus propios Unit Test.
En breve tendré finalizada esa entrada y podrás complementar a esta, la cual espero que te sea de utilidad.
Un saludo y a disfrutar programando.