¿Cómo se prueba la unidad de personas con Entity Framework 6?


170

Estoy comenzando con las pruebas de la Unidad y TDD en general. He incursionado antes pero ahora estoy decidido a agregarlo a mi flujo de trabajo y escribir un mejor software.

Ayer hice una pregunta que incluía esto, pero parece ser una pregunta por sí sola. Me he sentado para comenzar a implementar una clase de servicio que usaré para abstraer la lógica de negocios de los controladores y asignarla a modelos específicos e interacciones de datos usando EF6.

El problema es que ya me bloqueé porque no quería abstraer EF en un repositorio (todavía estará disponible fuera de los servicios para consultas específicas, etc.) y me gustaría probar mis servicios (se usará Contexto EF) .

Aquí supongo que es la pregunta, ¿hay algún punto para hacer esto? Si es así, ¿cómo lo está haciendo la gente en la naturaleza a la luz de las abstracciones permeables causadas por IQueryable y las muchas publicaciones excelentes de Ladislav Mrnka sobre el tema de las pruebas unitarias que no son sencillas debido a las diferencias en los proveedores de Linq cuando se trabaja con una memoria? implementación según lo propuesto a una base de datos específica.

El código que quiero probar parece bastante simple. (esto es solo un código ficticio para tratar de entender lo que estoy haciendo, quiero conducir la creación usando TDD)

Contexto

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

Servicio

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

Actualmente estoy en la mentalidad de hacer algunas cosas:

  1. Contexto burlón de EF con algo como este enfoque: burlándose de EF cuando pruebas unitarias o directamente usando un marco burlón en la interfaz como moq, ¿tomando el dolor de que las pruebas unitarias pueden pasar pero no necesariamente funcionan de extremo a extremo y respaldarlas con pruebas de integración?
  2. Tal vez usando algo como Effort para burlarse de EF: nunca lo he usado y no estoy seguro de si alguien más lo está usando en la naturaleza.
  3. No se molesta en probar nada que simplemente llame a EF, por lo que, esencialmente, ¿los métodos de servicio que llaman a EF directamente (getAll, etc.) no se prueban en la unidad sino que solo se prueban en la integración?

¿Alguien por ahí está haciendo esto sin un Repo y teniendo éxito?


Hola Modika, estaba pensando en esto recientemente (debido a esta pregunta: stackoverflow.com/questions/25977388/… ) En él trato de describir un poco más formalmente cómo trabajo en este momento, pero me encantaría saber cómo lo estas haciendo
Samy

Hola @samy, la forma en que decidimos hacerlo no fue una prueba unitaria de nada que haya afectado directamente a EF. Las consultas se probaron pero como prueba de integración, no como pruebas unitarias. Burlarse de EF se siente un poco sucio, pero este proyecto fue pequeño, por lo que el impacto en el rendimiento de tener muchas pruebas en una base de datos no fue realmente una preocupación, por lo que podríamos ser un poco más pragmáticos al respecto. Todavía no estoy 100% seguro de cuál es el mejor enfoque para ser completamente sincero con usted, en algún momento va a alcanzar EF (y su DB) y las pruebas unitarias no me parecen correctas aquí.
Modika

Respuestas:


186

Este es un tema que me interesa mucho. Hay muchos puristas que dicen que no debe probar tecnologías como EF y NHibernate. Tienen razón, ya se han probado de manera muy estricta y, como una respuesta anterior declaró, a menudo no tiene sentido gastar grandes cantidades de tiempo probando lo que no posee.

Sin embargo, usted posee la base de datos debajo.Aquí es donde este enfoque, en mi opinión, se rompe, no es necesario probar que EF / NH están haciendo su trabajo correctamente. Debe comprobar que sus asignaciones / implementaciones funcionan con su base de datos. En mi opinión, esta es una de las partes más importantes de un sistema que puede probar.

Hablando estrictamente, sin embargo, nos estamos moviendo fuera del dominio de las pruebas unitarias y en las pruebas de integración, pero los principios siguen siendo los mismos.

Lo primero que debe hacer es poder burlarse de su DAL para que su BLL pueda probarse independientemente de EF y SQL. Estas son sus pruebas unitarias. A continuación, debe diseñar sus pruebas de integración para probar su DAL, en mi opinión, estas son igual de importantes.

Hay un par de cosas a considerar:

  1. Su base de datos debe estar en un estado conocido con cada prueba. La mayoría de los sistemas usan una copia de seguridad o crean scripts para esto.
  2. Cada prueba debe ser repetible
  3. Cada prueba debe ser atómica

Hay dos enfoques principales para configurar su base de datos, el primero es ejecutar un script de creación de DB de UnitTest. Esto asegura que su base de datos de prueba de unidad siempre estará en el mismo estado al comienzo de cada prueba (puede restablecer esto o ejecutar cada prueba en una transacción para garantizar esto).

Su otra opción es lo que hago, ejecutar configuraciones específicas para cada prueba individual. Creo que este es el mejor enfoque por dos razones principales:

  • Su base de datos es más simple, no necesita un esquema completo para cada prueba
  • Cada prueba es más segura, si cambia un valor en su script de creación, no invalida docenas de otras pruebas.

Lamentablemente, su compromiso aquí es la velocidad. Lleva tiempo ejecutar todas estas pruebas, ejecutar todos estos scripts de configuración / desmontaje.

Un último punto, puede ser muy difícil escribir una cantidad tan grande de SQL para probar su ORM. Aquí es donde adopto un enfoque muy desagradable (los puristas aquí estarán en desacuerdo conmigo). ¡Utilizo mi ORM para crear mi prueba! En lugar de tener un script separado para cada prueba DAL en mi sistema, tengo una fase de configuración de prueba que crea los objetos, los adjunta al contexto y los guarda. Luego ejecuto mi prueba.

Esto está lejos de ser la solución ideal, sin embargo, en la práctica, encuentro que es MUCHO más fácil de administrar (especialmente cuando tiene varios miles de pruebas), de lo contrario, está creando un gran número de scripts. Practicidad sobre la pureza.

Sin duda volveré a mirar esta respuesta en unos pocos años (meses / días) y estaré en desacuerdo conmigo mismo ya que mis enfoques han cambiado; sin embargo, este es mi enfoque actual.

Para intentar resumir todo lo que he dicho anteriormente, esta es mi típica prueba de integración de base de datos:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

La clave para notar aquí es que las sesiones de los dos bucles son completamente independientes. En su implementación de RunTest, debe asegurarse de que el contexto se confirma y destruye y sus datos solo pueden provenir de su base de datos para la segunda parte.

Editar 13/10/2014

Dije que probablemente revisaría este modelo en los próximos meses. Si bien defiendo en gran medida el enfoque que defendí anteriormente, actualicé ligeramente mi mecanismo de prueba. Ahora tiendo a crear las entidades en TestSetup y TestTearDown.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Luego pruebe cada propiedad individualmente

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Hay varias razones para este enfoque:

  • No hay llamadas adicionales a la base de datos (una configuración, un desmontaje)
  • Las pruebas son mucho más granulares, cada prueba verifica una propiedad
  • La lógica de configuración / TearDown se elimina de los propios métodos de prueba

Creo que esto hace que la clase de prueba sea más simple y las pruebas más granulares (las afirmaciones simples son buenas )

Editar 3/5/2015

Otra revisión de este enfoque. Si bien las configuraciones de nivel de clase son muy útiles para pruebas como las propiedades de carga, son menos útiles cuando se requieren las diferentes configuraciones. En este caso, configurar una nueva clase para cada caso es excesivo.

Para ayudar con esto, ahora tiendo a tener dos clases base SetupPerTesty SingleSetup. Estas dos clases exponen el marco según sea necesario.

En el SingleSetuptenemos un mecanismo muy similar al descrito en mi primera edición. Un ejemplo sería

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

Sin embargo, las referencias que aseguran que solo se cargan las entidades correctas pueden usar un enfoque SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

En resumen, ambos enfoques funcionan según lo que intente probar.


2
Aquí hay un enfoque diferente para las pruebas de integración. TL; DR: use la aplicación en sí para configurar los datos de prueba, deshaga una transacción por prueba.
Gert Arnold

3
@Liath, gran respuesta. Has confirmado mis sospechas acerca de probar EF. Mi pregunta es esta; su ejemplo es para un caso muy concreto, que está bien. Sin embargo, como notó, es posible que deba probar cientos de entidades. De acuerdo con el principio DRY (No repetir), ¿cómo escala su solución, sin repetir el mismo patrón de código básico cada vez?
Jeffrey A. Gochin

44
Tengo que estar en desacuerdo con esto porque evita completamente el problema. La prueba unitaria se trata de probar la lógica de la función. En el ejemplo de OP, la lógica depende de un almacén de datos. Tienes razón cuando dices no probar EF, pero ese no es el problema. El problema es probar su código de forma aislada del almacén de datos. Probar su mapeo es un tema totalmente diferente. Para comprobar que la lógica está interactuando correctamente con los datos, debe poder controlar la tienda.
Sinaesthetic

77
Nadie está en la cerca acerca de si debe probar la unidad Entity Framework por sí mismo. Lo que sucede es que necesita probar algún método que haga algunas cosas y también haga una llamada EF a la base de datos. El objetivo es burlarse de EF para que pueda probar este método sin requerir una base de datos en su servidor de compilación.
The Muffin Man

44
Realmente me gusta el viaje. Gracias por agregar ediciones a lo largo del tiempo: es como leer el control de la fuente y comprender cómo ha evolucionado su pensamiento. Realmente aprecio la distinción funcional (con EF) y unidad (EF burlado) también.
Tom Leys

21

Esfuerzo Experiencia Comentarios aquí

Después de mucha lectura, he estado usando Effort en mis pruebas: durante las pruebas, el Context está construido por una fábrica que devuelve una versión en memoria, lo que me permite probar una pizarra en blanco cada vez. Fuera de las pruebas, la fábrica se resuelve en una que devuelve todo el contexto.

Sin embargo, tengo la sensación de que las pruebas contra una simulación completa de la base de datos tienden a arrastrar las pruebas hacia abajo; te das cuenta de que debes encargarte de configurar un montón de dependencias para probar una parte del sistema. También tiende a derivar hacia la organización de pruebas que pueden no estar relacionadas, solo porque solo hay un gran objeto que maneja todo. Si no presta atención, puede encontrarse haciendo pruebas de integración en lugar de pruebas unitarias

Hubiera preferido realizar pruebas contra algo más abstracto en lugar de un gran DBContext, pero no pude encontrar el punto óptimo entre las pruebas significativas y las pruebas básicas. Lo atribuyo a mi inexperiencia.

Así que encuentro el esfuerzo interesante; Si necesita comenzar a ejecutar, es una buena herramienta para comenzar rápidamente y obtener resultados. Sin embargo, creo que algo un poco más elegante y abstracto debería ser el siguiente paso y eso es lo que voy a investigar a continuación. Favorito de esta publicación para ver a dónde va después :)

Editar para agregar : el esfuerzo tarda un poco en calentarse, por lo que estás viendo aprox. 5 segundos al inicio de la prueba. Esto puede ser un problema para usted si necesita que su conjunto de pruebas sea muy eficiente.


Editado para aclaración:

Utilicé Effort para probar una aplicación de servicio web. Cada mensaje M que ingresa se enruta a unIHandlerOf<M> través de Windsor. Castle.Windsor resuelve lo IHandlerOf<M>que resuelve las dependencias del componente. Una de estas dependencias es laDataContextFactory , que permite al manejador preguntar por la fábrica.

En mis pruebas, ejemplifico el componente IHandlerOf directamente, me burlo de todos los subcomponentes del SUT y maneja el esfuerzo envuelto DataContextFactory en el controlador.

Significa que no realizo pruebas unitarias en sentido estricto, ya que el DB es afectado por mis pruebas. Sin embargo, como dije anteriormente, me permitió comenzar a ejecutar y pude probar rápidamente algunos puntos en la aplicación


Gracias por el aporte, lo que puedo hacer ya que tengo que ejecutar este proyecto, ya que es un trabajo de buena fe, es comenzar con algunos repositorios y ver cómo me va, pero el esfuerzo es muy interesante. ¿No le interesa en qué capa ha estado utilizando el esfuerzo en sus aplicaciones?
Modika

2
solo si Effort hubiera respaldado las transacciones correctamente
Sedat Kapanoglu

y el esfuerzo tiene un error para las cadenas con el cargador csv, cuando usamos '' en lugar de nulo en las cadenas.
Sam

13

Si desea unidad de código de prueba, entonces usted necesita para aislar su código desea probar (en este caso su servicio) a partir de recursos externos (por ejemplo, bases de datos). Probablemente podría hacer esto con algún tipo de proveedor de EF en memoria , sin embargo, una forma mucho más común es abstraer su implementación de EF, por ejemplo, con algún tipo de patrón de repositorio. Sin este aislamiento, todas las pruebas que escriba serán pruebas de integración, no pruebas unitarias.

En cuanto a la prueba del código EF: escribo pruebas de integración automatizadas para mis repositorios que escriben varias filas en la base de datos durante su inicialización, y luego llamo a las implementaciones de mi repositorio para asegurarme de que se comporten como se espera (por ejemplo, asegurándome de que los resultados se filtren correctamente, o que están ordenados en el orden correcto).

Estas son pruebas de integración, no pruebas unitarias, ya que las pruebas se basan en tener una conexión de base de datos presente, y que la base de datos de destino ya tiene instalado el último esquema actualizado.


Gracias @justin sé sobre el patrón del Repositorio, pero leer cosas como ayende.com/blog/4784/… y lostechies.com/jimmybogard/2009/09/11/wither-the-repository entre otros me han hecho pensar que no No quiero esta capa de abstracción, pero nuevamente hablan más sobre un enfoque de consulta que se vuelve muy confuso.
Modika

77
@Modika Ayende ha elegido una implementación deficiente del patrón de repositorio para criticar, y como resultado es 100% correcto: está sobredimensionado y no ofrece ningún beneficio. Una buena implementación aísla las partes comprobables de la unidad de su código de la implementación DAL. El uso de NHibernate y EF directamente hace que el código sea difícil (si no imposible) para la prueba unitaria y conduce a una base de código monolítico rígido. Todavía estoy un poco escéptico sobre el patrón del repositorio, sin embargo, estoy 100% convencido de que necesita aislar su implementación DAL de alguna manera y el repositorio es lo mejor que he encontrado hasta ahora.
Justin

2
@Modika Lea el segundo artículo nuevamente. "No quiero esta capa de abstracción" no es lo que dice. Además, lea sobre el patrón de repositorio original de Fowler ( martinfowler.com/eaaCatalog/repository.html ) o DDD ( dddcommunity.org/resources/ddd_terms ). No creas a los detractores sin comprender completamente el concepto original. Lo que realmente critican es un mal uso reciente del patrón, no el patrón en sí (aunque probablemente no lo sepan).
guillaume31

1
@ guillaume31 no estoy en contra del patrón de repositorio (lo entiendo) simplemente estoy tratando de averiguar si lo necesito para abstraer lo que ya es una abstracción a ese nivel, y si puedo omitirlo y probarlo directamente contra EF burlándome y usarlo en mis pruebas en una capa más arriba en mi aplicación. Además, si no uso un repositorio, obtengo el beneficio del conjunto de funciones ampliadas de EF, con un repositorio no puedo obtener eso.
Modika

Una vez que he aislado el DAL con un repositorio, necesito de alguna manera "Simular" la base de datos (EF). Hasta ahora, burlarme del contexto y de varias extensiones asíncronas (ToListAsync (), FirstOrDefaultAsync (), etc.) me ha resultado frustrante.
Kevin Burton

9

Así que aquí está el asunto, Entity Framework es una implementación, así que a pesar del hecho de que abstrae la complejidad de la interacción de la base de datos, la interacción directa sigue siendo un acoplamiento estrecho y es por eso que es confuso probarlo.

La prueba unitaria consiste en probar la lógica de una función y cada uno de sus resultados potenciales de forma aislada de cualquier dependencia externa, que en este caso es el almacén de datos. Para hacerlo, debe poder controlar el comportamiento del almacén de datos. Por ejemplo, si desea afirmar que su función devuelve falso si el usuario obtenido no cumple con algún conjunto de criterios, entonces su almacén de datos [simulado] debe estar configurado para devolver siempre un usuario que no cumple con los criterios, y viceversa viceversa para la afirmación opuesta.

Dicho esto, y aceptando el hecho de que EF es una implementación, probablemente favorecería la idea de abstraer un repositorio. Parece un poco redundante? No lo es, porque está resolviendo un problema que está aislando su código de la implementación de datos.

En DDD, los repositorios solo devuelven raíces agregadas, no DAO. De esa manera, el consumidor del repositorio nunca tiene que saber acerca de la implementación de datos (como no debería) y podemos usar eso como un ejemplo de cómo resolver este problema. En este caso, el objeto generado por EF es un DAO y, como tal, debe ocultarse de su aplicación. Este es otro beneficio del repositorio que usted define. Puede definir un objeto comercial como su tipo de retorno en lugar del objeto EF. Ahora lo que hace el repositorio es ocultar las llamadas a EF y asigna la respuesta de EF a ese objeto de negocio definido en la firma de repositorios. Ahora puede usar ese repositorio en lugar de la dependencia DbContext que inyecta en sus clases y, en consecuencia, ahora puede burlarse de esa interfaz para obtener el control que necesita para probar su código de forma aislada.

Es un poco más de trabajo y muchos lo critican, pero resuelve un problema real. Hay un proveedor en memoria que se mencionó en una respuesta diferente que podría ser una opción (no lo he probado), y su propia existencia es evidencia de la necesidad de la práctica.

Estoy completamente en desacuerdo con la respuesta principal porque evita el problema real que es aislar su código y luego toma una tangente sobre la prueba de su mapeo. De todos modos, pruebe su mapeo si lo desea, pero aborde el problema real aquí y obtenga una cobertura de código real.


8

No haría el código de prueba unitario que no tengo. ¿Qué estás probando aquí para que funcione el compilador MSFT?

Dicho esto, para que este código sea comprobable, casi TIENE que hacer que su capa de acceso a datos se separe de su código de lógica de negocios. Lo que hago es tomar todas mis cosas de EF y ponerlas en una (o múltiple) clase DAO o DAL que también tiene una interfaz correspondiente. Luego escribo mi servicio que tendrá el objeto DAO o DAL inyectado como una dependencia (inyección de constructor preferiblemente) referenciada como la interfaz. Ahora, la parte que necesita ser probada (su código) puede ser probada fácilmente burlándose de la interfaz DAO e inyectándola en su instancia de servicio dentro de la prueba de su unidad.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

Consideraría que las capas de acceso a datos en vivo son parte de las pruebas de integración, no de las pruebas unitarias. He visto a muchachos realizar verificaciones sobre cuántos viajes a la base de datos realiza la hibernación antes, pero estaban en un proyecto que involucraba miles de millones de registros en su almacén de datos y esos viajes adicionales realmente importaban.


1
Gracias por la respuesta, pero ¿cuál sería la diferencia de esto para decir un Repositorio donde se esconden los elementos internos de EF detrás de él en este nivel? Realmente no quiero abstraer EF, aunque todavía puedo estar haciendo eso con la interfaz IContext? Soy nuevo en esto, sé amable :)
Modika

3
@Modika A Repo también está bien. Cualquier patrón que quieras. "Realmente no quiero abstraer EF" ¿Quieres un código comprobable o no?
Jonathan Henson el

1
@Modika, mi punto es que no tendrás NINGÚN código comprobable si no separas tus preocupaciones. El acceso a datos y la lógica de negocios DEBEN estar en capas separadas para realizar buenas pruebas de mantenimiento.
Jonathan Henson

2
Simplemente no me pareció necesario envolver EF en una abstracción de repositorio, ya que esencialmente los IDbSets son repositorios y el contexto UOW, actualizaré mi pregunta un poco, ya que puede ser engañoso. El problema viene con cualquier abstracción y el punto principal es qué estoy probando exactamente porque mis consultas no se ejecutarán en los mismos límites (linq-a entidades vs linq-a-objetos), así que si solo estoy probando que mi servicio hace un llamada que parece un poco despilfarrador o estoy bien aquí?
Modika

1
, Si bien estoy de acuerdo con sus puntos generales, DbContext es una unidad de trabajo y los IDbSets son definitivamente algunos de implementación de repositorio, y no soy el único que piensa eso. Puedo burlarme de EF, y en alguna capa necesitaré ejecutar pruebas de integración, ¿eso realmente importa si lo hago en un Repositorio o más arriba en un Servicio? Estar estrechamente acoplado a un DB no es realmente una preocupación, estoy seguro de que sucede, pero no voy a planificar algo que puede no ocurrir.
Modika

8

En algún momento me puse a buscar estas consideraciones:

1- Si mi aplicación accede a la base de datos, ¿por qué la prueba no debería? ¿Qué pasa si hay algo mal con el acceso a datos? Las pruebas deben saberlo de antemano y alertarme sobre el problema.

2- El patrón de repositorio es algo duro y lento.

Así que se me ocurrió este enfoque, que no creo que sea el mejor, pero cumplí mis expectativas:

Use TransactionScope in the tests methods to avoid changes in the database.

Para hacerlo es necesario:

1- Instale el EntityFramework en el Proyecto de prueba. 2- Coloque la cadena de conexión en el archivo app.config de Test Project. 3- Consulte el sistema dll. Transacciones en el proyecto de prueba.

El efecto secundario único es que la semilla de identidad aumentará cuando intente insertar, incluso cuando la transacción se cancela. Pero dado que las pruebas se realizan contra una base de datos de desarrollo, esto no debería ser un problema.

Código de muestra:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
En realidad, me gusta mucho esta solución. Súper simple de implementar y escenarios de prueba más realistas. ¡Gracias!
slopapa

1
con EF 6, usarías DbContext.Database.BeginTransaction, ¿no?
SwissCoder

5

En resumen, diría que no, no vale la pena exprimir el jugo para probar un método de servicio con una sola línea que recupera datos del modelo. En mi experiencia, las personas que son nuevas en TDD quieren probar absolutamente todo. La vieja idea de abstraer una fachada a un marco de terceros solo para que pueda crear una simulación de esa API de marcos con la que bastardise / extender para que pueda inyectar datos ficticios es de poco valor en mi mente. Todos tienen una visión diferente de la cantidad de pruebas unitarias que es mejor. Tiendo a ser más pragmático en estos días y me pregunto si mi prueba realmente está agregando valor al producto final y a qué costo.


1
Sí al pragmatismo. Sigo argumentando que la calidad de sus pruebas unitarias es inferior a la calidad de su código original. Por supuesto, tiene valor usar TDD para mejorar su práctica de codificación y también para mejorar la capacidad de mantenimiento, pero TDD puede tener un valor decreciente. Ejecutamos todas nuestras pruebas contra la base de datos, porque nos da la confianza de que nuestro uso de EF y de las tablas en sí es sólido. Las pruebas tardan más en ejecutarse, pero son más confiables.
Salvaje

3

Quiero compartir un enfoque comentado y discutido brevemente, pero mostrar un ejemplo real que estoy usando actualmente para ayudar a la unidad a probar servicios basados ​​en EF.

Primero, me encantaría usar el proveedor en memoria de EF Core, pero se trata de EF 6. Además, para otros sistemas de almacenamiento como RavenDB, también sería un defensor de las pruebas a través del proveedor de base de datos en memoria. Una vez más, esto es específicamente para ayudar a probar el código basado en EF sin mucha ceremonia .

Estos son los objetivos que tenía al crear un patrón:

  • Debe ser simple para que otros desarrolladores del equipo entiendan
  • Debe aislar el código EF al nivel más bajo posible
  • No debe implicar la creación de interfaces extrañas de responsabilidad múltiple (como un patrón de repositorio "genérico" o "típico")
  • Debe ser fácil de configurar y configurar en una prueba unitaria

Estoy de acuerdo con las declaraciones anteriores de que EF todavía es un detalle de implementación y está bien sentir que necesita abstraerlo para hacer una prueba unitaria "pura". También estoy de acuerdo con que, idealmente, me gustaría asegurarme de que el código EF funcione, pero esto implica una base de datos de espacio aislado, un proveedor en memoria, etc. Mi enfoque resuelve ambos problemas: puede probar de forma segura el código dependiente de EF y crear pruebas de integración para probar su código EF específicamente.

La forma en que lo logré fue simplemente encapsulando el código EF en clases dedicadas de Consulta y Comando. La idea es simple: simplemente envuelva cualquier código EF en una clase y dependa de una interfaz en las clases que lo hubieran usado originalmente. El problema principal que necesitaba resolver era evitar agregar numerosas dependencias a las clases y configurar mucho código en mis pruebas.

Aquí es donde entra una biblioteca útil y simple: Mediatr . Permite mensajes simples en proceso y lo hace desacoplando las "solicitudes" de los controladores que implementan el código. Esto tiene un beneficio adicional de desacoplar el "qué" del "cómo". Por ejemplo, al encapsular el código EF en pequeños fragmentos, le permite reemplazar las implementaciones con otro proveedor o un mecanismo totalmente diferente, porque todo lo que está haciendo es enviar una solicitud para realizar una acción.

Utilizando la inyección de dependencia (con o sin un marco, su preferencia), podemos burlarnos fácilmente del mediador y controlar los mecanismos de solicitud / respuesta para permitir la prueba unitaria del código EF.

Primero, digamos que tenemos un servicio que tiene lógica de negocios que necesitamos probar:

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

¿Empiezas a ver el beneficio de este enfoque? No solo está encapsulando explícitamente todo el código relacionado con EF en clases descriptivas, sino que permite la extensibilidad al eliminar la preocupación de implementación de "cómo" se maneja esta solicitud: esta clase no le importa si los objetos relevantes provienen de EF, MongoDB, o un archivo de texto.

Ahora para la solicitud y el controlador, a través de MediatR:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

Como puede ver, la abstracción es simple y encapsulada. También es absolutamente comprobable porque en una prueba de integración, puede probar esta clase individualmente, no hay problemas comerciales mezclados aquí.

Entonces, ¿cómo es una prueba unitaria de nuestro servicio de funciones? Es muy simple. En este caso, estoy usando Moq para hacer burlas (usa lo que sea que te haga feliz):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

Puede ver que todo lo que necesitamos es una configuración única y ni siquiera necesitamos configurar nada adicional: es una prueba unitaria muy simple. Seamos claros: esto es totalmente posible sin algo como Mediatr (simplemente implementaría una interfaz y se burlaría de ella para las pruebas, por ejemplo IGetRelevantDbObjectsQuery), pero en la práctica para una gran base de código con muchas características y consultas / comandos, me encanta la encapsulación y soporte innato de DI que ofrece Mediatr.

Si te preguntas cómo organizo estas clases, es bastante simple:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

La organización por segmentos de características no viene al caso, pero esto mantiene todos los códigos relevantes / dependientes juntos y fácilmente detectables. Lo más importante es que separo las consultas frente a los comandos, siguiendo el principio de separación comando / consulta .

Esto cumple con todos mis criterios: es una ceremonia baja, es fácil de entender y hay beneficios adicionales ocultos. Por ejemplo, ¿cómo maneja guardar cambios? Ahora puede simplificar su contexto Db utilizando una interfaz de rol (IUnitOfWork.SaveChangesAsync()) y simulacros de llamadas a la interfaz de rol único o podría encapsular la confirmación / retroceso dentro de los RequestHandlers; sin embargo, si prefiere hacerlo, depende de usted, siempre que sea mantenible. Por ejemplo, tuve la tentación de crear una única solicitud / controlador genérico en el que simplemente pasaría un objeto EF y lo guardaría / actualizaría / eliminaría, pero debe preguntar cuál es su intención y recordarlo si desea intercambie el controlador con otro proveedor / implementación de almacenamiento, probablemente debería crear comandos / consultas explícitos que representen lo que pretende hacer. La mayoría de las veces, un solo servicio o función necesitará algo específico: no cree cosas genéricas antes de que lo necesite.

Por supuesto, hay advertencias para este patrón: puede ir demasiado lejos con un simple mecanismo de pub / sub. Limité mi implementación a solo abstraer el código relacionado con EF, pero los desarrolladores aventureros podrían comenzar a usar MediatR para exagerar y enviar mensajes a todo, algo que las buenas prácticas de revisión de código y las revisiones por pares deberían captar. Ese es un problema de proceso, no un problema con MediatR, así que solo tenga en cuenta cómo está utilizando este patrón.

Querías un ejemplo concreto de cómo las personas están probando / burlándose de EF y este es un enfoque que funciona con éxito para nosotros en nuestro proyecto, y el equipo está muy contento con lo fácil que es adoptarlo. ¡Espero que esto ayude! Al igual que con todas las cosas en la programación, existen múltiples enfoques y todo depende de lo que desee lograr. Valoro la simplicidad, la facilidad de uso, la facilidad de mantenimiento y la capacidad de descubrimiento, y esta solución cumple con todas esas demandas.


Gracias por la respuesta, es una excelente descripción del Patrón de QueryObject usando un Mediador, y algo que también estoy empezando a impulsar en mis proyectos. Es posible que tenga que actualizar la pregunta, pero ya no estoy probando la unidad EF, las abstracciones son demasiado permeables (aunque SqlLite podría estar bien), así que solo pruebo la integración de mis cosas que consultan la base de datos y las reglas comerciales de prueba unitaria y otra lógica.
Modika

3

Existe un esfuerzo que es un proveedor de base de datos de marco de entidad de memoria. En realidad no lo he intentado ... ¡Haa acaba de ver que esto fue mencionado en la pregunta!

Alternativamente, puede cambiar a EntityFrameworkCore que tiene un proveedor de base de datos en memoria incorporado.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

Utilicé una fábrica para obtener un contexto, por lo que puedo crear el contexto cerca de su uso. Esto parece funcionar localmente en Visual Studio, pero no en mi servidor de compilación TeamCity, todavía no estoy seguro de por qué.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

Hola Andrew, el problema nunca fue obtener el contexto, puedes crear el contexto que es lo que estábamos haciendo, abstrayendo el contexto y construyéndolo en la fábrica. El mayor problema fue la consistencia de lo que estaba en la memoria frente a lo que hace Linq4Entities, no son lo mismo, lo que puede conducir a pruebas engañosas. Actualmente, solo hacemos pruebas de integración de la base de datos, puede que no sea el mejor proceso para todos.
Modika

Este asistente de Moq funciona ( codeproject.com/Tips/1045590/… ) si tiene un contexto para burlarse. Si respalda el contexto simulado con una lista, no se comportará como un contexto respaldado por una base de datos SQL.
andrew pate

2

Me gusta separar mis filtros de otras partes del código y probarlos como describo en mi blog aquí http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Dicho esto, la lógica de filtro que se está probando no es idéntica a la lógica de filtro ejecutada cuando el programa se ejecuta debido a la traducción entre la expresión LINQ y el lenguaje de consulta subyacente, como T-SQL. Aún así, esto me permite validar la lógica del filtro. No me preocupo demasiado por las traducciones que suceden y cosas como la distinción entre mayúsculas y minúsculas hasta que pruebo la integración entre las capas.


0

Es importante probar lo que espera que haga el marco de la entidad (es decir, validar sus expectativas). Una forma de hacer esto que he usado con éxito, es usar moq como se muestra en este ejemplo (anhelar copiar en esta respuesta):

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

Sin embargo, tenga cuidado ... No se garantiza que un contexto SQL devuelva cosas en un orden específico a menos que tenga un "OrderBy" apropiado en su consulta linq, por lo que es posible escribir cosas que pasan cuando prueba usando una lista en memoria ( linq-to-persons) pero falla en su entorno uat / live cuando se utiliza (linq-to-sql).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.