Cómo burlarse de las limitaciones de la implementación de IQueryable de EntityFramework

Actualmente estoy escribiendo pruebas unitarias para la implementación de mi repository en una aplicación MVC4. Para burlar el contexto de los datos, comencé adoptando algunas ideas de esta publicación , pero ahora he descubierto algunas limitaciones que me hacen cuestionar si es posible incluso IQueryable adecuadamente IQueryable .

En particular, he visto algunas situaciones donde las pruebas pasan pero el código falla en la producción y no he podido encontrar ninguna forma de burlarme del comportamiento que causa esta falla.

Por ejemplo, el siguiente fragmento de código se utiliza para seleccionar entidades de Post que se encuentran dentro de una lista predefinida de categorías:

 var posts = repository.GetEntities(); // Returns IQueryable var categories = GetCategoriesInGroup("Post"); // Returns a fixed list of type Category var filtered = posts.Where(p => categories.Any(c => c.Name == p.Category)).ToList(); 

En mi entorno de prueba, he intentado burlar las posts usando la implementación falsa de DbSet mencionada anteriormente, y también al crear una List de instancias de Post y convertirlas a IQueryable usando el método de extensión AsQueryable() . Ambos enfoques funcionan en condiciones de prueba, pero el código realmente falla en la producción, con la siguiente excepción:

System.NotSupportedException : Unable to create a constant value of type 'Category'. Only primitive types or enumeration types are supported in this context.

Aunque los problemas de LINQ como este son fáciles de solucionar, el verdadero desafío es encontrarlos, dado que no se revelan en el entorno de prueba.

¿Estoy siendo poco realista al esperar que pueda burlarme del comportamiento de la implementación de IQueryable de Entity Framework?

Gracias por tus ideas,

Tim.

Creo que es muy difícil, si es posible, simular el comportamiento de Entity Framework. En primer lugar, porque requeriría un profundo conocimiento de todas las peculiaridades y casos extremos en los que linq-to-entites difiere de linq-to-objects. Como dices: el verdadero desafío es encontrarlos. Permítanme señalar tres áreas principales sin pretender ser siquiera exhaustivas:

Casos donde Linq-to-Objects tiene éxito y Linq-to-Entities falla:

  • .Select(x => x.Property1.ToString() . LINQ to Entities no reconoce el método ‘System.String ToString ()’ método … Esto se aplica a casi todos los métodos en clases nativas .Net y por supuesto a propios métodos. Solo unos pocos métodos .Net se traducirán a SQL. Consulte el Método CLR para la Asignación de funciones canónicas . A partir de EF 6.1, ToString es compatible por cierto, pero solo la sobrecarga sin parámetros.
  • Skip() sin preceder OrderBy .
  • Except e Intersect : puede producir consultas monstruosas que arrojen Alguna parte de su statement SQL está anidada demasiado profundamente. Reescribe la consulta o divídela en consultas más pequeñas.
  • Select(x => x.Date1 - x.Date2) : los argumentos de DbArithmeticExpression deben tener un tipo común numérico.
  • (su caso). .Where(p => p.Category == category) : En este contexto, solo se admiten tipos primitivos o tipos de enumeración.
  • Nodes.Where(n => n.ParentNodes.First().Id == 1) : El método ‘Primero’ solo puede usarse como una operación de consulta final.
  • context.Nodes.Last() : LINQ to Entities no reconoce el método ‘… Last …’ . Esto se aplica a muchos otros métodos de extensión IQueryable . Ver Métodos LINQ soportados y no soportados .
  • (Ver el comentario de Slauma a continuación): .Select(x => new A { Property1 = (x.BoolProperty ? new B { BProp1 = x.Prop1, BProp2 = x.Prop2 } : new B { BProp1 = x.Prop1 }) }) : El tipo ‘B’ aparece en dos inicializaciones estructuralmente incompatibles dentro de una sola consulta LINQ a Entidades … desde aquí .
  • context.Entities.Cast() : no se puede convertir el tipo ‘Entity’ para escribir ‘IEntity’. LINQ to Entities solo admite la conversión de primitiva EDM o tipos de enumeración.
  • .Select(p => p.Category?.Name) . El uso de propagación nula en una expresión arroja CS8072 Un árbol de expresiones lambda puede no contener un operador de propagación nulo. Esto puede arreglarse un día .
  • Esta pregunta: ¿Por qué esta combinación de Select, Where y GroupBy causa una excepción? me hizo consciente del hecho de que incluso hay construcciones de consulta completas que no son compatibles con EF, mientras que L2O no tendría ningún problema con ellas.

Casos en los que Linq-to-Objects falla y Linq-to-Entities tiene éxito:

  • .Select(p => p.Category.Name) : cuando p.Category es nulo, L2E devuelve null, pero L2O lanza Object reference no establecido en una instancia de un objeto. Esto no se puede solucionar usando la propagación nula (ver arriba).
  • Nodes.Max(n => n.ParentId.Value) con algunos valores nulos para n.ParentId . L2E devuelve un valor máximo, L2O arroja objetos Nullable debe tener un valor.
  • Usar EntityFunctions ( DbFunctions de DbFunctions partir de EF 6) o SqlFunctions .

Casos donde ambos tienen éxito / fallan pero se comportan de manera diferente:

  • Nodes.Include("ParentNodes") : L2O no tiene implementación de incluir. Ejecutará y devolverá nodos (si los Nodes son IQueryable ), pero sin los nodos principales.
  • Nodes.Select(n => n.ParentNodes.Max(p => p.Id)) con algunas colecciones ParentNodes vacías: ambas fallan pero con diferentes excepciones.
  • Nodes.Where(n => n.Name.Contains("par")) : L2O distingue entre mayúsculas y minúsculas, L2E depende de la intercalación de la base de datos (a menudo no es sensible a mayúsculas y minúsculas).
  • node.ParentNode = parentNode : con una relación bidireccional, en L2E esto también agregará el nodo a la colección de nodos del padre ( corrección de relación ). No en L2O. (Ver Unidad probando una relación EF bidireccional ).
  • .Select(p => p.Category == null ? string.Empty : p.Category.Name) de problemas para la propagación nula .Select(p => p.Category == null ? string.Empty : p.Category.Name) : .Select(p => p.Category == null ? string.Empty : p.Category.Name) : el resultado es el mismo, pero la consulta SQL generada también contiene la verificación nula y puede ser más difícil de optimizar
  • Nodes.AsNoTracking().Select(n => n.ParentNode . ¡Este es muy complicado! Con AsNoTracking EF crea nuevos objetos ParentNode para cada Node , por lo que puede haber duplicados. Sin AsNoTracking EF reutiliza los ParentNodes existentes, porque ahora la entidad el administrador de estado y las claves de entidad están involucradas. AsNoTracking() se puede llamar en L2O, pero no hace nada, por lo que nunca habrá una diferencia con o sin él.

¿Y qué hay sobre la burla de la carga floja / ansiosa y el efecto del ciclo de vida del contexto en las excepciones de carga diferida? O el efecto de algunas construcciones de consulta en el rendimiento (como construcciones que desencadenan N + 1 consultas SQL). ¿O excepciones debido a llaves de entidad duplicadas o faltantes? ¿O corrección de relación?

Mi opinión: nadie va a fingir eso. El área más alarmante es donde L2O tiene éxito y L2E falla. Ahora, ¿cuál es el valor de las pruebas de unidades verdes? Se ha dicho antes que EF solo puede probarse confiablemente en pruebas de integración (por ejemplo, aquí ) y estoy de acuerdo.

Sin embargo, eso no significa que debamos olvidarnos de las pruebas unitarias en proyectos con EF como capa de datos. Hay formas de hacerlo , pero, creo, no sin pruebas de integración.

He escrito algunas pruebas unitarias con Entity Framework 6.1.3 usando Moq y lo he utilizado para anular IQueryable . Tenga en cuenta que todos los DbSet que deben probarse deben marcarse como virtual . Ejemplo de Microsoft ellos mismos:

Consulta:

 using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System.Collections.Generic; using System.Data.Entity; using System.Linq; namespace TestingDemo { [TestClass] public class QueryTests { [TestMethod] public void GetAllBlogs_orders_by_name() { var data = new List { new Blog { Name = "BBB" }, new Blog { Name = "ZZZ" }, new Blog { Name = "AAA" }, }.AsQueryable(); var mockSet = new Mock>(); mockSet.As>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As>().Setup(m => m.GetEnumerator()).Returns(0 => data.GetEnumerator()); var mockContext = new Mock(); mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); var service = new BlogService(mockContext.Object); var blogs = service.GetAllBlogs(); Assert.AreEqual(3, blogs.Count); Assert.AreEqual("AAA", blogs[0].Name); Assert.AreEqual("BBB", blogs[1].Name); Assert.AreEqual("ZZZ", blogs[2].Name); } } } 

Insertar:

 using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System.Data.Entity; namespace TestingDemo { [TestClass] public class NonQueryTests { [TestMethod] public void CreateBlog_saves_a_blog_via_context() { var mockSet = new Mock>(); var mockContext = new Mock(); mockContext.Setup(m => m.Blogs).Returns(mockSet.Object); var service = new BlogService(mockContext.Object); service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet"); mockSet.Verify(m => m.Add(It.IsAny()), Times.Once()); mockContext.Verify(m => m.SaveChanges(), Times.Once()); } } } 

Ejemplo de servicio:

 using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Threading.Tasks; namespace TestingDemo { public class BlogService { private BloggingContext _context; public BlogService(BloggingContext context) { _context = context; } public Blog AddBlog(string name, string url) { var blog = _context.Blogs.Add(new Blog { Name = name, Url = url }); _context.SaveChanges(); return blog; } public List GetAllBlogs() { var query = from b in _context.Blogs orderby b.Name select b; return query.ToList(); } public async Task> GetAllBlogsAsync() { var query = from b in _context.Blogs orderby b.Name select b; return await query.ToListAsync(); } } } 

Fuente: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

    Intereting Posts