¿Las pruebas unitarias no deberían usar mis propios métodos?

80

Hoy estuve viendo el video " JUnit y el autor dijo que cuando probaba un método determinado en tu programa , no debes usar otros de tus propios métodos en el proceso.

Para ser más específico, él estaba hablando acerca de probar algún método de creación de registros que tomó 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 prueba de este método, no debería usar sus otros métodos DAO para consultar la base de datos verifique el resultado final (para verificar que el registro fue creado con los datos correctos). Afirmó que para eso, debería escribir un código adicional JDBC 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 (de nuevo) su propio código de validación / soporte (que debería ser más específico y enfocado, 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 ), ¿no debería estar bien usar solo algunos de esos métodos mientras se prueban otros? Si uno de ellos no está haciendo lo que se supone que debe hacer, su propio caso de prueba fallará, y podemos arreglarlo y ejecutar la batería de prueba nuevamente. No es necesario duplicar el código (incluso si el código duplicado es algo más simple) o esfuerzos desperdiciados.

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

¿Puede por favor compartir sus ideas sobre esto?

    
pregunta carlossierra 06.09.2016 - 17:40

11 respuestas

186

El espíritu de su afirmación es ciertamente correcto. El punto de las pruebas unitarias es aislar el código, probarlo libre de dependencias, para que cualquier comportamiento erróneo pueda ser reconocido rápidamente donde está sucediendo.

Dicho esto, la prueba de unidad es una herramienta, y está destinada a servir a sus propósitos, no es un altar al que se le ore. A veces, eso significa dejar las dependencias porque funcionan de manera suficientemente confiable y no quiere molestarse en burlarse de ellas, a veces eso significa que algunas de sus pruebas de unidad son en realidad bastante cercanas, si no en realidad son pruebas de integración.

En última instancia, no te estás calificando, lo que es importante es el producto final del software que se está probando, pero solo debes tener en cuenta cuándo estás doblando las reglas y decidir cuándo se realizarán las concesiones. vale la pena.

    
respondido por el whatsisname 06.09.2016 - 18:55
36

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

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

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

Sospecho que está utilizando la versión más flexible del término "prueba de unidad", 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 ejecute un código específico y luego inspeccione la base de datos para los criterios de aprobación.

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

    
respondido por el Eric King 06.09.2016 - 19:13
7

La respuesta es sí y no ...

Las pruebas únicas que se realizan de forma aislada son una necesidad absoluta, ya que le permite sentar 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 que tenga pruebas que crucen unidades.

Estas pruebas de unidad cruzada son buenas para la cobertura del código y cuando se realizan pruebas de funcionalidad de extremo a extremo, pero tienen algunos inconvenientes que debe tener en cuenta:

  • Sin una prueba aislada para confirmar solo lo que se está rompiendo, una prueba fallida de "unidad cruzada" requerirá una solución de problemas adicional para determinar qué es lo que está mal con su código.
  • Confiar demasiado en las pruebas entre unidades puede sacarte de la mentalidad de contrato en la que siempre deberías estar al escribir código SOLID orientado a objetos. Las pruebas aisladas normalmente tienen sentido porque sus unidades solo deben realizar una acción básica.
  • La prueba de extremo a extremo es deseable, pero puede ser peligrosa si una prueba requiere que escriba en una base de datos o realice alguna acción que no desearía que se produzca en un entorno de producción. Esta es una de las muchas razones por las que los marcos burlones como Mockito son tan populares porque te permiten simular un objeto e imitar una prueba de extremo a extremo sin realmente cambiando algo que no deberías.

Al final del día, desea realizar algunas de las dos ... muchas pruebas que atrapan la funcionalidad de bajo nivel y algunas que prueban la funcionalidad de extremo a extremo. Céntrate en la razón por la que estás escribiendo pruebas en primer lugar. Están allí para darle confianza de que su código está funcionando de la manera que usted espera. Todo lo que necesitas hacer para que eso suceda está bien.

Lo 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 desliza cuando un equipo de desarrollo se enfrenta a plazos difíciles. Por lo tanto, 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 lo "puras" que son sus pruebas.

    
respondido por el DanK 06.09.2016 - 19:01
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, te estás perdiendo el dolor de una implementación de prueba difícil, y ese dolor te está enseñando algo sobre tu código subyacente. Las pruebas dolorosas ahora significan un mantenimiento doloroso más tarde.

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

    
respondido por el Karl Bielefeldt 06.09.2016 - 21:10
1

Si la unidad de funcionalidad que se está probando es 'si los datos se almacenan de forma persistente y recuperable', me gustaría que la prueba de la unidad compruebe: almacene en una base de datos real, destruya los objetos que contienen referencias a la base de datos y luego llame al código para recuperar los objetos.

Probar si un registro se crea en la base de datos parece preocuparse por 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 prueba de unidad denota una 'unidad de código' o 'unidad de funcionalidad', funcionalidad que tal vez haya sido creada por muchas unidades de código en concierto. No considero que esta distinción sea útil; las preguntas que debería tener en cuenta son "¿la prueba le dice algo sobre si el sistema proporciona valor comercial" y "la prueba es frágil si la implementación cambia?" Pruebas como la que se describe son útiles cuando está realizando una TDD en el sistema; aún no ha escrito el 'objeto de obtención del registro de la base de datos', por lo que no puede probar la unidad completa de funcionalidad, pero es frágil contra los cambios de implementación, así que elimínelos una vez que la operación completa sea verificable.

    
respondido por el Pete Kirkham 07.09.2016 - 10:14
1

El espíritu es correcto.

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

Lo ideal es que apague 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. Usted explícitamente, de manera positiva, no desea probar el DB al probar uno de sus propios métodos.

Los beneficios son muchos. Más que nada, las pruebas se vuelven deslumbrantes porque no necesita preocuparse por configurar un entorno de base de datos correcto y revertirlo posteriormente.

Por supuesto, este es un gran objetivo en cualquier proyecto de software que no haga esto desde el principio. Pero es el espíritu de unidad probando.

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

    
respondido por el AnoE 07.09.2016 - 11:53
1

Hay al menos una razón muy importante no para usar el acceso directo a la base de datos en sus pruebas unitarias para el código de negocio que interactúa con la base de datos: si cambia la implementación de la base de datos, tendrá que volver a escribir 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 de su asignador de datos. Sin embargo, si no usa su asignador de datos mientras realiza las pruebas, verá que también tiene que cambiar cada prueba unitaria que haga 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 buscar y reemplazar.

Además, usar su asignador de datos como un mecanismo abstracto en lugar de hablar directamente con la base de datos hace que sea más fácil eliminar la dependencia de la base de datos por completo , lo que podría no ser relevante ahora, pero cuando llegue a hasta miles de pruebas unitarias y todas están llegando a una base de datos, estarás agradecido de poder refactorizarlas fácilmente para eliminar esa dependencia porque tu conjunto de pruebas pasará de tomar minutos para correr a tomar segundos, lo que puede tener una gran beneficiarse de su productividad.

Su pregunta indica que está pensando en la dirección correcta. Como sospechas, las pruebas unitarias también son un código. Es igual de importante hacer que sean fáciles de mantener y adaptar a los cambios futuros como para el resto de su base de código. 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 vale nada.

    
respondido por el Periata Breatta 08.09.2016 - 19:11
1

La mejor lección que aprendí al aprender acerca de las pruebas unitarias y las 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 lo lee nuevamente comienzan a ser verificables. Por supuesto, si está probando los métodos específicamente, terminará con una prueba para cada método aislado, 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 se produce debido a la perspectiva de los métodos de prueba. No pruebes los métodos. Pruebas de comportamiento. ¿Cuál es el comportamiento de la clase WidgetDao? Persiste los widgets. Ok, ¿cómo verificas que los widgets persistan? Bueno, ¿cuál es la definición de persistencia? Significa que cuando lo escribas, puedes leerlo de nuevo. Así que 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);
}

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

Las otras respuestas se centran en lo importante que es el aislamiento, el debate pragmático frente al realista, y si una prueba de unidad puede o no consultar una base de datos. Sin embargo, aquellos que realmente no responden a la pregunta.

Para probar si se puede almacenar algo, debe almacenarlo y luego leerlo nuevamente. 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ás con pruebas que no te dicen nada.

    
respondido por el Brandon 08.09.2016 - 22:54
0

Un ejemplo de un caso de falla que prefieres atrapar, es que el objeto bajo prueba utiliza una capa de almacenamiento en caché, pero no logra conservar los datos según sea necesario. Luego, si consulta el objeto, dirá "sí, tengo el nuevo nombre y la nueva dirección", pero desea que la prueba falle porque en realidad no hizo lo que se suponía.

Alternativamente (y pasando por alto la violación de responsabilidad única), supongamos 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 los volverá a convertir desde Shift JIS, pero su prueba no detectó el error. Es de esperar que se detecte en una prueba de integración posterior, pero el objetivo principal 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, su propio caso de prueba fallará y podremos arreglarlo y ejecutar la batería de prueba nuevamente.

No puedes asumir esto, porque si no tienes cuidado, escribe un conjunto de pruebas mutuamente dependientes. El "ahorra?" prueba llama al método de guardar que está probando, y luego el método de carga para confirmar que se guardó. El "se carga?" prueba llama al método de guardar para configurar el dispositivo de prueba y luego el método de carga que está probando para verificar el resultado. Ambas pruebas se basan en la corrección del método que no están probando, lo que significa que ninguna de ellas realmente prueba la corrección del método que está probando.

La clave de que hay un problema aquí es que dos pruebas que supuestamente están probando diferentes unidades 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 establecedor persiste en los datos, no que el par de establecedor / getter funcione en conjunto. Entonces, sabes que algo está mal, solo tienes que averiguar qué y corregir las pruebas.

Si su código está bien diseñado para la prueba de unidad, entonces hay al menos dos formas de probar si el método bajo prueba realmente ha persistido correctamente en los datos:

  • simula la interfaz de la base de datos y haz que tu simulacro registre el hecho de que se han activado 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.

  • pásele 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. Es posible que esta no sea la prueba más pura posible, porque un motor de base de datos completo es algo muy importante para escribir un simulacro glorificado, con más posibilidades de que pase por alto alguna sutileza que hace que una prueba pase, aunque sea algo está mal (por ejemplo, no debería usar la misma conexión de base de datos para leer que la que se usó para escribir, porque es posible que vea 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.

Así que es un mero detalle de la implementación de 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 mejor la unidad aislándola que puedo si permito que conspire con otros métodos incorrectos en la misma clase para que se vean bien, incluso cuando algo está mal. Por lo tanto, quiero utilizar cualquier medio conveniente para verificar que los datos correctos se mantuvieron, aparte de confiar en el componente cuyo método estoy probando.

Si su código no está bien diseñado para pruebas de unidad, 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 sometida a prueba, se convierte en una discusión acerca de qué tan cerca es posible aislar la unidad sometida a prueba. Sin embargo, la conclusión es la misma. Si puede evitar las conspiraciones entre unidades defectuosas, entonces lo hace, sujeto al tiempo disponible y cualquier otra cosa que piense que sería más eficaz para encontrar fallas en el código.

    
respondido por el Steve Jessop 07.09.2016 - 16:17
0

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

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

Digamos que tienes dos pruebas: A - leyendo la base de datos B: insértelo 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, puedes tener un falso negativo para B. El error puede estar en B o en A. No puedes 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 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 probará la prueba para la prueba ...

    
respondido por el AxelH 08.09.2016 - 14:17
-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) utilizar los métodos proporcionados para la prueba. 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 solo le importa realmente que la clase se adhiera a su contrato (crear, almacenar y recuperar registros en este caso), no cómo lo logra. Si crea sus pruebas unitarias de forma inteligente (contra una interfaz, no directamente una clase), puede probar diferentes implementaciones, ya que también tienen que cumplir con el contrato de la interfaz.

Por otra parte, en algunos casos tienes que verificar realmente cómo se persisten las cosas (¿quizás algún otro proceso se base en que los datos estén en el formato correcto en esa base de datos?). Ahora no puede confiar simplemente en obtener el registro correcto de la clase, sino que debe verificar que esté correctamente almacenado 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 que la clase se adhiera a su contrato (crear, almacenar y recuperar), sino también cómo lo logra (ya que los datos persistentes deben adherirse a otra interfaz). Sin embargo, ahora las pruebas dependen de la interfaz de clase y del formato de almacenamiento, por lo que será difícil reutilizarlos por completo. (Algunos podrían llamar a estos tipos de pruebas "pruebas de integración".)

    
respondido por el hoffmale 06.09.2016 - 23:23

Lea otras preguntas en las etiquetas