Cómo probar un objeto con consultas de bases de datos

153

He oído que las pruebas unitarias son "totalmente increíbles", "realmente geniales" y "todo tipo de cosas buenas", pero el 70% o más de mis archivos implican acceso a la base de datos (algunos leen y otros escriben) y no estoy seguro de cómo para escribir una prueba unitaria para estos archivos.

Estoy usando PHP y Python, pero creo que es una pregunta que se aplica a la mayoría / todos los lenguajes que utilizan el acceso a la base de datos.

Teifion
fuente

Respuestas:

82

Sugeriría burlarse de sus llamadas a la base de datos. Los simulacros son básicamente objetos que se parecen al objeto al que está intentando llamar un método, en el sentido de que tienen las mismas propiedades, métodos, etc. disponibles para la persona que llama. Pero en lugar de realizar cualquier acción para la que estén programados cuando se llama a un método en particular, lo omite por completo y solo devuelve un resultado. Ese resultado generalmente lo define usted con anticipación.

Para configurar sus objetos para burlarse, probablemente necesite usar algún tipo de inversión del patrón de inyección de control / dependencia, como en el siguiente pseudocódigo:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Ahora en su prueba de unidad, crea una simulación de FooDataProvider, que le permite llamar al método GetAllFoos sin tener que acceder a la base de datos.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Un escenario de burla común, en pocas palabras. Por supuesto, es probable que también desee probar la unidad de sus llamadas de base de datos reales, para lo cual deberá acceder a la base de datos.

Doug R
fuente
Sé que esto es antiguo, pero ¿qué hay de crear una tabla duplicada para la que ya está en la base de datos? ¿De esa manera puede confirmar que las llamadas DB funcionan?
cervecero el
1
He estado usando el PDO de PHP como mi acceso de nivel más bajo a la base de datos, sobre el cual extraje una interfaz. Luego construí una capa de base de datos consciente de la aplicación además de eso. Esta es la capa que contiene todas las consultas SQL sin procesar y otra información. El resto de la aplicación interactúa con esta base de datos de nivel superior. He encontrado que esto funciona bastante bien para las pruebas unitarias; Pruebo las páginas de mi aplicación para ver cómo interactúan con la base de datos de la aplicación. Pruebo la base de datos de mi aplicación para ver cómo interactúa con PDO. Supongo que PDO funciona sin errores. Código fuente: manx.codeplex.com
legalizar
1
@bretterer: crear una tabla duplicada es bueno para las pruebas de integración. Para las pruebas unitarias, generalmente usaría un objeto simulado que le permitirá probar una unidad de código independientemente de la base de datos.
BornToCode
2
¿Cuál es el valor de burlarse de las llamadas a la base de datos en las pruebas unitarias? No parece útil porque podría cambiar la implementación para devolver un resultado diferente, pero su prueba de unidad pasaría (incorrectamente).
bmay2
2
@ bmay2 No estás equivocado. Mi respuesta original fue escrita hace mucho tiempo (¡9 años!) Cuando mucha gente no escribía su código de manera comprobable, y cuando las herramientas de prueba carecían severamente. No recomendaría este enfoque nunca más. Hoy simplemente configuraría una base de datos de prueba y la llenaría con los datos que necesitaba para la prueba, y / o diseñaría mi código para poder probar tanta lógica como sea posible sin una base de datos.
Doug R
25

Idealmente, sus objetos deben ser persistentes ignorantes. Por ejemplo, debe tener una "capa de acceso a datos", a la que haría solicitudes, que devolvería objetos. De esta manera, puede dejar esa parte fuera de las pruebas de su unidad, o probarlas de forma aislada.

Si sus objetos están estrechamente acoplados a su capa de datos, es difícil hacer una prueba de unidad adecuada. la primera parte de la prueba unitaria, es "unidad". Todas las unidades deben poder probarse de forma aislada.

En mis proyectos de C #, uso NHibernate con una capa de datos completamente separada. Mis objetos viven en el modelo de dominio central y se accede desde mi capa de aplicación. La capa de aplicación habla tanto con la capa de datos como con la capa del modelo de dominio.

La capa de aplicación también se denomina a veces la "capa empresarial".

Si está utilizando PHP, cree un conjunto específico de clases para SOLO acceso a datos. Asegúrese de que sus objetos no tengan idea de cómo son persistentes y conecte los dos en sus clases de aplicación.

Otra opción sería usar burlas / talones.

Sean Chambers
fuente
Siempre he estado de acuerdo con esto, pero en la práctica debido a los plazos y "está bien, ahora agregue solo una función más, a las 2 p.m. de hoy", esta es una de las cosas más difíciles de lograr. Sin embargo, este tipo de cosas es el objetivo principal de la refactorización, si mi jefe alguna vez decide que no ha pensado en 50 nuevos problemas emergentes que requieren una lógica y tablas comerciales completamente nuevas.
Darren Ringer
3
Si sus objetos están estrechamente acoplados a su capa de datos, es difícil hacer una prueba de unidad adecuada. la primera parte de la prueba unitaria, es "unidad". Todas las unidades deben poder probarse de forma aislada. buena explicación
Amitābha
11

La forma más fácil de probar un objeto con acceso a la base de datos es mediante ámbitos de transacción.

Por ejemplo:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Esto revertirá el estado de la base de datos, básicamente como una reversión de transacción para que pueda ejecutar la prueba tantas veces como lo desee sin ningún efecto secundario. Hemos utilizado este enfoque con éxito en grandes proyectos. Nuestra construcción tarda un poco en ejecutarse (15 minutos), pero no es horrible tener 1800 pruebas unitarias. Además, si el tiempo de compilación es una preocupación, puede cambiar el proceso de compilación para que tenga varias compilaciones, una para construir src, otra que se activa luego que maneja pruebas unitarias, análisis de código, empaquetado, etc.

BZ
fuente
1
+1 Ahorra mucho tiempo cuando la unidad prueba las capas de acceso a datos. Solo tenga en cuenta que TS a menudo necesitará MSDTC, lo que puede no ser deseable (dependiendo de si su aplicación necesitará MSDTC)
StuartLC
La pregunta original era sobre PHP, este ejemplo parece ser C #. Los ambientes son muy diferentes.
legalizar el
2
El autor de la pregunta declaró que es una pregunta general que se aplica a todos los idiomas que tienen algo que ver con una base de datos.
Vedran
9
y esto queridos amigos, se llama pruebas de integración
AA.
10

Tal vez pueda darle una idea de nuestra experiencia cuando comenzamos a analizar las pruebas unitarias de nuestro proceso de nivel medio que incluía un montón de operaciones sql de "lógica de negocios".

Primero creamos una capa de abstracción que nos permitía "insertar" cualquier conexión de base de datos razonable (en nuestro caso, simplemente admitíamos una única conexión de tipo ODBC).

Una vez que esto estuvo en su lugar, pudimos hacer algo así en nuestro código (trabajamos en C ++, pero estoy seguro de que entiende la idea):

GetDatabase (). ExecuteSQL ("INSERTAR EN foo (bla, bla)")

En tiempo de ejecución normal, GetDatabase () devolvería un objeto que alimentaba todos nuestros sql (incluidas las consultas), a través de ODBC directamente a la base de datos.

Luego comenzamos a buscar en las bases de datos en memoria: el mejor, en gran medida, parece ser SQLite. ( http://www.sqlite.org/index.html ). Es notablemente simple de configurar y usar, y nos permitió subclasificar y anular GetDatabase () para reenviar sql a una base de datos en memoria que se creó y destruyó para cada prueba realizada.

Todavía estamos en las primeras etapas de esto, pero hasta ahora se ve bien, sin embargo, debemos asegurarnos de crear las tablas que sean necesarias y llenarlas con datos de prueba; sin embargo, hemos reducido la carga de trabajo aquí al crear Un conjunto genérico de funciones auxiliares que puede hacer mucho de todo esto por nosotros.

En general, ha ayudado enormemente con nuestro proceso TDD, ya que realizar cambios que parecen bastante inocuos para corregir ciertos errores puede tener efectos bastante extraños en otras áreas (difíciles de detectar) de su sistema, debido a la naturaleza misma de SQL / bases de datos.

Obviamente, nuestras experiencias se han centrado en un entorno de desarrollo de C ++, sin embargo, estoy seguro de que quizás podría obtener algo similar trabajando en PHP / Python.

Espero que esto ayude.

Alan
fuente
9

Debe burlarse del acceso a la base de datos si desea probar sus clases de forma unitaria. Después de todo, no desea probar la base de datos en una prueba unitaria. Esa sería una prueba de integración.

Resuma las llamadas y luego inserte una simulación que solo devuelva los datos esperados. Si sus clases no hacen más que ejecutar consultas, quizás no valga la pena probarlas, sin embargo ...

Martin Klinke
fuente
6

El libro xUnit Test Patterns describe algunas formas de manejar el código de prueba de unidad que llega a una base de datos. Estoy de acuerdo con las otras personas que dicen que no quieres hacer esto porque es lento, pero debes hacerlo en algún momento, en mi opinión. Es una buena idea burlarse de la conexión db para probar cosas de nivel superior, pero consulte este libro para obtener sugerencias sobre cosas que puede hacer para interactuar con la base de datos real.

Chris Farmer
fuente
4

Opciones que tienes:

  • Escriba un script que borre la base de datos antes de comenzar las pruebas unitarias, luego complete db con un conjunto predefinido de datos y ejecute las pruebas. También puede hacerlo antes de cada prueba: será lento, pero menos propenso a errores.
  • Inyectar la base de datos. (Ejemplo en pseudo-Java, pero se aplica a todos los lenguajes OO)

    Base de datos de clase {
     Consulta de resultado público (consulta de cadena) {... db real aquí ...}
    }

    clase MockDatabase extiende la base de datos { Consulta de resultado público (consulta de cadena) { devolver "resultado simulado"; } }

    clase ObjectThatUsesDB { public ObjectThatUsesDB (base de datos db) { this.database = db; } }

    ahora en producción usa una base de datos normal y para todas las pruebas simplemente inyecta la base de datos simulada que puede crear ad hoc.

  • No use DB en absoluto en la mayor parte del código (de todos modos, es una mala práctica). Cree un objeto de "base de datos" que, en lugar de devolver resultados, devolverá objetos normales (es decir, devolverá en Userlugar de una tupla {name: "marcin", password: "blah"}) escriba todas sus pruebas con objetos reales construidos ad hoc y escriba una prueba grande que dependa de una base de datos que asegure esta conversión funciona bien

Por supuesto, estos enfoques no son mutuamente excluyentes y puede mezclarlos y combinarlos según lo necesite.

Marcin
fuente
3

La unidad que prueba el acceso a su base de datos es bastante fácil si su proyecto tiene una alta cohesión y un acoplamiento flojo. De esta manera, puede probar solo las cosas que hace cada clase en particular sin tener que probar todo a la vez.

Por ejemplo, si prueba su clase de interfaz de usuario, las pruebas que escriba solo deberían intentar verificar que la lógica dentro de la interfaz de usuario funcionó como se esperaba, no la lógica de negocios o la acción de la base de datos detrás de esa función.

Si desea realizar una prueba unitaria del acceso real a la base de datos, en realidad terminará con más de una prueba de integración, porque dependerá de la pila de red y su servidor de base de datos, pero puede verificar que su código SQL haga lo que le pidió. hacer.

El poder oculto de las pruebas unitarias para mí personalmente ha sido que me obliga a diseñar mis aplicaciones de una manera mucho mejor de lo que podría sin ellas. Esto se debe a que realmente me ayudó a romper con la mentalidad de "esta función debería hacer todo".

Lo siento, no tengo ningún ejemplo de código específico para PHP / Python, pero si desea ver un ejemplo de .NET, tengo una publicación que describe una técnica que solía hacer esta misma prueba.

Toran Billups
fuente
2

Por lo general, trato de dividir mis pruebas entre probar los objetos (y ORM, si corresponde) y probar la base de datos. Pruebo el lado del objeto de las cosas burlándome de las llamadas de acceso a datos, mientras que pruebo el lado db de las cosas probando las interacciones del objeto con el db que, en mi experiencia, generalmente es bastante limitado.

Solía ​​frustrarme con la escritura de pruebas unitarias hasta que empiezo a burlarme de la parte de acceso a datos para no tener que crear una base de datos de prueba o generar datos de prueba sobre la marcha. Al burlarse de los datos, puede generarlos todos en tiempo de ejecución y asegurarse de que sus objetos funcionan correctamente con entradas conocidas.

akmad
fuente
2

Nunca he hecho esto en PHP y nunca he usado Python, pero lo que quieres hacer es burlarte de las llamadas a la base de datos. Para hacer eso, puede implementar alguna IoC, ya sea una herramienta de terceros o administrarla usted mismo, luego puede implementar alguna versión simulada de la persona que llama la base de datos, que es donde controlará el resultado de esa llamada falsa.

Se puede realizar una forma simple de IoC simplemente codificando en Interfaces. Esto requiere algún tipo de orientación de objeto en su código, por lo que puede que no se aplique a lo que está haciendo (lo digo porque todo lo que tengo que seguir es su mención de PHP y Python)

Espero que sea útil, si nada más, tiene algunos términos para buscar ahora.

codeLes
fuente
2

Estoy de acuerdo con el primer post: el acceso a la base de datos debe eliminarse en una capa DAO que implemente una interfaz. Luego, puede probar su lógica contra una implementación de código auxiliar de la capa DAO.

Chris Marasti-Georg
fuente
2

Podrías usar marcos burlones para abstraer el motor de la base de datos. No sé si PHP / Python obtuvo algo, pero para los lenguajes mecanografiados (C #, Java, etc.) hay muchas opciones

También depende de cómo diseñó esos códigos de acceso a la base de datos, porque algunos diseños son más fáciles de probar que otros, como se mencionó en las publicaciones anteriores.

chakrit
fuente
2

Configurar datos de prueba para pruebas unitarias puede ser un desafío.

Cuando se trata de Java, si usa Spring API para pruebas unitarias, puede controlar las transacciones a nivel de unidad. En otras palabras, puede ejecutar pruebas unitarias que implican actualizaciones / inserciones / eliminaciones de bases de datos y deshacer los cambios. Al final de la ejecución, deja todo en la base de datos como estaba antes de comenzar la ejecución. Para mí, es tan bueno como puede ser.

Bino Manjasseril
fuente