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:
- 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?
- 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.
- 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?
Respuestas:
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:
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:
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:
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.
Luego pruebe cada propiedad individualmente
Hay varias razones para este enfoque:
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
SetupPerTest
ySingleSetup
. Estas dos clases exponen el marco según sea necesario.En el
SingleSetup
tenemos un mecanismo muy similar al descrito en mi primera edición. Un ejemplo seríaSin embargo, las referencias que aseguran que solo se cargan las entidades correctas pueden usar un enfoque SetupPerTest
En resumen, ambos enfoques funcionan según lo que intente probar.
fuente
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 un
IHandlerOf<M>
través de Windsor. Castle.Windsor resuelve loIHandlerOf<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
fuente
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.
fuente
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.
fuente
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.
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.
fuente
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:
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:
fuente
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.
fuente
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:
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:
¿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:
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):
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:
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.
fuente
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é.
fuente
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.
fuente
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).
fuente