2011-07-11 20 views
12

Estoy usando EF 4.1 para construir un modelo de dominio. Tengo una clase de tareas con un método Validar (cadena de código de usuario) y en él quiero asegurar los mapas de código de usuario a un usuario válido en la base de datos, por lo que:Prueba de Moq LINQ Donde consultas

public static bool Validate(string userCode) 
{ 
    IDbSet<User> users = db.Set<User>(); 
    var results = from u in users 
       where u.UserCode.Equals(userCode) 
       select u; 
    return results.FirstOrDefault() != null; 
} 

puedo usar Moq burlarse IDbSet ningún problema . Pero se metió en problemas con la llamada Dónde:

User user = new User { UserCode = "abc" }; 
IList<User> list = new List<User> { user }; 
var users = new Mock<IDbSet<User>>(); 
users.Setup(x => x.Where(It.IsAny<Expression<Func<User, bool>>>())).Returns(list.AsQueryable); 

Initialization method JLTi.iRIS3.Tests.TaskTest.SetUp threw exception. 
System.NotSupportedException: System.NotSupportedException: Expression 
references a method that does not belong to the mocked object: 
x => x.Where<User>(It.IsAny<Expression`1>()). 

Aparte de la creación de un nivel de indirección (por ejemplo, usando un ServiceLocator para obtener un objeto que ejecuta el LINQ y luego burlarse de ese método) No puedo pensar en cómo más para probar esto, pero quiero asegurarme de que no haya forma antes de presentar otra capa. Y puedo ver que este tipo de consultas LINQ se necesitarán con bastante frecuencia para que los objetos de servicio puedan perder el control rápidamente.

¿Algún alma amable podría ayudar? ¡Gracias!

+1

¿Hay algún motivo por el que intente sacarlo de allí con el uso de simulaciones en una prueba unitaria en lugar de simplemente escribir una prueba de integración? – csano

+2

No es necesario que realice la prueba unitaria EF :) – Eranga

+0

El método Validate() en realidad está simplificado. Tiene muchos argumentos y cada argumento es probado. Algunas son simples verificaciones de rango, algunas son verificaciones de existencia de DB (como el código de usuario aquí). –

Respuesta

9

Como sé Moq es capaz de establecer métodos sólo virtuales del propio objeto burlado, pero que están tratando de establecer el método extensiones (estática) - de ninguna manera! Estos métodos están absolutamente fuera de su alcance simulado.

Además, ese código es incorrecto y difícil de probar. Necesita mucha inicialización para poder probarlo. Use este lugar:

internal virtual IQueryable<User> GetUserSet() 
{ 
    return db.Set<User>(); 
} 

public bool Validate(string userCode) 
{ 
    IQueryable<User> users = GetUserSet(); 
    var results = from u in users 
        where u.UserCode.Equals(userCode) 
        select u; 
    return results.FirstOrDefault() != null; 
} 

Usted sólo tendrá que configurar GetUserSet para devolver su lista. Este tipo de pruebas tiene algunos problemas importantes:

  • no está probando la implementación real - en el caso de EF burlarse de conjuntos es el enfoque estúpida porque una vez que lo haces cambio LINQ a las entidades a LINQ a objetos. Esos dos son totalmente diferentes y linq-to-entities es solo un pequeño subconjunto de linq-to-objects = las pruebas de su unidad pueden pasar con linq-to-objects pero su código fallará en tiempo de ejecución.
  • Una vez que utiliza este enfoque, no puede usar Include porque include depende de DbQuery/DbSet. De nuevo necesitas una prueba de integración para usarla.
  • Esto no prueba que su carga diferida funciona

El mejor enfoque es la eliminación de sus consultas LINQ de Validate método - sólo los llaman como otro método virtual del objeto. Pruebe por unidad su método Validate con métodos de consulta simulados y use pruebas de integración para probar las consultas.

+0

Entiendo tu punto acerca de eliminar LINQ de Validate. El problema es que Validate es estático, por lo que GetUserSet no se puede burlar. Tendré que crear artificialmente una nueva clase de ayuda para implementar GetUserSet; eso es lo que quise decir al crear un nivel de indirección. –

9

Aunque no lo he intentado, porque IDBSet implementa IEnumerable, puede que tenga que simular el método del enumerador para que las instrucciones linq recojan su lista de usuarios. En realidad, no desea simular el linq pero, según el aspecto de su código, desea probar si está buscando el usuario correcto según el UserCode, que creo que es una prueba de unidad válida.

var user = new User { UserCode = "abc" }; 
var list = new List<User> { user }; 
var users = new Mock<IDbSet<User>>(); 
users.Setup(x => x.GetEnumerator()).Returns(list.GetEnumerator()); 

Usted puede obtener un conflicto con la versión no genérica de la GetEnumerator pero esto le sirva de ayuda en el camino correcto. Luego debe colocar el objeto simulado en el contexto de datos que depende de otro código que no vemos.

+0

Gracias ... temía escribir una implementación completa de IDbSet . –

+7

Lo hice, pero luego la ejecución de '.Where' en ese objeto simulado arroja una excepción. – CoderDennis

4

me pareció más fácil sólo para escribir el talón:

internal class FakeDbSet<T> : IDbSet<T>where T : class 
{ 
    readonly HashSet<T> _data; 
    readonly IQueryable _query; 

    public FakeDbSet() 
    { 
     _data = new HashSet<T>(); 
     _query = _data.AsQueryable(); 
    } 

    public virtual T Find(params object[] keyValues) 
    { 
     throw new NotImplementedException("Derive from FakeDbSet<T> and override Find"); 
    } 

    public T Add(T item) 
    { 
     _data.Add(item); 
     return item; 
    } 

    public T Remove(T item) 
    { 
     _data.Remove(item); 
     return item; 
    } 

    public T Attach(T item) 
    { 
     _data.Add(item); 
     return item; 
    } 

    public void Detach(T item) 
    { 
     _data.Remove(item); 
    } 

    Type IQueryable.ElementType 
    { 
     get { return _query.ElementType; } 
    } 

    Expression IQueryable.Expression 
    { 
     get { return _query.Expression; } 
    } 

    IQueryProvider IQueryable.Provider 
    { 
     get { return _query.Provider; } 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return _data.GetEnumerator(); 
    } 

    IEnumerator<T> IEnumerable<T>.GetEnumerator() 
    { 
     return _data.GetEnumerator(); 
    } 


    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T 
    { 
     return Activator.CreateInstance<TDerivedEntity>(); 
    } 

    public T Create() 
    { 
     return Activator.CreateInstance<T>(); 
    } 

    public ObservableCollection<T> Local 
    { 
     get 
     { 

      return new ObservableCollection<T>(_data); 
     } 
    } 
16

There is an article on MSDN highlighting how to mock using moq: El quid de la cuestión es la de representar LINQ a las operaciones con entidades LINQ a objetos.

var mockSet = new Mock<DbSet<Blog>>(); 
mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider); 
mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); 
mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); 
mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); 

Como señala Ladislav hay desventajas a esto como LINQ to Objects es simplemente diferente a LINQ a Entidades por lo que puede dar lugar a falsos positivos. Pero ahora, al tratarse de un artículo de MSDN, señala que, al menos, es posible y quizás se recomiende en algunos casos.

Una cosa que puede haber cambiado desde las respuestas originales a esta publicación es que el equipo de Entity Framework ha abierto áreas de Entity Framework en EF 6.0 para que sea más fácil burlarse de sus internos.

+1

¡Genial! Lo necesitaba hoy, qué coincidencia que acabas de publicar también :) Funciona muy bien para mí. – demoncodemonkey