¿Hay algún punto en las pruebas unitarias que apagan y se burlan de todo lo público?

59

Cuando realizo pruebas unitarias de la manera "correcta", es decir, en cada llamada pública y devuelvo valores o simulaciones preestablecidas, siento que en realidad no estoy probando nada. Literalmente estoy mirando mi código y creando ejemplos basados ​​en el flujo de la lógica a través de mis métodos públicos. Y cada vez que cambia la implementación, tengo que ir y cambiar esas pruebas, una vez más, sin sentir realmente que estoy logrando algo útil (ya sea a mediano o largo plazo). También hago pruebas de integración (incluidas las rutas no felices) y realmente no me importa el aumento de los tiempos de prueba. Con eso, siento que en realidad estoy probando regresiones, porque han detectado múltiples, mientras que todas las pruebas unitarias me muestran que la implementación de mi método público cambió, lo cual ya sé.

Las pruebas unitarias son un tema amplio, y siento que soy yo quien no comprende algo aquí. ¿Cuál es la ventaja decisiva de las pruebas unitarias frente a las pruebas de integración (excluyendo la sobrecarga de tiempo)?

cautiva
fuente
44
Mis dos centavos: no abusar de los simulacros
Juan Mendes
1
Ver también "Burlarse es un olor a código"
user949300

Respuestas:

37

Cuando realizo pruebas unitarias de la manera "correcta", es decir, en cada llamada pública y devuelvo valores o simulaciones preestablecidas, siento que en realidad no estoy probando nada. Literalmente estoy mirando mi código y creando ejemplos basados ​​en el flujo de la lógica a través de mis métodos públicos.

Esto parece que el método que está probando necesita varias otras instancias de clase (de las que tiene que burlarse), y llama a varios métodos por sí solo.

Este tipo de código es realmente difícil de probar por unidad, por las razones que usted describe.

Lo que he encontrado útil es dividir tales clases en:

  1. Clases con la "lógica de negocios" real. Estos usan pocas o ninguna llamada a otras clases y son fáciles de probar (valor (es) dentro - valor fuera).
  2. Clases que interactúan con sistemas externos (archivos, bases de datos, etc.). Estos envuelven el sistema externo y proporcionan una interfaz conveniente para sus necesidades.
  3. Clases que "unen todo"

Luego, las clases de 1. son fáciles de probar por unidad, porque solo aceptan valores y devuelven un resultado. En casos más complejos, estas clases pueden necesitar realizar llamadas por su cuenta, pero solo llamarán a las clases desde 2. (y no llamarán directamente, por ejemplo, a una función de base de datos), y las clases desde 2. son fáciles de burlar (porque solo exponga las partes del sistema envuelto que necesita).

Las clases de 2. y 3. por lo general no se pueden probar de manera significativa en la unidad (porque no hacen nada útil por sí mismas, solo son códigos de "pegamento"). OTOH, estas clases tienden a ser relativamente simples (y pocas), por lo que deberían cubrirse adecuadamente con pruebas de integración.


Un ejemplo

Una clase

Supongamos que tiene una clase que recupera un precio de una base de datos, aplica algunos descuentos y luego actualiza la base de datos.

Si tiene todo esto en una clase, deberá llamar a las funciones de DB, que son difíciles de burlar. En pseudocódigo:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Los tres pasos necesitarán acceso a la base de datos, por lo que una gran cantidad de burlas (complejas), que probablemente se rompan si el código o la estructura de la base de datos cambian.

Separar

Se divide en tres clases: PriceCalculation, PriceRepository, App.

PriceCalculation solo realiza el cálculo real y obtiene los valores que necesita. La aplicación une todo:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

De esa manera:

  • PriceCalculation encapsula la "lógica empresarial". Es fácil de probar porque no llama a nada por sí solo.
  • PriceRepository se puede probar con pseudounidades configurando una base de datos simulada y probando las llamadas de lectura y actualización. Tiene poca lógica, por lo tanto, pocas rutas de código, por lo que no necesita demasiadas de estas pruebas.
  • La aplicación no se puede probar de manera significativa en la unidad, porque es un código de pegamento. Sin embargo, también es muy simple, por lo que las pruebas de integración deberían ser suficientes. Si más tarde la aplicación se vuelve demasiado compleja, rompes más clases de "lógica de negocios".

Finalmente, puede resultar que PriceCalculation debe hacer sus propias llamadas a la base de datos. Por ejemplo, porque solo PriceCalculation sabe qué datos necesita, por lo que App no ​​puede obtenerlos por adelantado. Luego puede pasarle una instancia de PriceRepository (o alguna otra clase de repositorio), adaptada a las necesidades de PriceCalculation. Entonces será necesario burlarse de esta clase, pero será simple, porque la interfaz de PriceRepository es simple, por ejemplo PriceRepository.getPrice(articleNo, contractType). Lo más importante es que la interfaz de PriceRepository aísla PriceCalculation de la base de datos, por lo que es poco probable que los cambios en el esquema de la base de datos o la organización de datos cambien su interfaz y, por lo tanto, rompan las simulaciones.

sleske
fuente
55
Pensé que estaba solo sin ver el punto en la unidad probando todo, gracias
entromete
44
Simplemente no estoy de acuerdo cuando dices que las clases de tipo 3 son pocas, siento que la mayoría de mi código es de tipo 3 y casi no hay lógica de negocios. Esto es lo que quiero decir: stackoverflow.com/questions/38496185/…
Rodrigo Ruiz
27

¿Cuál es la ventaja decisiva de las pruebas unitarias frente a las pruebas de integración?

Esa es una falsa dicotomía.

Las pruebas unitarias y las pruebas de integración sirven para dos propósitos similares pero diferentes. El propósito de las pruebas unitarias es asegurarse de que sus métodos funcionen. En términos prácticos, las pruebas unitarias aseguran que el código cumpla con el contrato descrito por las pruebas unitarias. Esto es evidente en la forma en que se diseñan las pruebas unitarias: establecen específicamente lo que se supone que debe hacer el código y afirman que el código lo hace.

Las pruebas de integración son diferentes. Las pruebas de integración ejercitan la interacción entre los componentes del software. Puede tener componentes de software que pasen todas sus pruebas y aún así fallen las pruebas de integración porque no interactúan correctamente.

Sin embargo, si hay una ventaja decisiva para las pruebas unitarias, es esto: las pruebas unitarias son mucho más fáciles de configurar y requieren mucho menos tiempo y esfuerzo que las pruebas de integración. Cuando se usan correctamente, las pruebas unitarias fomentan el desarrollo de código "comprobable", lo que significa que el resultado final será más confiable, más fácil de entender y más fácil de mantener. El código comprobable tiene ciertas características, como una API coherente, un comportamiento repetible y devuelve resultados que son fáciles de afirmar.

Las pruebas de integración son más difíciles y más caras, porque a menudo necesita burlas elaboradas, configuraciones complejas y afirmaciones difíciles. En el nivel más alto de integración del sistema, imagine intentar simular la interacción humana en una interfaz de usuario. Sistemas de software completos están dedicados a ese tipo de automatización. Y lo que buscamos es la automatización; Las pruebas en humanos no son repetibles y no se escalan como lo hacen las pruebas automatizadas.

Finalmente, las pruebas de integración no garantizan la cobertura del código. ¿Cuántas combinaciones de bucles de código, condiciones y ramas está probando con sus pruebas de integración? ¿De verdad lo sabes? Existen herramientas que puede usar con las pruebas unitarias y los métodos bajo prueba que le indicarán cuánta cobertura de código tiene y cuál es la complejidad ciclomática de su código. Pero solo funcionan bien a nivel de método, donde viven las pruebas unitarias.


Si sus pruebas cambian cada vez que refactoriza, ese es un problema diferente. Se supone que las pruebas unitarias consisten en documentar lo que hace su software, probar que lo hace y luego probar que lo hace nuevamente cuando refactoriza la implementación subyacente. Si su API cambia, o necesita que sus métodos cambien de acuerdo con un cambio en el diseño del sistema, eso es lo que se supone que sucederá. Si está sucediendo mucho, considere escribir sus pruebas primero, antes de escribir el código. Esto lo obligará a pensar en la arquitectura general y le permitirá escribir código con la API ya establecida.

Si pasa mucho tiempo escribiendo pruebas unitarias para código trivial como

public string SomeProperty { get; set; }

entonces deberías reexaminar tu enfoque. Se supone que las pruebas unitarias prueban el comportamiento, y no hay comportamiento en la línea de código anterior. Sin embargo, ha creado una dependencia en su código en alguna parte, ya que es casi seguro que se hará referencia a esa propiedad en otra parte de su código. En lugar de hacerlo, considere escribir métodos que acepten la propiedad necesaria como parámetro:

public string SomeMethod(string someProperty);

Ahora su método no tiene ninguna dependencia de algo fuera de sí mismo, y ahora es más comprobable, ya que es completamente autónomo. De acuerdo, no siempre podrá hacer esto, pero mueve su código en la dirección de ser más comprobable, y esta vez está escribiendo una prueba unitaria para el comportamiento real.

Robert Harvey
fuente
2
Soy consciente de que el servidor de pruebas unitarias y las pruebas de integración tienen diferentes objetivos, sin embargo, todavía no entiendo cómo las pruebas unitarias son útiles si se resisten y se burlan de todas las llamadas públicas que hacen las pruebas unitarias. Entendería que "el código cumple con el contrato descrito por las pruebas unitarias", si no fuera por trozos y simulacros; mis pruebas unitarias son literalmente reflejos de lógica dentro de los métodos que estoy probando. Usted (yo) realmente no está probando nada, solo mira su código y lo 'convierte' en pruebas. En cuanto a la dificultad de la automatización y la cobertura del código, actualmente estoy haciendo Rails, y ambos están bien atendidos.
invade
2
Si sus pruebas son solo reflejos de la lógica del método, lo está haciendo mal. Sus pruebas unitarias deberían esencialmente darle un valor al método, aceptar un valor de retorno y hacer una afirmación sobre cuál debería ser ese valor de retorno. No se requiere lógica para hacer eso.
Robert Harvey
2
Tiene sentido, pero aún tiene que bloquear todas las demás llamadas públicas (db, algunas 'globales', como el estado actual del usuario, etc.) y terminar con el código de prueba siguiendo la lógica del método.
invade
1
Entonces, ¿supongo que las pruebas unitarias son para cosas en su mayoría aisladas que confirman como 'conjunto de entradas -> conjunto de resultados esperados'?
invade
1
Mi experiencia en la creación de muchas pruebas unitarias y de integración (sin mencionar las herramientas avanzadas de burlas, pruebas de integración y cobertura de código utilizadas por esas pruebas) contradice la mayoría de sus afirmaciones aquí: 1) "El propósito de las pruebas unitarias es asegurarse de que su el código hace lo que se supone ": lo mismo se aplica a las pruebas de integración (aún más); 2) "las pruebas unitarias son mucho más fáciles de configurar": no, no lo son (con frecuencia, son las pruebas de integración las que son más fáciles); 3) "Cuando se usa correctamente, las pruebas unitarias fomentan el desarrollo de código" comprobable ": lo mismo con las pruebas de integración; (continúa)
Rogério
4

Las pruebas unitarias con simulacros son para asegurarse de que la implementación de la clase sea correcta. Se burla de las interfaces públicas de las dependencias del código que está probando. De esta manera, tiene control sobre todo lo externo a la clase y está seguro de que una prueba fallida se debe a algo interno de la clase y no a uno de los otros objetos.

También está probando el comportamiento de la clase bajo prueba, no la implementación. Si refactoriza el código (creando nuevos métodos internos, etc.), las pruebas unitarias no deberían fallar. Pero si está cambiando lo que hace el método público, entonces absolutamente las pruebas deberían fallar porque ha cambiado el comportamiento.

También parece que está escribiendo las pruebas después de haber escrito el código, intente escribir las pruebas primero. Intente delinear el comportamiento que la clase debería tener y luego escriba la cantidad mínima de código para que las pruebas pasen.

Tanto las pruebas unitarias como las pruebas de integración son útiles para garantizar la calidad de su código. Las pruebas unitarias examinan cada componente de forma aislada. Y las pruebas de integración aseguran que todos los componentes interactúen correctamente. Quiero tener ambos tipos en mi suite de prueba.

Las pruebas unitarias me han ayudado en mi desarrollo, ya que puedo concentrarme en una parte de la aplicación a la vez. Burlándose de los componentes que aún no he hecho. También son excelentes para la regresión, ya que documentan cualquier error en la lógica que he encontrado (incluso en las pruebas unitarias).

ACTUALIZAR

Crear una prueba que solo se asegure de que se invoquen los métodos tiene valor, ya que se asegura de que los métodos realmente se llamen. Particularmente si primero escribe sus pruebas, tiene una lista de verificación de los métodos que deben suceder. Dado que este código es bastante de procedimiento, no tiene mucho que verificar, aparte de que se llaman los métodos. Estás protegiendo el código para el cambio en el futuro. Cuando necesita llamar a un método antes que al otro. O que siempre se llama a un método, incluso si el método inicial arroja una excepción.

Es posible que la prueba para este método nunca cambie o solo cambie cuando está cambiando los métodos. ¿Por qué es esto algo malo? Ayuda a reforzar el uso de las pruebas. Si tiene que arreglar una prueba después de cambiar el código, tendrá la costumbre de cambiar las pruebas con el código.

Schleis
fuente
Y si un método no llama a nada privado, entonces no tiene sentido que la unidad lo pruebe, ¿correcto?
invade
Si el método es privado, no lo prueba explícitamente, debe probarse a través de la interfaz pública. Todos los métodos públicos deben ser probados, para garantizar que el comportamiento sea correcto.
Schleis
No, quiero decir que si un método público no llama a nada privado, ¿tiene sentido probar ese método público?
invade
Si. El método hace algo, ¿no? Por lo tanto, debe ser probado. Desde el punto de vista de la prueba, no sé si está usando algo privado. Solo sé que si proporciono la entrada A, debería obtener la salida B.
Schleis
Ah, sí, el método hace algo y ese algo es llamar a otros métodos públicos (y solo eso). Entonces, la forma en que 'probaría' adecuadamente 'es poner las llamadas con algunos valores de retorno y luego configurar las expectativas del mensaje. ¿Qué EXACTAMENTE estás probando en este caso? ¿Que se hacen las llamadas correctas? Bueno, escribiste ese método y puedes mirarlo y ver exactamente lo que hace. Creo que las pruebas unitarias son más apropiadas para los métodos aislados que se supone que deben usarse como 'entrada -> salida', por lo que puede configurar un montón de ejemplos y luego hacer pruebas de regresión cuando refactorice.
invade
3

Estaba experimentando una pregunta similar, hasta que descubrí el poder de las pruebas de componentes. En resumen, son lo mismo que las pruebas unitarias, excepto que no se burla de forma predeterminada, sino que usa objetos reales (idealmente mediante inyección de dependencia).

De esa manera, puede crear rápidamente pruebas robustas con una buena cobertura de código. No es necesario actualizar tus simulacros todo el tiempo. Puede ser un poco menos preciso que las pruebas unitarias con 100% de simulacros, pero el tiempo y el dinero que ahorra lo compensan. Lo único que realmente necesita para usar simulacros o accesorios son backends de almacenamiento o servicios externos.

En realidad, la burla excesiva es un antipatrón: los antipatrones y los simulacros de TDD son malos .

lastzero
fuente
0

Aunque la operación ya ha marcado una respuesta, solo estoy agregando mis 2 centavos aquí.

¿Cuál es la ventaja decisiva de las pruebas unitarias frente a las pruebas de integración (excluyendo la sobrecarga de tiempo)?

Y también en respuesta a

Cuando realizo pruebas unitarias de la manera "correcta", es decir, en cada llamada pública y devuelvo valores o simulaciones preestablecidas, siento que en realidad no estoy probando nada.

Hay un útil pero no exactamente lo que OP ha pedido:

¿Las pruebas unitarias funcionan pero todavía hay errores?

Desde mi poca experiencia en las suites de pruebas, entiendo que las pruebas unitarias siempre son para probar la funcionalidad de nivel de método más básica de una clase. En mi opinión, cada método, ya sea público, privado o interno, merece tener una unidad de pruebas dedicada. Incluso en mi experiencia reciente, tenía un método público que llamaba a otro pequeño método privado. Entonces, había dos enfoques:

  1. no cree una (s) prueba (s) unitaria (s) para método privado.
  2. crear una (s) prueba (s) de unidad para un método privado.

Si piensa lógicamente, el punto de tener un método privado es: el método público principal se está volviendo demasiado grande o desordenado. Para resolver esto, usted refactoriza sabiamente y crea pequeños fragmentos de código que merecen ser métodos privados separados que a su vez hacen que su método público principal sea menos voluminoso. Refactoriza teniendo en cuenta que este método privado podría reutilizarse más adelante. Puede haber casos en los que no exista otro método público que dependa de ese método privado, pero quién sabe sobre el futuro.


Considerando el caso cuando el método privado es reutilizado por muchos otros métodos públicos.

Entonces, si hubiera elegido el enfoque 1: habría duplicado las pruebas unitarias y habrían sido complicadas, ya que tenía varias pruebas unitarias para cada rama del método público y privado.

Si hubiera elegido el enfoque 2: el código escrito para las pruebas unitarias sería relativamente menor, y sería mucho más fácil probarlo.


Considerando el caso cuando el método privado no se reutiliza No tiene sentido escribir una prueba unitaria separada para ese método.

En cuanto a las pruebas de integración , tienden a ser exhaustivas y más de alto nivel. Le dirán que, dado un aporte, todas sus clases deben llegar a esta conclusión final. Para comprender más sobre la utilidad de las pruebas de integración, consulte el enlace mencionado.

shankbond
fuente