¿Deberíamos probar todos nuestros métodos?

62

Así que hoy tuve una conversación con mi compañero de equipo sobre las pruebas unitarias. Todo comenzó cuando me preguntó "oye, ¿dónde están las pruebas para esa clase, solo veo una?". Toda la clase era un gerente (o un servicio si prefieres llamarlo así) y casi todos los métodos simplemente delegaban cosas a un DAO, por lo que era similar a:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

Un tipo de repetitivo sin lógica (o al menos no considero una delegación tan simple como lógica) pero un repetitivo útil en la mayoría de los casos (separación de capas, etc.). Y tuvimos una discusión bastante larga sobre si debería o no probarlo unitariamente (creo que vale la pena mencionar que hice una prueba completa del DAO). Sus principales argumentos son que no era TDD (obviamente) y que alguien podría querer ver la prueba para verificar qué hace este método (no sé cómo podría ser más obvio) o que en el futuro alguien podría querer cambiar la prueba. implementación y agregue nueva (o más como "cualquier") lógica (en cuyo caso supongo que alguien debería simplemente probar esa lógica ).

Sin embargo, esto me hizo pensar. ¿Debemos esforzarnos por obtener el mayor porcentaje de cobertura de prueba? ¿O es simplemente un arte por el arte entonces? Simplemente no veo ninguna razón detrás de probar cosas como:

  • captadores y establecedores (a menos que realmente tengan algo de lógica en ellos)
  • "código repetitivo

Obviamente, una prueba para tal método (con simulacros) me llevaría menos de un minuto, pero supongo que todavía es tiempo perdido y un milisegundo más para cada CI.

¿Hay alguna razón racional / no "inflamable" de por qué uno debe probar cada línea de código (o tantas como pueda)?

Zenzen
fuente
2
Todavía estoy decidiendo sobre esta pregunta, pero he aquí una charla de alguien que ha decidido que la respuesta es "no". Ian Cooper: TDD, ¿dónde salió todo mal? Para resumir esta gran charla, debe probar de afuera hacia adentro y probar nuevos comportamientos, no nuevos métodos.
Daniel Kaplan
Esta es realmente una gran charla, una visita obligada, una charla reveladora para mucha gente, me encanta. Pero creo que la respuesta no es "no". Es "sí, pero indirectamente". Ian Cooper habla sobre la arquitectura hexagonal y las características / comportamientos de prueba que se burlan / topan los puertos. En este caso, estos puertos son los DAO y este "administrador / servicio" se prueba no con una prueba de unidad individual solo para esta clase sino con una "prueba de unidad" (unidad en la definición de Ian Cooper con la que estoy completamente de acuerdo) que prueba alguna característica en su dominio que usa este administrador / servicio.
AlfredoCasado
Dependerá en cierta medida de su sistema, si está desarrollando un sistema con un nivel de certificación de seguridad de moderado a alto, deberá cubrir todos los métodos, independientemente de la triviallity
jk.

Respuestas:

49

Voy por la regla de oro de Kent Beck:

Prueba todo lo que pueda romperse.

Por supuesto, eso es subjetivo hasta cierto punto. Para mí, los captadores / setters triviales y las frases sencillas como la suya anterior no valen la pena. Pero, de nuevo, paso la mayor parte de mi tiempo escribiendo pruebas unitarias para código heredado, solo soñando con un buen proyecto TDD greenfield ... En tales proyectos, las reglas son diferentes. Con el código heredado, el objetivo principal es cubrir la mayor cantidad de terreno con el menor esfuerzo posible, por lo que las pruebas unitarias tienden a ser de mayor nivel y más complejas, más como pruebas de integración si uno es pedante con respecto a la terminología. Y cuando tiene dificultades para aumentar la cobertura general del código del 0%, o simplemente logra superarlo en un 25%, los captadores y establecedores de pruebas unitarias son la menor de sus preocupaciones.

OTOH en un proyecto de TDD totalmente nuevo, puede ser más práctico escribir pruebas incluso para tales métodos. Especialmente porque ya ha escrito la prueba antes de tener la oportunidad de comenzar a preguntarse "¿vale esta una línea una prueba dedicada?" Y al menos estas pruebas son triviales de escribir y rápidas de ejecutar, por lo que no es un gran problema de ninguna manera.

Péter Török
fuente
¡Ah, olvidé totalmente esa cita! Supongo que lo usaré como mi argumento principal porque, francamente, ¿qué se puede romper aquí? En realidad no mucho. Lo único que puede romperse es la invocación del método y, si eso sucede, significa que sucedió algo realmente malo. ¡Gracias!
Zenzen
55
@Zenzen: "¿qué se puede romper aquí? Realmente no mucho". - Entonces puede romperse. Solo un pequeño error tipográfico. O alguien agrega un código. O estropea la dependencia. Realmente creo que Beck afirmaría que su ejemplo principal califica como rompible. Getters y setters, menos aún, aunque me he encontrado en un error de copiar / pegar, incluso entonces. La verdadera pregunta es, si es demasiado trivial para escribir una prueba, ¿por qué existe?
pdr
1
La cantidad de tiempo que pasó pensando en eso ya podría haber escrito la prueba. digo escribir la prueba, no te vayas cuando no escribir una prueba como un área gris, aparecerán más ventanas rotas.
kett_chup
1
Agregaré que mi experiencia general es que probar getters y setters es algo valioso a largo plazo, pero de baja prioridad. La razón es porque tiene una probabilidad "cero" de encontrar un error ahora, no puede garantizar que otro desarrollador no agregue algo en tres meses ("una simple declaración if") que tendrá la posibilidad de romperse . Tener una prueba de unidad en su lugar protege contra eso. Al mismo tiempo, no es realmente una prioridad demasiado alta, porque no vas a encontrar nada pronto de esa manera.
Dclements
77
Probar a ciegas todo lo que podría romperse no tiene sentido. Debe haber una estrategia en la que los componentes de alto riesgo se prueben primero.
CodeART
13

Existen pocos tipos de pruebas unitarias:

  • Basado en el estado. Actúas y luego afirmas contra el estado del objeto. Por ejemplo, hago un depósito. Luego verifico si el saldo ha aumentado.
  • Valor de retorno basado. Actúa y afirma contra el valor de retorno.
  • Basada en la interacción. Verifica que su objeto llamó a otro objeto. Esto parece ser lo que estás haciendo en tu ejemplo.

Si primero escribiera su prueba, tendría más sentido, ya que esperaría llamar a una capa de acceso a datos. La prueba fallaría inicialmente. Luego escribiría el código de producción para hacer pasar la prueba.

Idealmente, debería probar el código lógico, pero las interacciones (objetos que llaman a otros objetos) son igualmente importantes. En tu caso, lo haría

  • Compruebe que llamé a la capa de acceso a datos con el parámetro exacto que se ha pasado.
  • Compruebe que solo se haya llamado una vez.
  • Verifique que devuelva exactamente lo que me ha dado la capa de acceso a datos. De lo contrario, bien podría devolver nulo.

Actualmente no hay lógica allí, pero no siempre será así.

Sin embargo, si está seguro de que no habrá lógica en este método y es probable que permanezca igual, entonces consideraría llamar a la capa de acceso a datos directamente desde el consumidor. Haría esto solo si el resto del equipo está en la misma página. No desea enviar un mensaje incorrecto al equipo diciendo "Hola chicos, está bien ignorar la capa de dominio, solo llame a la capa de acceso a datos directamente".

También me concentraría en probar otros componentes si hubiera una prueba de integración para este método. Sin embargo, todavía no he visto una empresa con pruebas de integración sólidas.

Habiendo dicho todo esto, no probaría ciegamente todo. Establecería los puntos calientes (componentes con alta complejidad y alto riesgo de rotura). Luego me concentraría en estos componentes. No tiene sentido tener una base de código donde el 90% de la base de código es bastante sencilla y está cubierta por pruebas unitarias, cuando el 10% restante representa la lógica central del sistema y no están cubiertas por pruebas unitarias debido a su complejidad.

Finalmente, ¿cuál es el beneficio de probar este método? ¿Cuáles son las implicaciones si esto no funciona? ¿Son catastróficos? No se esfuerce por obtener una alta cobertura de código. La cobertura del código debe ser un producto derivado de un buen conjunto de pruebas unitarias. Por ejemplo, puede escribir una prueba que caminará por el árbol y le dará una cobertura del 100% de este método, o puede escribir tres pruebas unitarias que también le darán una cobertura del 100%. La diferencia es que al escribir tres pruebas se prueban casos extremos, en lugar de simplemente caminar por el árbol.

CodeART
fuente
¿Por qué verificaría que su DAL solo se haya llamado una vez?
Marjan Venema
9

Aquí hay una buena manera de pensar en la calidad de su software:

  1. la verificación de tipos está manejando parte del problema.
  2. las pruebas se encargarán del resto

Para las funciones triviales y repetitivas, puede confiar en que la verificación de tipos haga su trabajo, y para el resto, necesita casos de prueba.

tp1
fuente
Por supuesto, la verificación de tipos solo funciona si está usando tipos específicos en su código, y está trabajando en un lenguaje compilado o de lo contrario se asegura de que una verificación de análisis estático se ejecute con frecuencia, por ejemplo, como parte de CI.
bdsl
6

En mi opinión, la complejidad ciclomática es un parámetro. Si un método no es lo suficientemente complejo (como getters y setters). No se necesitan pruebas unitarias. El nivel de Complejidad Ciclomática de McCabe debería ser más de 1. Otra palabra debería haber una declaración mínima de 1 bloque.

Fırat KÜÇÜK
fuente
Recuerde que algunos captadores o establecedores tienen efectos secundarios (aunque se desaconseja y se considera una mala práctica en la mayoría de los casos), por lo que el cambio en su código fuente también puede afectarlo.
Andrzej Bobak
3

Un rotundo SÍ con TDD (y con algunas excepciones)

Muy controvertido, pero diría que a cualquiera que responda 'no' a esta pregunta le falta un concepto fundamental de TDD.

Para mí, la respuesta es un rotundo si sigues TDD. Si no lo eres, entonces no es una respuesta plausible.

El DDD en TDD

TDD es a menudo citado por tener los principales beneficios.

  • Defensa
    • Asegurar que el código puede cambiar pero no su comportamiento .
    • Esto permite la práctica siempre tan importante de refactorización .
    • Ganas este TDD o no.
  • Diseño
    • Usted especifica qué debe hacer algo, cómo debe comportarse antes de implementarlo .
    • Esto a menudo significa decisiones de implementación más informadas .
  • Documentación
    • El conjunto de pruebas debe servir como la documentación de especificación (requisitos).
    • El uso de pruebas para tal propósito significa que la documentación y la implementación siempre están en un estado consistente; un cambio en uno significa un cambio en otro. Compare con los requisitos de mantenimiento y el diseño en un documento de Word separado.

Separar la responsabilidad de la implementación

Como programadores, es terriblemente tentador pensar en los atributos como algo de importancia y captadores y establecedores como una especie de sobrecarga.

Pero los atributos son un detalle de implementación, mientras que los establecedores y captadores son la interfaz contractual que realmente hace que los programas funcionen.

Es mucho más importante deletrear que un objeto debe:

Permitir a sus clientes cambiar su estado.

y

Permitir a sus clientes consultar su estado

entonces cómo se almacena realmente este estado (para el cual un atributo es la forma más común, pero no la única).

Una prueba como

(The Painter class) should store the provided colour

es importante para la parte de documentación de TDD.

El hecho de que la implementación final es trivial (atributo) y no conlleva ningún beneficio de defensa debe ser desconocido para usted cuando escribe la prueba.

La falta de ingeniería de ida y vuelta ...

Uno de los problemas clave en el mundo del desarrollo del sistema es la falta de ingeniería de ida y vuelta 1 : el proceso de desarrollo de un sistema se fragmenta en subprocesos desarticulados cuyos artefactos (documentación, código) a menudo son inconsistentes.

1 Brodie, Michael L. "John Mylopoulos: cosiendo semillas del modelado conceptual". Modelado conceptual: fundamentos y aplicaciones. Springer Berlin Heidelberg, 2009. 1-9.

... y cómo lo resuelve TDD

Es la parte de documentación de TDD que asegura que las especificaciones del sistema y su código sean siempre consistentes.

Diseñe primero, implemente después

Dentro de TDD, primero escribimos la prueba de aceptación fallida, solo luego escribimos el código que les permitió pasar.

Dentro del BDD de nivel superior, primero escribimos escenarios y luego los hacemos pasar.

¿Por qué debería excluir setters y getter?

En teoría, es perfectamente posible dentro de TDD que una persona escriba la prueba y otra implemente el código que la hace pasar.

Entonces pregúntate a ti mismo:

En caso de que la persona que escribe las pruebas para una clase mencione captadores y colocadores.

Como getters y setters son una interfaz pública para una clase, la respuesta es obviamente , o no habrá forma de establecer o consultar el estado de un objeto.

Obviamente, si escribe el código primero, la respuesta puede no ser tan clara.

Excepciones

Hay algunas excepciones obvias a esta regla: funciones que son detalles de implementación claros y claramente no forman parte del diseño del sistema.

Por ejemplo, a el método local 'B ()':

function A() {

    // B() will be called here    

    function B() {
        ...
    }
} 

O la función privada square()aquí:

class Something {
private:
    square() {...}
public:
    addAndSquare() {...}
    substractAndSquare() {...}
}

O cualquier otra función que no sea parte de una publicinterfaz que necesite ortografía en el diseño del componente del sistema.

Izhaki
fuente
1

Cuando se enfrente a una pregunta filosófica, vuelva a los requisitos de manejo.

¿Su objetivo es producir software razonablemente confiable a un costo competitivo?

¿O es para producir software de la mayor confiabilidad posible casi sin importar el costo?

Hasta cierto punto, los dos objetivos de calidad y velocidad de desarrollo / costo se alinean: pasa menos tiempo escribiendo pruebas que reparando defectos.

Pero más allá de ese punto, no lo hacen. No es tan difícil, digamos, un error reportado por desarrollador por mes. Reducir a la mitad a uno por dos meses solo libera un presupuesto de quizás un día o dos, y esa cantidad de pruebas adicionales probablemente no reducirá a la mitad su tasa de defectos. Entonces ya no es un simple ganar / ganar; debe justificarlo en función del costo del defecto para el cliente.

Este costo variará (y, si desea ser malvado, también lo hará su capacidad para hacer que se le apliquen dichos costos, ya sea a través del mercado o de una demanda). No quieres ser malvado, así que cuentas esos costos por completo; a veces algunas pruebas aún globalmente hacen que el mundo sea más pobre por su existencia.

En resumen, si intenta aplicar ciegamente los mismos estándares a un sitio web interno que el software de vuelo de un avión de pasajeros, terminará fuera del negocio o en la cárcel.

soru
fuente
0

Su respuesta a esto depende de su filosofía (¿cree que es Chicago vs Londres? Estoy seguro de que alguien lo buscará). El jurado todavía está fuera de esto en el enfoque más efectivo en tiempo (porque, después de todo, ese es el mayor impulsor de este tiempo menos dedicado a las soluciones).

Algunos enfoques dicen probar solo la interfaz pública, otros dicen probar el orden de cada llamada de función en cada función. Se han librado muchas guerras santas. Mi consejo es probar ambos enfoques. Elija una unidad de código y hágalo como X, y otra como Y. Después de unos meses de prueba e integración, regrese y vea cuál se adapta mejor a sus necesidades.

luego
fuente
0

Es una pregunta difícil.

Hablando estrictamente, diría que no es necesario. Es mejor escribir pruebas de unidad de estilo BDD y de nivel de sistema que aseguren que los requisitos comerciales funcionen según lo previsto en escenarios positivos y negativos.

Dicho esto, si su método no está cubierto por estos casos de prueba, debe preguntarse por qué existe en primer lugar y si es necesario, o si hay requisitos ocultos en el código que no se reflejan en su documentación o historias de usuario que debe codificarse en un caso de prueba de estilo BDD.

Personalmente, me gusta mantener la cobertura por línea en torno al 85-95% y los registros de entrada a la línea principal para asegurar que la cobertura de prueba de unidad existente por línea alcance este nivel para todos los archivos de código y que no se descubran archivos.

Suponiendo que se sigan las mejores prácticas de prueba, esto ofrece una gran cobertura sin obligar a los desarrolladores a perder tiempo tratando de descubrir cómo obtener cobertura adicional en código difícil de ejercer o código trivial simplemente por el bien de la cobertura.

Keith trae
fuente
-1

El problema es la pregunta en sí, no necesita probar todos los "métodos" o todas las "clases" que necesita para probar todas las características de sus sistemas.

Su pensamiento clave en términos de características / comportamientos en lugar de pensar en términos de métodos y clases. Por supuesto, un método está aquí para proporcionar soporte para una o más funciones, al final se prueba todo su código, al menos todo el código importa en su base de código.

En su escenario, probablemente esta clase de "administrador" es redundante o innecesaria (como todas las clases con un nombre que contiene la palabra "administrador"), o tal vez no, pero parece un detalle de implementación, probablemente esta clase no merece una unidad prueba porque esta clase no tiene ninguna lógica comercial relevante. Probablemente necesite esta clase para que alguna característica funcione, la prueba para esta característica cubre esta clase, de esta manera puede refactorizar esta clase y hacer una prueba que verifique que lo que importa, sus características, todavía funciona después de la refactorización.

Piense en características / comportamientos no en clases de métodos, no puedo repetir esto suficientes veces.

AlfredoCasado
fuente
-4

Sin embargo, esto me hizo pensar. ¿Debemos esforzarnos por obtener el mayor porcentaje de cobertura de prueba?

Sí, idealmente al 100%, pero algunas cosas no son comprobables por unidad.

captadores y establecedores (a menos que realmente tengan algo de lógica en ellos)

Getters / Setters son estúpidos , simplemente no los uses. En su lugar, coloque su variable miembro en la sección pública.

"código repetitivo

Obtenga un código común y pruébelo en la unidad. Eso debería ser tan simple como eso.

¿Hay alguna razón racional / no "inflamable" de por qué uno debe probar cada línea de código (o tantas como pueda)?

Al no hacerlo, es posible que te pierdas algunos errores muy obvios. Las pruebas unitarias son como una red segura para detectar ciertos tipos de errores, y debe usarla tanto como sea posible.

Y lo último: estoy en un proyecto donde la gente no quería perder el tiempo escribiendo pruebas unitarias para un "código simple", pero luego decidieron no escribir nada. Al final, partes del código se convirtieron en una gran bola de barro .

BЈовић
fuente
Bueno, vamos a aclarar una cosa: no quise decir que no uso TDD / pruebas de escritura. Todo lo contrario. Sé que las pruebas pueden encontrar errores en los que no pensé, pero ¿qué hay para probar aquí? Simplemente creo que ese método es uno de los "no comprobables por unidad". Como dijo Péter Török (citando a Kent Beck), debes probar cosas que pueden romperse. ¿Qué podría romperse aquí? Realmente no mucho (solo hay una delegación simple en este método). PUEDO escribir una prueba unitaria, pero simplemente tendrá una simulación del DAO y una afirmación, no muchas pruebas. En cuanto a getters / setters, algunos marcos los requieren.
Zenzen
1
Además, como no lo noté "Obtenga un código común y pruébelo en la unidad. Eso debería ser tan simple como eso". ¿Qué quieres decir con eso? Es una clase de servicio (en una capa de servicio entre la GUI y el DAO), es común a toda la aplicación. Realmente no puedo hacerlo más genérico (ya que acepta algunos parámetros y llama a cierto método en el DAO). La única razón es que hay que adherirse a la arquitectura en capas de la aplicación para que la GUI no llame directamente al DAO.
Zenzen
20
-1 para "Getters / Setters son estúpidos, simplemente no los uses. En su lugar, coloca tu variable miembro en la sección pública". - Muy mal. Esto se ha discutido varias veces en SO . Usar campos públicos en todas partes es realmente peor incluso que usar getters y setters en todas partes.
Péter Török