TDD Red-Green-Refactor y si / cómo probar métodos que se vuelven privados

91

por lo que yo entiendo, la mayoría de la gente parece estar de acuerdo en que los métodos privados no deben probarse directamente, sino a través de cualquier método público que los llame. Puedo ver su punto, pero tengo algunos problemas con esto cuando trato de seguir las "Tres leyes de TDD", y uso el ciclo "Rojo - verde - refactor". Creo que se explica mejor con un ejemplo:

En este momento, necesito un programa que pueda leer un archivo (que contiene datos separados por tabulaciones) y filtrar todas las columnas que contienen datos no numéricos. Supongo que probablemente ya hay algunas herramientas simples disponibles para hacer esto, pero decidí implementarlo desde cero, principalmente porque pensé que podría ser un proyecto agradable y limpio para mí para poder practicar con TDD.

Entonces, primero, "me pongo el sombrero rojo", es decir, necesito una prueba que falla. Pensé, necesitaré un método que encuentre todos los campos no numéricos en una línea. Así que escribo una prueba simple, por supuesto que no se compila de inmediato, así que comienzo a escribir la función en sí misma, y después de un par de ciclos de ida y vuelta (rojo / verde) tengo una función de trabajo y una prueba completa.

A continuación, continúo con una función, "gatherNonNumericColumns" que lee el archivo, una línea a la vez, y llama a mi función "findNonNumericFields" en cada línea para reunir todas las columnas que eventualmente deben eliminarse. Un par de ciclos rojo-verde, y he terminado, teniendo de nuevo, una función de trabajo y una prueba completa.

Ahora, me imagino que debería refactorizar. Como mi método "findNonNumericFields" se diseñó solo porque pensé que lo necesitaría al implementar "gatherNonNumericColumns", me parece que sería razonable dejar que "findNonNumericFields" se vuelva privado. Sin embargo, eso rompería mis primeras pruebas, ya que ya no tendrían acceso al método que estaban probando.

Entonces, termino con métodos privados y una serie de pruebas que lo prueban. Dado que tanta gente aconseja que no se deben probar los métodos privados, parece que me he metido en un rincón aquí. Pero, ¿dónde exactamente fallé?

Supongo que podría haber comenzado en un nivel superior, escribiendo una prueba que pruebe lo que eventualmente se convertirá en mi método público (es decir, encontrarAndFilterOutAllNonNumericalColumns), pero eso se siente un tanto contrario a todo el punto de TDD (al menos según Tío Bob): que debes cambiar constantemente entre las pruebas de escritura y el código de producción, y que, en cualquier momento, todas tus pruebas funcionaron en el último minuto. Porque si comienzo escribiendo una prueba para un método público, pasarán varios minutos (u horas, o incluso días en casos muy complejos) antes de que todos los detalles de los métodos privados funcionen para que la prueba que prueba al público el método pasa.

Entonces, ¿qué hacer? ¿Es TDD (con el rápido ciclo de refactor rojo-verde) simplemente no es compatible con los métodos privados? ¿O hay una falla en mi diseño?

    
pregunta Henrik Berg 15.04.2015 - 10:51
fuente

15 respuestas

42

Unidades

Creo que puedo identificar exactamente dónde comenzó el problema:

  

Pensé que necesitaría un método que encuentre todos los campos no numéricos en una línea.

Esto debería seguirse inmediatamente preguntándose "¿Será esa una unidad comprobable por separado para gatherNonNumericColumns o parte de la misma?"

Si la respuesta es " sí, separa ", entonces su curso de acción es simple: ese método debe ser público en una clase apropiada, para que pueda probarse como una unidad. Su mentalidad es algo como "Necesito probar un método de eliminación y también necesito probar otro método"

Por lo que dices, sin embargo, pensaste que la respuesta es " no, parte de la misma ". En este punto, su plan ya no debería ser escribir completamente y probar findNonNumericFields luego escribir gatherNonNumericColumns . En su lugar, debería ser simplemente escribir gatherNonNumericColumns . Por ahora, findNonNumericFields debería ser una parte probable del destino que tiene en mente cuando elige su próximo caso de prueba rojo y hace su refactorización. Esta vez su mentalidad es "Necesito probar un método de eliminación, y mientras lo hago, debo tener en cuenta que mi implementación completa probablemente incluirá este otro método".

Mantener un ciclo corto

Hacer lo anterior no debería no conducir a los problemas que describe en su penúltimo párrafo:

  

Porque si comienzo escribiendo una prueba para un método público, habrá varios minutos (u horas, o incluso días en casos muy complejos) antes de que todos los detalles de los métodos privados funcionen para que la prueba probando los pases del método público.

En ningún momento esta técnica requiere que escriba una prueba roja que solo se volverá verde cuando implemente la totalidad de findNonNumericFields desde cero. Es mucho más probable que findNonNumericFields comience como un código en línea en el método público que está probando, el cual se acumulará en el transcurso de varios ciclos y se extraerá eventualmente durante una refactorización.

Hoja de ruta

Para proporcionar una hoja de ruta aproximada para este ejemplo en particular, no conozco los casos de prueba exactos que utilizó, pero digo que estaba escribiendo gatherNonNumericColumns como su método público. Entonces, lo más probable es que los casos de prueba sean los mismos que los que escribió para findNonNumericFields , cada uno de los cuales utiliza una tabla con una sola fila. Cuando ese escenario de una fila se implementó por completo y deseaba escribir una prueba para forzarlo a extraer el método, escribiría un caso de dos filas que requeriría que agregue su iteración.

    
respondido por el Ben Aaronson 15.04.2015 - 17:01
fuente
66

Mucha gente piensa que las pruebas unitarias se basan en métodos; no es. Debe basarse en la unidad más pequeña que tenga sentido. Para la mayoría de las cosas, esto significa que la clase es lo que debe probar como una entidad completa. No hay métodos individuales en él.

Ahora, obviamente, llamará a los métodos de la clase, pero debería pensar que las pruebas se aplican al objeto de caja negra que tiene, por lo que debería poder ver las operaciones lógicas que proporciona su clase; Estas son las cosas que necesitas probar. Si su clase es tan grande que la operación lógica es demasiado compleja, entonces tiene un problema de diseño que debe solucionarse primero.

Una clase con mil métodos puede parecer comprobable, pero si solo prueba cada método individualmente, realmente no está probando la clase. Es posible que algunas clases deban estar en cierto estado antes de llamar a un método, por ejemplo, una clase de red que necesita una configuración de conexión antes de enviar datos. El método de envío de datos no puede considerarse independientemente de toda la clase.

Por lo tanto, debería ver que los métodos privados son irrelevantes para las pruebas. Si no puede ejercer sus métodos privados llamando a la interfaz pública de su clase, entonces esos métodos privados son inútiles y no serán utilizados de ninguna manera.

Creo que muchas personas intentan convertir los métodos privados en unidades comprobables porque parece fácil realizar pruebas para ellos, pero esto lleva la granularidad de la prueba demasiado lejos. Martin Fowler dice

  

Aunque empiezo con la noción de que la unidad es una clase, a menudo   tomar un montón de clases estrechamente relacionadas y tratarlos como un solo   unidad

que tiene mucho sentido para un sistema orientado a objetos, ya que los objetos están diseñados para ser unidades. Si desea probar métodos individuales, tal vez debería estar creando un sistema de procedimiento como C, o una clase compuesta completamente de funciones estáticas.

    
respondido por el gbjbaanb 15.04.2015 - 11:24
fuente
51

El hecho de que sus métodos de recopilación de datos son lo suficientemente complejos como para merecer las pruebas y lo suficientemente separados de su objetivo principal para que sean métodos propios en lugar de parte de algunos puntos de bucle a la solución: haga estos los métodos no son privados, pero los miembros de alguna otra clase de otra que proporciona funciones de recopilación / filtrado / tabulación.

Luego escribe pruebas para los aspectos estúpidos de la recopilación de datos de la clase auxiliar (por ejemplo, "distinguir números de caracteres") en un lugar y pruebas para su objetivo principal (por ejemplo, "obtener las cifras de ventas") en otro lugar, y no tiene que repetir las pruebas básicas de filtrado en las pruebas para su lógica de negocios normal.

En general, si su clase que hace una cosa contiene un código extenso para hacer otra cosa que se requiere para, pero aparte de, su propósito principal, ese código debe estar en otra clase y ser llamado a través de métodos públicos. No debe estar oculto en las esquinas privadas de una clase que solo accidentalmente contiene ese código. Esto mejora la capacidad de prueba y la comprensión al mismo tiempo.

    
respondido por el Kilian Foth 15.04.2015 - 10:58
fuente
29

Personalmente, siento que te diste mucho de la mente de implementación cuando escribiste las pruebas. Usted asumió que necesitaría ciertos métodos. ¿Pero realmente necesitas que hagan lo que la clase debe hacer? ¿Fallaría la clase si alguien viniera y lo refactara internamente? Si estuviera utilizando la clase (y esa debería ser la mentalidad del evaluador en mi opinión), realmente podría importarle menos si hay un método explícito para verificar los números.

Debes probar la interfaz pública de una clase. La implementación privada es privada por una razón. No es parte de la interfaz pública porque no es necesaria y puede cambiar. Es un detalle de implementación.

Si escribe pruebas en la interfaz pública, nunca obtendrá el problema con el que se encontró. O puede crear casos de prueba para la interfaz pública que cubren sus métodos privados (excelente) o no puede. En ese caso, podría ser el momento de pensar detenidamente sobre los métodos privados y tal vez eliminarlos por completo si no se los puede encontrar de todos modos.

    
respondido por el nvoigt 15.04.2015 - 11:28
fuente
11

No hace TDD según lo que espera que la clase haga internamente.

Sus casos de prueba deben basarse en lo que la clase / funcionalidad / programa tiene que hacer al mundo externo. En su ejemplo, ¿alguna vez el usuario llamará a su clase de lector con find all the non-numerical fields in a line?

Si la respuesta es "no", en primer lugar es una mala prueba. Desea escribir la prueba en el nivel de funcionalidad a nivel de clase / interfaz , no el nivel de "qué tendrá que implementar el método de clase para que esto funcione", que es su prueba.

El flujo de TDD es:

  • rojo (¿qué hace la clase / objeto / función / etc al mundo externo?
  • verde (escriba el código mínimo para que funcione esta función del mundo externo)
  • refactor (cuál es el mejor código para hacer que esto funcione)

NO es hacer "porque necesitaré X en el futuro como un método privado, déjame implementarlo y pruébelo primero. "Si se encuentra haciendo esto, está haciendo la etapa" roja "incorrectamente. Este parece ser su problema aquí.

Si te encuentras a menudo escribiendo pruebas para métodos que se convierten en métodos privados, estás haciendo una de las pocas cosas:

  • No entiendo correctamente los casos de uso de su interfaz / nivel público lo suficientemente bien como para escribir una prueba para ellos
  • Cambiando drásticamente su diseño y refactorizando varias pruebas (lo que puede ser bueno, dependiendo de si esa funcionalidad se prueba en las pruebas más nuevas)
respondido por el enderland 16.04.2015 - 05:08
fuente
9

Te encuentras con un error común con las pruebas en general.

La mayoría de las personas que son nuevas en las pruebas comienzan a pensar de esta manera:

  • escriba una prueba para la función F
  • implementar F
  • escriba una prueba para la función G
  • implementar G usando una llamada a F
  • escribe una prueba para una función H
  • implementar H usando una llamada a G

y así sucesivamente.

El problema aquí es que, de hecho, no tiene ninguna prueba unitaria para la función H. La prueba que se supone que prueba a H en realidad está probando H, G y F al mismo tiempo.

Para resolver esto, debes darte cuenta de que las unidades comprobables nunca deben depender unas de otras, sino más bien de sus interfaces . En su caso, donde las unidades son funciones simples, las interfaces son solo su firma de llamada. Por lo tanto, debe implementar G de tal manera que pueda usarse con cualquier función que tenga la misma firma que F.

Cómo se puede hacer exactamente esto depende de su lenguaje de programación. En muchos idiomas puede pasar funciones (o punteros a ellos) como argumentos a otras funciones. Esto le permitirá probar cada función por separado.

    
respondido por el initcrash 15.04.2015 - 17:53
fuente
8

Se supone que las pruebas que escribe durante el desarrollo guiado por pruebas se aseguran de que una clase implemente correctamente su API pública, al mismo tiempo que se asegura de que esa API pública sea fácil de probar y usar.

Por supuesto, puedes usar métodos privados para implementar esa API, pero no es necesario crear pruebas a través de TDD: la funcionalidad se probará porque la API pública funcionará correctamente.

Ahora suponga que sus métodos privados son lo suficientemente complicados como para que merezcan pruebas independientes, pero no tienen sentido como parte de la API pública de su clase original. Bueno, esto probablemente significa que en realidad deberían ser métodos públicos en alguna otra clase, uno que su clase original aproveche en su propia implementación.

Al probar solo la API pública, está facilitando la modificación de los detalles de la implementación en el futuro. Las pruebas que no son útiles solo lo molestarán más adelante cuando deban reescribirse para admitir una refactorización elegante que acaba de descubrir.

    
respondido por el Bill Michell 15.04.2015 - 19:49
fuente
4

Creo que la respuesta correcta es la conclusión a la que llegó al comenzar con los métodos públicos. Comenzarías escribiendo una prueba que llame a ese método. Fallaría, así que creas un método con ese nombre que no hace nada. Entonces puede que tenga razón en una prueba que verifique un valor de retorno.

(No estoy completamente seguro de lo que hace su función. ¿Devuelve una cadena con el contenido del archivo con los valores no numéricos eliminados?)

Si su método devuelve una cadena, verifique ese valor de retorno. Así que simplemente continúen construyéndolo.

Creo que cualquier cosa que ocurra en un método privado debería estar en el método público en algún momento durante su proceso, y luego solo se cambió al método privado como parte de un paso de refactorización. Refactorizar no requiere tener pruebas fallidas, que yo sepa. Solo necesitas pruebas fallidas al agregar funcionalidad. Solo necesita ejecutar sus pruebas después del refactor para asegurarse de que todas pasen.

    
respondido por el Matt Dyer 16.04.2015 - 00:04
fuente
3
  

se siente como si me hubiera pintado en una esquina aquí. Pero, ¿dónde exactamente fallé?

Hay un viejo adagio.

  

Cuando no planeas, planeas fallar.

La gente parece pensar que cuando haces TDD, simplemente te sientas, escribes las pruebas y el diseño simplemente sucederá mágicamente. Esto no es cierto Debe tener un plan de alto nivel. Me he dado cuenta de que obtengo mis mejores resultados de TDD cuando diseño la interfaz (API pública) primero. Personalmente, creo un interface real que define la clase primero.

jadeo ¡Escribí un "código" antes de escribir cualquier prueba! Bueno no. No lo hice Escribí un contrato a seguir, un diseño . Sospecho que podría obtener resultados similares al anotar un diagrama UML en papel cuadriculado. El punto es, debes tener un plan. TDD no es una licencia para ir a piratear un pedazo de código.

Realmente siento que "Test First" es un nombre inapropiado. Diseñar primero luego probar.

Por supuesto, siga los consejos que otros han dado para extraer más clases de su código. Si siente la necesidad de probar las partes internas de una clase, extraiga esas partes internas en una unidad fácil de probar e inyecte.

    
respondido por el RubberDuck 17.04.2015 - 03:37
fuente
2

¡Recuerde que las pruebas también se pueden refactorizar! Si hace que un método sea privado, está reduciendo la API pública y, por lo tanto, es perfectamente aceptable deshacerse de algunas pruebas correspondientes para esa "funcionalidad perdida" (AKA reducida complejidad).

Otros han dicho que su método privado se llamará como parte de sus otras pruebas de API, o será inalcanzable y, por lo tanto, eliminable. De hecho, las cosas son más detalladas si pensamos en rutas de ejecución .

Por ejemplo, si tenemos un método público que realiza la división, es posible que queramos probar la ruta que produce la división por cero. Si hacemos que el método sea privado, tenemos una opción: o podemos considerar la ruta de división por cero, o podemos eliminar esa ruta considerando cómo lo llaman los otros métodos.

De esta manera, podemos eliminar algunas pruebas (por ejemplo, dividir por cero) y refactorizar las otras en términos de la API pública restante. Por supuesto, en un mundo ideal, las pruebas existentes se ocupan de todos los caminos restantes, pero la realidad siempre es un compromiso;)

    
respondido por el Warbo 15.04.2015 - 16:33
fuente
2

Hay ocasiones en que un método privado podría convertirse en un método público de otra clase.

Por ejemplo, es posible que tenga métodos privados que no sean seguros para subprocesos y deje la clase en un estado temporal. Estos métodos pueden ser movidos a una clase separada que es mantenida en privado por su primera clase. Entonces, si su clase es una Cola, puede tener una clase InternalQueue que tenga métodos públicos, y la clase Queue mantenga la instancia de InternalQueue en privado. Esto le permite probar la cola interna, y también deja en claro cuáles son las operaciones individuales en InternalQueue.

(Esto es más obvio cuando imagina que no había una clase de Lista, y si intentó implementar las funciones de Lista como métodos privados en la clase que las usa).

    
respondido por el Thomas Andrews 15.04.2015 - 20:49
fuente
0

Me pregunto por qué su idioma solo tiene dos niveles de privacidad, completamente público y completamente privado.

¿Puede organizar que sus métodos no públicos sean accesibles al paquete o algo así? Luego coloque sus pruebas en el mismo paquete y disfrute probando el funcionamiento interno que no es parte de la interfaz pública. Su sistema de compilación excluirá las pruebas al crear un binario de lanzamiento.

Por supuesto, a veces necesitas tener métodos verdaderamente privados, que no sean accesibles a nada más que a la clase definitoria. Espero que todos esos métodos sean muy pequeños. En general, mantener los métodos pequeños (por ejemplo, por debajo de 20 líneas) ayuda mucho: las pruebas, el mantenimiento y solo entender el código se vuelve más fácil.

    
respondido por el 9000 15.04.2015 - 17:01
fuente
0

Ocasionalmente, he golpeado métodos privados para protegerlos y permitir pruebas más precisas (más estrictas que la API pública expuesta). Esta debería ser la excepción (con suerte muy rara) en lugar de la regla, pero puede ser útil en ciertos casos específicos con los que puede encontrarse. Además, eso es algo que no querría considerar al crear una API pública, más de "trampa" que se puede usar en el software de uso interno en esas situaciones raras.

    
respondido por el Brian Knoblauch 15.04.2015 - 18:49
fuente
0

Experimenté esto y sentí tu dolor.

Mi solución fue:

deja de tratar las pruebas como construir un monolito.

Recuerde que cuando ha escrito un conjunto de pruebas, digamos 5, para eliminar algunas funciones, no tiene que mantener todas estas pruebas alrededor , especialmente cuando esto se convierte en parte de otra cosa.

Por ejemplo, a menudo tengo:

  • prueba de nivel bajo 1
  • código para cumplirlo
  • prueba de bajo nivel 2
  • código para cumplirlo
  • prueba de bajo nivel 3
  • código para cumplirlo
  • prueba de nivel bajo 4
  • código para cumplirlo
  • prueba de nivel bajo 5
  • código para cumplirlo

entonces tengo

  • prueba de nivel bajo 1
  • prueba de bajo nivel 2
  • prueba de bajo nivel 3
  • prueba de nivel bajo 4
  • prueba de nivel bajo 5

Sin embargo, si ahora agrego funciones de nivel superior que lo llaman, que tienen muchas pruebas, podría ser capaz de reducir esas pruebas de bajo nivel para simplemente ser:

  • prueba de nivel bajo 1
  • prueba de nivel bajo 5

El diablo está en los detalles y la capacidad para hacerlo dependerá de las circunstancias.

    
respondido por el Michael Durrant 19.04.2015 - 13:27
fuente
-2

¿El sol gira alrededor de la tierra o la tierra alrededor del sol? Según Einstein, la respuesta es sí, o ambos, ya que ambos modelos difieren solo en el punto de vista, del mismo modo, la encapsulación y el desarrollo basado en pruebas solo están en conflicto porque creemos que lo están. Nos sentamos aquí, como Galileo y el Papa, lanzándonos insultos: tonto, ¿no ves que los métodos privados también necesitan pruebas? hereje, no rompas la encapsulación! Del mismo modo, cuando reconocemos que la verdad es más grandiosa de lo que pensamos, podemos intentar algo así como encapsular las pruebas para las interfaces privadas para que las pruebas para las interfaces públicas no rompan la encapsulación.

Intente esto: agregue dos métodos, uno que no tiene entrada pero solo justs devuelve el número de pruebas privadas y otro que toma un número de prueba como parámetro y devuelve pasar / fallar.

    
respondido por el hildred 18.04.2015 - 16:52
fuente

Lea otras preguntas en las etiquetas