.net - visual - Fake DbContext of Entity Framework 4.1 para probar
unit testing controllers (5)
"Lamentablemente no lo está haciendo bien porque ese artículo está equivocado. Finge que FakeContext hará que su unidad de código se pueda probar, pero no lo hará"
Soy el creador de la publicación de blog a la que te refieres. El problema que veo aquí es una comprensión errónea de los fundamentos de las pruebas unitarias en N-capas. Mi publicación no debe usarse directamente para probar la lógica del controlador.
Una prueba de unidad debería hacer exactamente lo que su nombre implica y probar ''Una unidad''. Si estoy probando un controlador (como lo está haciendo arriba), me olvido por completo del acceso a los datos. Debería eliminar todas las llamadas al contexto de la base de datos en mi mente y reemplazarlas con una llamada al método de recuadro negro como si esas operaciones fueran desconocidas para mí. Es el código alrededor de esas operaciones que estoy interesado en probar.
Ejemplo:
En mi aplicación MVC usamos el patrón de repositorio. Tengo un repositorio, digamos CustomerRepository: ICustomerRepository, que realizará todas las operaciones de mi base de datos del Cliente.
Si tuviera que probar mis controladores, ¿querría que las pruebas probaran mi repositorio, mi acceso a la base de datos y la propia lógica del controlador? ¡por supuesto no! hay muchas ''unidades'' en esta tubería. Lo que quiere hacer es crear un repositorio falso que implemente ICustomerRepository para permitirle probar la lógica del controlador de forma aislada.
Hasta donde yo sé, esto no se puede hacer solo en el contexto de la base de datos. (excepto tal vez por usar Microsoft Moles, que puede consultar si lo desea). Esto es simplemente porque todas las consultas se realizan fuera del contexto en su clase de controlador.
Si quisiera probar la lógica de CustomerRepository, ¿cómo lo haría? La forma más fácil es usar un contexto falso. Esto me permitirá asegurarme de que cuando intento obtener un cliente por identificación, en realidad recibe al cliente por identificación, etc. Los métodos de repositorio son muy simples y el problema "No se puede traducir en una expresión de tienda" generalmente no aparecerá. Aunque en algunos casos menores puede (a veces debido a consultas linq escritas incorrectamente) en estos casos, es importante realizar pruebas de integración que prueben el código hasta la base de datos. Estos problemas se encontrarán en las pruebas de integración. He usado esta técnica N-Layered desde hace bastante tiempo y no he encontrado problemas con esto.
Pruebas de integración
Obviamente, probar su aplicación contra la base de datos en sí es un ejercicio costoso y una vez que obtiene decenas de miles de pruebas se convierte en una pesadilla, por otro lado es mejor imitar cómo se usará el código en el "mundo real". Estas pruebas son importantes (desde la interfaz de usuario a la base de datos) también y se realizarán como parte de las pruebas de integración, NO como pruebas unitarias.
Acaz, lo que realmente necesitas en tu escenario es un repositorio que se pueda burlar / falsificar. Si desea probar sus controladores mientras lo hace, entonces su controlador debería tomar un objeto que envuelva la funcionalidad de la base de datos. Luego, puede devolver todo lo que necesite para probar todos los aspectos de la funcionalidad de su controlador.
ver http://msdn.microsoft.com/en-us/library/ff714955.aspx
Para probar el repositorio en sí (debatido si es necesario en todos los casos), querrá simular el contexto o usar algo similar al marco de ''Moles''.
LINQ es intrínsecamente difícil de probar. El hecho de que la consulta se defina fuera del contexto utilizando métodos de extensión nos brinda una gran flexibilidad pero crea una pesadilla de prueba. Envuelva su contexto en un repositorio y este problema desaparece.
lo siento mucho :)
Estoy usando este tutorial para falsificar mi DbContext y probar: http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic-repository/
Pero tengo que cambiar la implementación de FakeMainModuleContext para usar en mis controladores:
public class FakeQuestiona2011Context : IQuestiona2011Context
{
private IDbSet<Credencial> _credencial;
private IDbSet<Perfil> _perfil;
private IDbSet<Apurador> _apurador;
private IDbSet<Entrevistado> _entrevistado;
private IDbSet<Setor> _setor;
private IDbSet<Secretaria> _secretaria;
private IDbSet<Pesquisa> _pesquisa;
private IDbSet<Pergunta> _pergunta;
private IDbSet<Resposta> _resposta;
public IDbSet<Credencial> Credencial { get { return _credencial ?? (_credencial = new FakeDbSet<Credencial>()); } set { } }
public IDbSet<Perfil> Perfil { get { return _perfil ?? (_perfil = new FakeDbSet<Perfil>()); } set { } }
public IDbSet<Apurador> Apurador { get { return _apurador ?? (_apurador = new FakeDbSet<Apurador>()); } set { } }
public IDbSet<Entrevistado> Entrevistado { get { return _entrevistado ?? (_entrevistado = new FakeDbSet<Entrevistado>()); } set { } }
public IDbSet<Setor> Setor { get { return _setor ?? (_setor = new FakeDbSet<Setor>()); } set { } }
public IDbSet<Secretaria> Secretaria { get { return _secretaria ?? (_secretaria = new FakeDbSet<Secretaria>()); } set { } }
public IDbSet<Pesquisa> Pesquisa { get { return _pesquisa ?? (_pesquisa = new FakeDbSet<Pesquisa>()); } set { } }
public IDbSet<Pergunta> Pergunta { get { return _pergunta ?? (_pergunta = new FakeDbSet<Pergunta>()); } set { } }
public IDbSet<Resposta> Resposta { get { return _resposta ?? (_resposta = new FakeDbSet<Resposta>()); } set { } }
public void SaveChanges()
{
// do nothing (probably set a variable as saved for testing)
}
}
Y mi prueba así:
[TestMethod]
public void IndexTest()
{
IQuestiona2011Context fakeContext = new FakeQuestiona2011Context();
var mockAuthenticationService = new Mock<IAuthenticationService>();
var apuradores = new List<Apurador>
{
new Apurador() { Matricula = "1234", Nome = "Acaz Souza Pereira", Email = "[email protected]", Ramal = "1234" },
new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "[email protected]", Ramal = "4321" },
new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "[email protected]", Ramal = "4213" }
};
apuradores.ForEach(apurador => fakeContext.Apurador.Add(apurador));
ApuradorController apuradorController = new ApuradorController(fakeContext, mockAuthenticationService.Object);
ActionResult actionResult = apuradorController.Index();
Assert.IsNotNull(actionResult);
Assert.IsInstanceOfType(actionResult, typeof(ViewResult));
ViewResult viewResult = (ViewResult)actionResult;
Assert.IsInstanceOfType(viewResult.ViewData.Model, typeof(IndexViewModel));
IndexViewModel indexViewModel = (IndexViewModel)viewResult.ViewData.Model;
Assert.AreEqual(3, indexViewModel.Apuradores.Count);
}
Lo estoy haciendo bien?
Como mencionó Ladislav Mrnka, debes probar Linq-to-Entity pero no Linq-to-Object. Normalmente utilicé Sql CE como banco de prueba y siempre recreé la base de datos antes de cada prueba. Esto puede hacer que la prueba sea un poco lenta, pero hasta ahora estoy de acuerdo con el rendimiento de mis más de 100 unidades de pruebas.
Primero, cambie la configuración de la cadena de conexión con SqlCe en el App.config de su proyecto de prueba.
<connectionStrings>
<add name="MyDbContext"
connectionString="Data Source=|DataDirectory|MyDb.sdf"
providerName="System.Data.SqlServerCe.4.0"
/>
</connectionStrings>
En segundo lugar, configure el inicializador db con DropCreateDatabaseAlways .
Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());
Y luego, fuerce a EF para inicializar antes de ejecutar cada prueba.
public void Setup() {
Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());
context = new MyDbContext();
context.Database.Initialize(force: true);
}
Si está utilizando xunit, llame al método de instalación en su constructor. Si está utilizando MSTest, ponga TestInitializeAttribute en ese método. Si nunit .......
Desafortunadamente no lo está haciendo bien porque ese artículo está mal. Finge que FakeContext
hará que tu unidad de código sea comprobable, pero no lo hará. Una vez que expone IDbSet
o IDbSet
a su controlador y IDbSet
el conjunto en la recolección de memoria, nunca puede estar seguro de que la prueba de su unidad realmente prueba su código. Es muy fácil escribir una consulta LINQ en su controlador que pasará su prueba de unidad (porque FakeContext
usa LINQ-to-Objects) pero falla en tiempo de ejecución (porque su contexto real usa LINQ-to-Entities). Eso hace que el propósito de tu unidad sea inútil.
Mi opinión: no te molestes en simular el contexto si quieres exponer los sets al controlador. En su lugar, use pruebas de integración con una base de datos real para probar. Esa es la única forma de validar que las consultas LINQ definidas en el controlador hagan lo que usted espera.
Claro, si quieres llamar solo a ToList
o FirstOrDefault
en tus sets tu FakeContext
te servirá bien, pero una vez que hagas algo más complejo puedes encontrar una trampa muy pronto (simplemente pon la cadena "No se puede traducir en una expresión de la tienda" en Google - todos estos problemas aparecerán solo cuando ejecute Linq-to-entities, pero pasarán sus pruebas con Linq-to-objects).
Esta es una pregunta bastante común para que pueda verificar algunos otros ejemplos:
- Para devolver IQueryable o no devuelve IQueryable
- Pruebas unitarias DbContext
- ASP.NET MVC3 y la primera arquitectura del Entity Framework Code
- Desde el punto de vista organizativo, ¿dónde debería colocar consultas comunes cuando uso Entity Framework Code First?
- ¿Es posible anclar el contexto y las clases de Entity Framework para probar la capa de acceso a datos?
Puede crear un Fab DbContext utilizando Esfuerzo para EF 6+. Ver https://effort.codeplex.com/ . Esfuerzo significa E ntity F ramework F ake O bjectContext R ealization T ool.
Para un artículo con una muestra de trabajo, consulte http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Tool o http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx .
Sé que no deberíamos hacerlo, pero a veces tienes que hacerlo de todos modos (tu jefe también podría preguntarte, por ejemplo, y no cambiaría de opinión).
Entonces, como tuve que hacerlo, lo dejé aquí, podría ayudar a algunas personas. Soy bastante nuevo en c # / .net y todo por lo que está lejos de ser optimizado / limpio, supongo, pero parece funcionar.
siguiendo a MSDN Encuentre aquí la clase que falta y usando un poco de reflexión logré agregar propiedades de una vía: los elementos clave aquí son AddNavigationProperty y RefreshNavigationProperties . Si alguien tiene una sugerencia para mejorar este código, con gusto los tomaré
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
namespace MockFactory
{
public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
where TEntity : class
{
public readonly ObservableCollection<TEntity> _data;
private readonly IQueryable _query;
private readonly Dictionary<Type, object> entities;
public TestDbSet()
{
_data = new ObservableCollection<TEntity>();
_query = _data.AsQueryable();
entities = new Dictionary<Type, object>();
}
public override ObservableCollection<TEntity> Local
{
get { return _data; }
}
IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
{
return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
}
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
{
return _data.GetEnumerator();
}
Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
Expression IQueryable.Expression
{
get { return _query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
}
IEnumerator IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
public void AddNavigationProperty<T>(DbSet<T> dbSet) where T : class
{
entities.Add(typeof (T), dbSet);
}
public void RefreshNavigationProperty(TEntity item)
{
foreach (var entity in entities)
{
var property = item.GetType().GetProperty(entity.Key.Name);
var type =
(int)item.GetType().GetProperty(entity.Key.Name.Replace(typeof(TEntity).Name, "")).GetValue(item);
var dbSets = (IEnumerable<object>)entity.Value.GetType().GetField("_data").GetValue(entity.Value);
var dbSet = dbSets.Single(x => (int)x.GetType().GetProperty("Id").GetValue(x) == type);
property.SetValue(item, dbSet);
}
}
public override TEntity Add(TEntity item)
{
RefreshNavigationProperty(item);
_data.Add(item);
return item;
}
public override TEntity Remove(TEntity item)
{
_data.Remove(item);
return item;
}
public override TEntity Attach(TEntity item)
{
_data.Add(item);
return item;
}
public override TEntity Create()
{
return Activator.CreateInstance<TEntity>();
}
public override TDerivedEntity Create<TDerivedEntity>()
{
return Activator.CreateInstance<TDerivedEntity>();
}
}
}
Luego puedes crear tu contexto
public TestContext()
{
TypeUsers = new TestDbSet<TypeUser>();
StatusUsers = new TestDbSet<StatusUser>();
TypeUsers.Add(new TypeUser {Description = "FI", Id = 1});
TypeUsers.Add(new TypeUser {Description = "HR", Id = 2});
StatusUsers.Add(new StatusUser { Description = "Created", Id = 1 });
StatusUsers.Add(new StatusUser { Description = "Deleted", Id = 2 });
StatusUsers.Add(new StatusUser { Description = "PendingHR", Id = 3 });
Users = new TestDbSet<User>();
((TestDbSet<User>) Users).AddNavigationProperty(StatusUsers);
((TestDbSet<User>)Users).AddNavigationProperty(TypeUsers);
}
public override DbSet<TypeUser> TypeUsers { get; set; }
public override DbSet<StatusUser> StatusUsers { get; set; }
public override DbSet<User> Users { get; set; }
public int SaveChangesCount { get; private set; }
public override int SaveChanges(string modifierId)
{
SaveChangesCount++;
return 1;
}
}
Finalmente, no olvide en su prueba actualizar las propiedades de navegación antes de hacer la afirmación (debería haber una forma mejor, pero no pude encontrarla)
ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);