7

He leído docenas de publicaciones sobre PRO y CONs de intentar simular/falsificar EF en la lógica comercial. Todavía no he decidido qué hacer, pero una cosa sí sé es que debo separar las consultas de la lógica comercial. En this post he visto que la Ladislav ha contestado que hay 2 buenas maneras:Decouple EF consultas de BL - Métodos de extensión VS Clase por consulta

  1. Sean dónde se encuentran y utilizan métodos de extensión personalizados, consultar, Vistas de base de datos asignado o la definición de encargo consultas para definir las partes reutilizables .
  2. Expone cada consulta individual como método en alguna clase separada. El método no debe exponer IQueryable y no debe aceptar Expression como parámetro = . La lógica de consulta completa debe estar envuelta en el método. Pero esto hará que su clase cubra métodos relacionados de manera similar al repositorio (el único que puede ser burlado o falso). Esta implementación está cerca de la implementación utilizada con procedimientos almacenados.
  1. ¿Qué método crees que es mejor cualquier qué?
  2. ¿Hay CUALQUIER desventajas para poner las consultas en su propio lugar? (Tal vez la pérdida de algunas funciones de EF o algo por el estilo)
  3. ¿Tengo que encapsular incluso las consultas más simples como:

    using (MyDbContext entities = new MyDbContext) 
    { 
        User user = entities.Users.Find(userId); // ENCAPSULATE THIS ? 
    
        // Some BL Code here 
    } 
    

Respuesta

7

así que supongo que el punto principal es la capacidad de prueba de su código, ISN no es así? En tal caso, debe comenzar contando las responsabilidades del método que desea probar y luego refactorizar su código utilizando un patrón de responsabilidad único.

Su código de ejemplo tiene al menos tres responsabilidades:

  • Creación de un objeto es una responsabilidad - contexto es un objeto. Además, es un objeto que no desea utilizar en su prueba unitaria, por lo que debe mover su creación a otra parte.
  • La ejecución de consultas es responsabilidad. Además, es una responsabilidad que le gustaría evitar en su prueba de unidad.
  • Haciendo un poco de lógica de negocio es una responsabilidad

Para simplificar las pruebas que debe refactorizar el código y dividir las responsabilidades a métodos separados.

public class MyBLClass() 
{ 
    public void MyBLMethod(int userId) 
    { 
     using (IMyContext entities = GetContext()) 
     { 
      User user = GetUserFromDb(entities, userId); 

      // Some BL Code here 
     } 
    } 

    protected virtual IMyContext GetContext() 
    { 
     return new MyDbContext(); 
    } 

    protected virtual User GetUserFromDb(IMyDbContext entities, int userId) 
    { 
     return entities.Users.Find(userId); 
    } 
} 

Ahora unidad lógica de negocio de pruebas debería ser pan comido porque su unidad de prueba puede heredar el método de la clase y el método factoría de contexto falso y ejecución de la consulta y llegar a ser totalmente independiente el EF.

// NUnit unit test 
[TestFixture] 
public class MyBLClassTest : MyBLClass 
{ 
    private class FakeContext : IMyContext 
    { 
     // Create just empty implementation of context interface 
    } 

    private User _testUser; 

    [Test] 
    public void MyBLMethod_DoSomething() 
    { 
     // Test setup 
     int id = 10; 
     _testUser = new User 
      { 
       Id = id, 
       // rest is your expected test data - that is what faking is about 
       // faked method returns simply data your test method expects 
      }; 

     // Execution of method under test 
     MyBLMethod(id); 

     // Test validation 
     // Assert something you expect to happen on _testUser instance 
     // inside MyBLMethod 
    } 

    protected override IMyContext GetContext() 
    { 
     return new FakeContext(); 
    } 

    protected override User GetUserFromDb(IMyContext context, int userId) 
    { 
     return _testUser.Id == userId ? _testUser : null; 
    } 
} 

A medida que agrega más métodos y su aplicación crece va a refactorizar los métodos de ejecución de consultas y método factoría de contexto a clases separadas para seguir la responsabilidad única de clases, así - obtendrá factoría de contexto y, o bien algún proveedor de consulta o en algunos casos repositorio (pero ese repositorio nunca devolverá IQueryable ni obtendrá Expression como parámetro en ninguno de sus métodos). Esto también le permitirá seguir el principio DRY donde su creación de contexto y las consultas más comúnmente utilizadas se definirán solo una vez en un lugar central.

Así que al final puede tener algo como esto:

public class MyBLClass() 
{ 
    private IContextFactory _contextFactory; 
    private IUserQueryProvider _userProvider; 

    public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider) 
    { 
     _contextFactory = contextFactory; 
     _userProvider = userProvider; 
    } 

    public void MyBLMethod(int userId) 
    { 
     using (IMyContext entities = _contextFactory.GetContext()) 
     { 
      User user = _userProvider.GetSingle(entities, userId); 

      // Some BL Code here 
     } 
    } 
} 

Cuando esas interfaces se verá como:

public interface IContextFactory 
{ 
    IMyContext GetContext(); 
} 

public class MyContextFactory : IContextFactory 
{ 
    public IMyContext GetContext() 
    { 
     // Here belongs any logic necessary to create context 
     // If you for example want to cache context per HTTP request 
     // you can implement logic here. 
     return new MyDbContext(); 
    } 
} 

y

public interface IUserQueryProvider 
{ 
    User GetUser(int userId); 

    // Any other reusable queries for user entities 
    // Non of queries returns IQueryable or accepts Expression as parameter 
    // For example: IEnumerable<User> GetActiveUsers(); 
} 

public class MyUserQueryProvider : IUserQueryProvider 
{ 
    public User GetUser(IMyContext context, int userId) 
    { 
     return context.Users.Find(userId); 
    } 

    // Implementation of other queries 

    // Only inside query implementations you can use extension methods on IQueryable 
} 

Su prueba ahora solo uso falsificaciones para la fábrica de contexto y el proveedor de consultas.

// NUnit + Moq unit test 
[TestFixture] 
public class MyBLClassTest 
{ 
    private class FakeContext : IMyContext 
    { 
     // Create just empty implementation of context interface 
    } 

    [Test] 
    public void MyBLMethod_DoSomething() 
    { 
     // Test setup 
     int id = 10; 
     var user = new User 
      { 
       Id = id, 
       // rest is your expected test data - that is what faking is about 
       // faked method returns simply data your test method expects 
      }; 

     var contextFactory = new Mock<IContextFactory>(); 
     contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext()); 

     var queryProvider = new Mock<IUserQueryProvider>(); 
     queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user); 

     // Execution of method under test 
     var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object); 
     myBLClass.MyBLMethod(id); 

     // Test validation 
     // Assert something you expect to happen on user instance 
     // inside MyBLMethod 
    } 
} 

Sería poco diferente en el caso de repositorio que debe tener relación con el contexto pasada a su constructor antes de inyectar a su clase de negocios. Su clase de negocio aún puede definir algunas consultas que nunca se usan en ninguna otra clase; esas consultas probablemente sean parte de su lógica. También puede usar métodos de extensión para definir algunas partes reutilizables de las consultas, pero siempre debe usar esos métodos de extensión fuera de la lógica empresarial central que desee probar unitariamente (en los métodos de ejecución de consultas o en el proveedor/repositorio de consultas). Eso le permitirá fácilmente falsificar el proveedor de consultas o los métodos de ejecución de consultas.

Vi your previous question y pensé en escribir una publicación de blog sobre ese tema, pero el núcleo de mi opinión sobre las pruebas con EF está en esta respuesta.

Editar:

repositorio es diferente tema que no corresponda a su pregunta original. El repositorio específico sigue siendo un patrón válido. No estamos en contra de los repositorios, we are against generic repositories porque no proporcionan ninguna característica adicional y no resuelven ningún problema.

El problema es que el repositorio por sí solo no resuelve nada. Hay tres patrones que deben usarse juntos para formar la abstracción adecuada: Repositorio, Unidad de trabajo y Especificaciones. Los tres ya están disponibles en EF: DbSet/ObjectSet como repositorios, DbContext/ObjectContext como Unidad de trabajos y Linq para entidades como especificaciones. El principal problema con la implementación personalizada de repositorios genéricos mencionados en todas partes es que reemplazan solo el repositorio y la unidad de trabajo con implementación personalizada, pero aún dependen de las especificaciones originales => la abstracción es incompleta y se filtra en pruebas donde el repositorio falso se comporta de la misma manera que conjunto/contexto falso

La principal desventaja de mi proveedor de consultas es el método explícito para cualquier consulta que necesite ejecutar. En el caso del repositorio no tendrá dichos métodos, tendrá pocos métodos que acepten la especificación (pero, una vez más, esas especificaciones deben definirse en el principio DRY) que generarán las condiciones de filtrado de consultas, pedidos, etc.

public interface IUserRepository 
{ 
    User Find(int userId); 
    IEnumerable<User> FindAll(ISpecification spec); 
} 

La discusión de este tema está más allá del alcance de esta pregunta y requiere que estudie un poco de su propio estudio.

Btw. burlarse y fingir tiene un propósito diferente: simula una llamada si necesita obtener datos de prueba del método en la dependencia y se burla de la llamada si necesita afirmar que el método en la dependencia se invocó con los argumentos esperados.

+0

Estoy tan contenta de que haya sido usted quien haya respondido, ya que parece ser el "amigo" con respecto a la capacidad de EF. Tengo un par de preguntas: dijiste ** obtendrás la fábrica de contexto y algún proveedor de consultas o en algunos casos repositorio (pero ese repositorio nunca devolverá IQueryable ni obtendrá Expression como parámetro en ninguno de sus métodos). Esto también le permitirá seguir el principio DRY donde su creación de contexto y las consultas más comúnmente utilizadas se definirán solo una vez en un lugar central. ** - Pensé que estaba totalmente en contra de usar el repositorio. ¿Puedes aclarar con un ejemplo? –

+0

¿Puede aclarar también esta frase? ** "Sería un poco diferente en el caso de repositorio que debería tener referencia al contexto pasado a su constructor antes de inyectarlo a su clase de negocio." ** (un ejemplo de un repositorio de este tipo y cuando uno debería usarlo sería muy apreciado) –

+0

y finalmente - el último bloque que escribió: ** Su clase de negocio aún puede definir algunas consultas que nunca se usan en ninguna otra clase; esas consultas probablemente sean parte de su lógica. ** - su primer ejemplo de código utilizó esta técnica, ¿verdad? ¿Cuándo usaría eso y cuándo usaría los métodos de extensión? ¿A dónde van estos métodos de extensión? –

Cuestiones relacionadas