¿No deberían las pruebas unitarias usar mis propios métodos?


83

Hoy estaba viendo un video "Conceptos básicos de JUnit " y el autor dijo que al probar un método determinado en su programa, no debe usar otros métodos propios en el proceso.

Para ser más específico, estaba hablando de probar algún método de creación de registros que tomara un nombre y un apellido para los argumentos, y los usó para crear registros en una tabla dada. Pero afirmó que en el proceso de probar este método, no debería usar sus otros métodos DAO para consultar la base de datos para verificar el resultado final (para verificar que el registro se haya creado con los datos correctos). Afirmó que para eso, debería escribir código JDBC adicional para consultar la base de datos y verificar el resultado.

Creo que entiendo el espíritu de su afirmación: no desea que el caso de prueba de un método dependa de la corrección de los otros métodos (en este caso, el método DAO), y esto se logra escribiendo (nuevamente) su propia validación / código de soporte (que debería ser más específico y centrado, por lo tanto, un código más simple).

No obstante, las voces dentro de mi cabeza comenzaron a protestar con argumentos como la duplicación de código, esfuerzos adicionales innecesarios, etc. Quiero decir, si ejecutamos toda la batería de prueba y probamos todos nuestros métodos públicos a fondo (incluido el método DAO en este caso), debería ¿Está bien usar algunos de esos métodos mientras se prueban otros métodos? Si uno de ellos no está haciendo lo que se supone que debe hacer, entonces su propio caso de prueba fallará, y podemos arreglarlo y volver a ejecutar la batería de prueba. No es necesario duplicar el código (incluso si el código duplicado es algo más simple) ni desperdiciar esfuerzos.

Tengo un fuerte presentimiento sobre esto debido a varias aplicaciones recientes de Excel - VBA que he escrito (debidamente probadas por unidad gracias a Rubberduck para VBA ), donde aplicar esta recomendación significaría mucho trabajo adicional adicional, sin beneficio percibido.

¿Puedes por favor compartir tus ideas sobre esto?


79
Es un poco extraño ver una prueba unitaria que involucra la base de datos en absoluto
Richard Tingle el

44
En mi opinión, está bien llamar a otras clases IFF que son lo suficientemente rápidas. Simula cualquier cosa que tenga que ir al disco o a través de una red. No tiene sentido burlarse de una simple vieja OMI de clase.
RubberDuck

2
¿Tienes un enlace a este video?
candied_orange

17
" debidamente probado en la unidad gracias a Rubberduck para VBA ", señor, acabo de alegrarme el día. Solucionaría el error tipográfico y lo editaría de "RubberDuck" a "Rubberduck", pero me sentiría como un spammer haciendo eso y agregando un enlace a rubberduckvba.com (soy dueño del nombre de dominio y soy copropietario del proyecto con @RubberDuck), así que solo comentaré aquí en su lugar. De todos modos, ¡es increíble ver a personas que realmente utilizan la herramienta que ha sido responsable de la mayoría de mis noches de insomnio durante la mayor parte de los últimos dos años! =)
Mathieu Guindon

44
@ Mat'sMug y RubberDuck Me encanta lo que estás haciendo con Rubberduck. Por favor sigue así. Ciertamente, mi vida es más fácil por eso (sucede que hago muchos pequeños programas y prototipos en Excel VBA). Por cierto, la mención de Rubberduck en mi publicación fue solo yo tratando de ser amable con el mismo RubberDuck, lo que a su vez ha sido amable conmigo aquí en PE. :)
carlossierra

Respuestas:


186

El espíritu de su reclamo es de hecho correcto. El objetivo de las pruebas unitarias es aislar el código, probarlo sin dependencias, para que cualquier comportamiento erróneo pueda reconocerse rápidamente donde está sucediendo.

Dicho esto, las pruebas unitarias son una herramienta, y están destinadas a servir a sus propósitos, no es un altar para rezar. A veces eso significa dejar dependencias porque funcionan de manera suficientemente confiable y no quieres molestarte en burlarte de ellas, a veces eso significa que algunas de las pruebas de tu unidad son bastante cercanas, si no pruebas de integración.

En última instancia, no se lo calificará, lo importante es el producto final del software que se está probando, pero solo tendrá que tener en cuenta cuándo está doblando las reglas y decidiendo cuándo valen la pena las compensaciones.


117
¡Hurra por el pragmatismo!
Robert Harvey

66
Agregaría que no es tanto la fiabilidad como la velocidad, que es el problema que lleva a eliminar las dependencias. Sin embargo, la prueba del mantra de aislamiento es tan convincente que a menudo se pasa por alto este argumento.
Michael Durrant

44
"... la prueba de la unidad es una herramienta, y está destinada a servir a tus propósitos, no es un altar para rezar". ¡Esto!
Wayne Conrad

15
"no es un altar para rezar" - ¡Entrenadores enojados de TDD entrantes!
Den

55
@ jpmc26: peor aún, no desea que se pasen todas las pruebas unitarias porque manejaron correctamente el servicio web inactivo, lo cual es un comportamiento válido y correcto, ¡pero nunca ejerce el código cuando está activo! Una vez que lo aprietas, puedes elegir si está "arriba" o "abajo" y probar ambos.
Steve Jessop

36

Creo que esto se reduce a la terminología. Para muchos, una "prueba unitaria" es algo muy específico y, por definición , no se puede tener una condición de aprobación / falla que dependa de cualquier código fuera de la unidad (método, función, etc.) que se esté probando. Esto incluiría la interacción con una base de datos.

Para otros, el término "prueba unitaria" es mucho más flexible y abarca cualquier tipo de prueba automatizada, incluido el código de prueba que prueba porciones integradas de la aplicación.

Un purista de prueba (si puedo usar ese término) podría llamar a eso una prueba de integración , distinguida de una prueba unitaria sobre la base de que la prueba depende de más de una unidad pura de código.

Sospecho que está utilizando la versión más flexible del término "prueba unitaria" y en realidad se refiere a una "prueba de integración".

Usando esas definiciones, no debe depender del código fuera de la unidad bajo prueba en una "prueba de unidad". Sin embargo, en una "prueba de integración", es perfectamente razonable escribir una prueba que ejercite una pieza específica de código y luego inspeccione la base de datos para los criterios de aprobación.

Si debe depender de pruebas de integración o pruebas unitarias o ambas es un tema de discusión mucho más amplio.


Tienes razón en tus sospechas. Estoy haciendo un mal uso de los términos y ahora puedo ver cómo cada tipo de prueba se puede manejar de manera más adecuada. ¡Gracias!
carlossierra

11
"Para otros, el término" prueba de unidad "es mucho más flexible", aparte de cualquier otra cosa, los llamados "marcos de prueba de unidad" son una forma realmente conveniente de organizar y ejecutar pruebas de nivel superior ;-)
Steve Jessop,

1
En lugar de una distinción "pura" versus "floja", que está cargada de connotaciones, sugeriría que la distinción es entre aquellos que ven "unidades", "dependencias", etc. desde una perspectiva de dominio (por ejemplo, "autenticación" contaría como una unidad, la "base de datos" contaría como una dependencia, etc.) frente a quienes las ven desde una perspectiva de lenguaje de programación (por ejemplo, los métodos son unidades, las clases son dependencias). Me parece que definir lo que se entiende por frases como "la unidad bajo prueba" puede convertir los argumentos ("¡Estás equivocado!") En discusiones ("¿Es este el nivel correcto de granularidad?").
Warbo

1
@EricKing Por supuesto, para la gran mayoría de aquellos en su primera categoría, la idea misma de involucrar a la base de datos en todas las pruebas unitarias es anatema, ya sea que use sus DAO o acceda directamente a la base de datos.
Periata Breatta

1
@AmaniKilumanga La pregunta era básicamente "alguien dice que no debería hacer esto en pruebas unitarias, pero lo hago en pruebas unitarias y creo que está bien. ¿Está bien?" Y mi respuesta fue "Sí, pero la mayoría de la gente llamaría a eso una prueba de integración en lugar de una prueba unitaria". En cuanto a su pregunta, no sé qué quiere decir con "¿pueden las pruebas de integración utilizar métodos de código de producción?".
Eric King

7

La respuesta es sí y no...

Las pruebas únicas que se realizan de forma aislada son imprescindibles, ya que le permiten establecer las bases para que su código de bajo nivel funcione correctamente. Pero al codificar bibliotecas más grandes, también encontrará áreas de código que requerirán pruebas que crucen unidades.

Estas pruebas de unidades cruzadas son buenas para la cobertura del código y cuando se prueban las funciones de extremo a extremo, pero vienen con algunos inconvenientes que debe tener en cuenta:

  • Sin una prueba aislada para confirmar qué se está rompiendo, una prueba fallida de "unidad cruzada" requerirá una solución de problemas adicional para determinar qué está mal con su código
  • Confiar demasiado en las pruebas de unidades cruzadas puede sacarlo de la mentalidad contractual en la que siempre debe estar al escribir código SOLID orientado a objetos. Las pruebas aisladas suelen tener sentido porque sus unidades solo deben realizar una sola acción básica.
  • Las pruebas de extremo a extremo son deseables, pero pueden ser peligrosas si una prueba requiere que escriba en una base de datos o realice alguna acción que no desea que ocurra en un entorno de producción. Esta es una de las muchas razones por las que los frameworks burlones como Mockito son tan populares porque le permite falsificar un objeto e imitar una prueba de extremo a extremo sin cambiar realmente algo que no debería.

Al final del día, desea tener algunas de ambas ... muchas pruebas que atrapan la funcionalidad de bajo nivel y algunas que prueban la funcionalidad de extremo a extremo. En primer lugar, enfóquese en la razón por la que está escribiendo exámenes. Están ahí para darle confianza de que su código está funcionando de la manera que espera. Lo que sea que necesite hacer para que eso suceda está bien.

El hecho triste pero cierto es que si está utilizando pruebas automatizadas, ya tiene una ventaja sobre muchos desarrolladores. Las buenas prácticas de prueba son lo primero que se pierde cuando un equipo de desarrollo se enfrenta a plazos difíciles. Entonces, siempre y cuando se apegue a sus armas y escriba pruebas unitarias que sean un reflejo significativo de cómo debería funcionar su código, no me obsesionaría con cuán "puras" son sus pruebas.


4

Un problema con el uso de otros métodos de objeto para probar una condición es que se perderán los errores que se cancelan entre sí. Más importante aún, se está perdiendo el dolor de una implementación de prueba difícil, y ese dolor le está enseñando algo sobre su código subyacente. Las pruebas dolorosas ahora significan un mantenimiento doloroso más adelante.

Reduzca el tamaño de sus unidades, divida su clase, refactorice y rediseñe hasta que sea más fácil probar sin volver a implementar el resto de su objeto. Obtenga ayuda si cree que su código o sus pruebas no pueden simplificarse más. Trate de pensar en un colega que siempre parece tener suerte y obtenga tareas que sean fáciles de evaluar de manera limpia. Pídale ayuda, porque no es suerte.


1
La clave aquí está en la unidad. Si necesita hacer llamadas a otros procedimientos y procesos, no sería, para mí, una unidad. Si se realizan varias llamadas y se deben validar varios pasos, entonces, si es una pieza de código grande que una unidad, divídala en piezas más pequeñas que cubran una sola pieza de lógica. Normalmente, una llamada a la base de datos también se burlaría o una base de datos en memoria se usaría en la prueba de la unidad para acceder a una base de datos real y necesitar deshacer datos después de la prueba.
dlb

1

Si la unidad de funcionalidad que se está probando es 'son los datos almacenados de forma persistente y recuperable', entonces tendría la prueba de prueba unitaria que: almacenar en una base de datos real, destruir cualquier objeto que tenga referencias a la base de datos, luego llamar al código para obtener el objetos de vuelta.

La prueba de si se crea un registro en la base de datos parece estar relacionada con los detalles de implementación del mecanismo de almacenamiento en lugar de probar la unidad de funcionalidad expuesta al resto del sistema.

Es posible que desee atajar la prueba para mejorar el rendimiento utilizando una base de datos simulada, pero he tenido contratistas que hicieron exactamente eso y se fueron al final de su contrato con un sistema que pasó tales pruebas, pero en realidad no almacenó nada en la base de datos entre reinicios del sistema.

Puede argumentar si 'unidad' en la prueba de unidad denota una 'unidad de código' o 'unidad de funcionalidad', funcionalidad tal vez creada por muchas unidades de código en concierto. No creo que sea una distinción útil: las preguntas que tendré en cuenta son: "¿la prueba le dice algo sobre si el sistema proporciona valor comercial" y "es la prueba frágil si la implementación cambia?" Las pruebas como la descrita son útiles cuando está TDD en el sistema: aún no ha escrito el 'obtener objeto del registro de la base de datos', por lo que no puede probar la unidad completa de funcionalidad, pero son frágiles contra los cambios de implementación, por lo que quítelos una vez que la operación completa sea comprobable.


1

El espíritu es correcto.

Idealmente, en una prueba unitaria , está probando una unidad (un método individual o una clase pequeña).

Idealmente, debería desconectar todo el sistema de base de datos. Es decir, ejecutaría su método en un entorno falso y solo se aseguraría de que llame a las API de DB correctas en el orden correcto. Explícitamente, positivamente, no desea probar la base de datos cuando pruebe uno de sus propios métodos.

Los beneficios son muchos. Sobre todo, las pruebas se vuelven cegadoras rápidamente porque no necesita preocuparse por configurar un entorno de base de datos correcto y revertirlo después.

Por supuesto, este es un objetivo elevado en cualquier proyecto de software que no lo haga desde el principio. Pero es el espíritu de las pruebas unitarias .

Tenga en cuenta que hay otras pruebas, como pruebas de características, pruebas de comportamiento, etc., que son diferentes de este enfoque; no confunda "pruebas" con "pruebas unitarias".


"solo asegúrate de que llame a las API de DB correctas en el orden correcto" - sí, claro. Hasta que resulta que has leído mal la documentación y el orden correcto real que deberías haber estado usando es completamente diferente. No te burles de los sistemas fuera de tu control.
Joker_vD

44
@Joker_vD Para eso son las pruebas de integración. En las pruebas unitarias, los sistemas externos se deben burlar absolutamente.
Ben Aaronson

1

Hay al menos una razón muy importante para no utilizar el acceso directo a la base de datos en sus pruebas unitarias para el código comercial que interactúa con la base de datos: si cambia la implementación de su base de datos, tendrá que reescribir todas esas pruebas unitarias. Cambie el nombre de una columna, para su código solo tiene que cambiar una sola línea en la definición del mapeador de datos. Sin embargo, si no usa su mapeador de datos durante las pruebas, encontrará que también tiene que cambiar cada prueba de unidad que hace referencia a esta columna . Esto podría convertirse en una cantidad extraordinaria de trabajo, particularmente para cambios más complejos que son menos susceptibles de búsqueda y reemplazo.

Además, el uso de su mapeador de datos como un mecanismo abstracto en lugar de hablar directamente con la base de datos hace que sea más fácil eliminar por completo la dependencia de la base de datos , lo que podría no ser relevante ahora, pero cuando obtiene hasta miles de pruebas unitarias y todas están llegando a una base de datos, agradecerá que pueda refactorizarlos fácilmente para eliminar esa dependencia porque su conjunto de pruebas dejará de tomar minutos para ejecutarse y tomar segundos, lo que puede tener un gran beneficio en su productividad.

Su pregunta indica que está pensando en la línea correcta. Como sospecha, las pruebas unitarias también son código. Es tan importante hacerlos fáciles de mantener y adaptar a futuros cambios como para el resto de su código base. Esfuércese por mantenerlos legibles y eliminar la duplicación, y tendrá un conjunto de pruebas mucho mejor para ello. Y un buen conjunto de pruebas es uno que se acostumbra. Y un conjunto de pruebas que se usa ayuda a encontrar errores, mientras que uno que no se usa simplemente no tiene valor.


1

La mejor lección que aprendí cuando aprendí sobre pruebas unitarias y pruebas de integración es no probar métodos, sino probar comportamientos. En otras palabras, ¿qué hace este objeto?

Cuando lo veo de esa manera, un método que persiste en los datos y otro método que los lee de nuevo comienzan a ser comprobables. Si está probando los métodos específicamente, por supuesto, terminará con una prueba para cada método de forma aislada, como por ejemplo:

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

Este problema ocurre debido a la perspectiva de los métodos de prueba. No pruebes los métodos. Probar comportamientos. ¿Cuál es el comportamiento de la clase WidgetDao? Persiste los widgets. Ok, ¿cómo verificas que persiste los widgets? Bueno, ¿cuál es la definición de persistencia? Significa que cuando lo escribes, puedes volver a leerlo. Entonces leer + escribir juntos se convierte en una prueba, y en mi opinión, una prueba más significativa.

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

Esa es una prueba lógica, coherente, confiable y, en mi opinión, significativa.

Las otras respuestas se centran en la importancia del aislamiento, el debate pragmático versus el realista, y si una prueba unitaria puede o no consultar una base de datos. Sin embargo, esos no responden realmente la pregunta.

Para probar si algo se puede almacenar, debe almacenarlo y luego volver a leerlo. No puede probar si algo está almacenado si no tiene permiso para leerlo. No pruebe el almacenamiento de datos por separado de la recuperación de datos. Terminará con pruebas que no le dicen nada.


Enfoque muy perspicaz, y como usted dice, creo que realmente dio con una de las principales dudas que tenía. ¡Gracias!
carlossierra

0

Un ejemplo de un caso de falla que preferiría detectar, es que el objeto bajo prueba usa una capa de almacenamiento en caché pero no persiste los datos según sea necesario. Luego, si consulta el objeto, dirá "sí, tengo el nuevo nombre y dirección", pero desea que la prueba falle porque en realidad no ha hecho lo que se suponía que debía hacer.

Alternativamente (y pasando por alto la violación de responsabilidad única), suponga que se requiere que persista una versión codificada en UTF-8 de la cadena en un campo orientado a bytes, pero en realidad persiste Shift JIS. Algún otro componente leerá la base de datos y espera ver UTF-8, de ahí el requisito. Luego, el viaje de ida y vuelta a través de este objeto informará el nombre y la dirección correctos porque lo convertirá de nuevo desde Shift JIS, pero su prueba no detecta el error. Es de esperar que sea detectado por alguna prueba de integración posterior, pero el objetivo de las pruebas unitarias es detectar los problemas temprano y saber exactamente qué componente es responsable.

Si uno de ellos no está haciendo lo que se supone que debe hacer, entonces su propio caso de prueba fallará y podemos arreglarlo y volver a ejecutar la batería de prueba.

No puede asumir esto, porque si no tiene cuidado, escribe un conjunto de pruebas mutuamente dependientes. El "¿salva?" prueba llama al método de guardar que está probando, y luego al método de carga para confirmar que está guardado. El "¿se carga?" test llama al método save para configurar el dispositivo de prueba y luego al método de carga que está probando para verificar el resultado. Ambas pruebas se basan en la exactitud del método que no están probando, lo que significa que ninguno de ellos prueba realmente la exactitud del método que está probando.

La pista de que hay un problema aquí es que dos pruebas que supuestamente están probando unidades diferentes en realidad hacen lo mismo . Ambos llaman a un setter seguido de un getter, luego verifican que el resultado sea el valor original. Pero quería probar que el setter persiste los datos, no que el par setter / getter trabaje en conjunto. Entonces, sabes que algo anda mal, solo tienes que averiguar qué y arreglar las pruebas.

Si su código está bien diseñado para pruebas unitarias, entonces hay al menos dos formas en que puede probar si los datos realmente han sido conservados correctamente por el método bajo prueba:

  • simule la interfaz de la base de datos y haga que su simulacro registre el hecho de que se han llamado a las funciones adecuadas con los valores esperados. Esto prueba que el método hace lo que se supone que debe hacer, y es la prueba unitaria clásica.

  • pasarle una base de datos real con exactamente la misma intención , para registrar si los datos se han conservado correctamente o no. Pero en lugar de tener una función simulada que simplemente dice "sí, obtuve los datos correctos", su prueba se lee directamente de la base de datos y confirma que es correcta. Puede que esta no sea la prueba más pura posible, porque un motor de base de datos completo es bastante importante para escribir un simulacro glorificado, con más posibilidades de que pase por alto alguna sutileza que haga pasar una prueba aunque algo esté mal (por ejemplo, yo no debería usar la misma conexión de base de datos para leer que se usó para escribir, porque podría ver una transacción no confirmada). Pero prueba lo correcto, y al menos sabes que precisamente implementa toda la interfaz de la base de datos sin tener que escribir ningún código simulado.

Por lo tanto, es un simple detalle de la implementación de la prueba si leo los datos de la base de datos de prueba por JDBC o si me burlo de la base de datos. De cualquier manera, el punto es que puedo probar la unidad mejor aislándola que si puedo permitir que conspire con otros métodos incorrectos en la misma clase para que se vea bien, incluso cuando algo está mal. Por lo tanto, quiero usar cualquier medio conveniente para verificar que los datos correctos persistieron, aparte de confiar en el componente cuyo método estoy probando.

Si su código no está bien diseñado para pruebas unitarias, es posible que no tenga otra opción, ya que el objeto cuyo método desea probar podría no aceptar la base de datos como una dependencia inyectada. En cuyo caso, la discusión sobre la mejor manera de aislar la unidad bajo prueba, se convierte en una discusión sobre qué tan cerca es posible aislar la unidad bajo prueba. Sin embargo, la conclusión es la misma. Si puede evitar conspiraciones entre unidades defectuosas, lo hace, sujeto al tiempo disponible y a cualquier otra cosa que piense que sería más eficaz para encontrar fallas en el código.


0

Así es como me gusta hacerlo. Esta es solo una elección personal ya que esto no afectará el resultado del producto, solo la forma de producirlo.

Esto depende de las posibilidades de poder agregar valores simulados en su base de datos sin la aplicación que está probando.

Digamos que tiene dos pruebas: A - leer la base de datos B - insertar en la base de datos (depende de A)

Si A pasa, entonces está seguro de que el resultado de B dependerá de la parte probada en B y no de las dependencias. Si A falla, puede tener un falso negativo para B. El error puede estar en B o en A. No puede estar seguro hasta que A devuelva un éxito.

No intentaría escribir algunas consultas en una prueba de unidad ya que no debería poder conocer la estructura de la base de datos detrás de la aplicación (o podría cambiar). Lo que significa que su caso de prueba podría fallar debido a su propio código. A menos que escriba una prueba para la prueba, pero quién evaluará la prueba para la prueba ...


-1

Al final, todo se reduce a lo que quieres probar.

A veces es suficiente probar la interfaz proporcionada de su clase (por ejemplo, el registro se crea y almacena y se puede recuperar más tarde, tal vez después de un "reinicio"). En este caso, está bien (y generalmente es una buena idea) usar los métodos proporcionados para las pruebas. Esto permite que las pruebas se reutilicen, por ejemplo, si desea cambiar el mecanismo de almacenamiento (base de datos diferente, base de datos en memoria para pruebas, ...).

Tenga en cuenta que esto significa que realmente solo le importa que la clase se adhiera a su contrato (creando, almacenando y recuperando registros en este caso), no cómo logra esto. Si crea sus pruebas unitarias de forma inteligente (contra una interfaz, no una clase directamente), puede probar diferentes implementaciones, ya que también deben cumplir con el contrato de la interfaz.

Por otro lado, en algunos casos tienes que verificar cómo las cosas persisten (¿quizás algún otro proceso se basa en que los datos estén en el formato correcto en esa base de datos?). Ahora no puede simplemente confiar en recuperar el registro correcto de la clase, sino que debe verificar que esté almacenado correctamente en la base de datos. Y la forma más directa de hacerlo es consultar la base de datos en sí.

Ahora no solo le importa la clase que se adhiere a su contrato (crear, almacenar y recuperar), sino también cómo lo logra (dado que los datos persistentes deben adherirse a otra interfaz). Sin embargo, ahora las pruebas dependen tanto de la interfaz de clase como del formato de almacenamiento, por lo que será difícil reutilizarlas por completo. (Algunos podrían llamar a este tipo de pruebas "pruebas de integración").


@downvoters: ¿podría decirme sus razones para que pueda mejorar los aspectos que cree que me faltan?
hoffmale
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.