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 los métodos públicos que los llamen. Puedo ver su punto, pero tengo algunos problemas con esto cuando trato de seguir las "Tres leyes de TDD" y utilizo el ciclo "Rojo - verde - refactorizador". 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í practicar un poco con TDD.
Entonces, primero, "me puse 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. Entonces escribo una prueba simple, por supuesto, no se compila de inmediato, así que comienzo a escribir la función en sí, 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, "collectNonNumericColumns" 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, una vez más, tengo una función de trabajo y una prueba completa.
Ahora, me imagino que debería refactorizar. Dado que mi método "findNonNumericFields" fue diseñado solo porque pensé que lo necesitaría al implementar "collectNonNumericColumns", me parece razonable permitir 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 un conjunto de pruebas que lo prueban. Dado que tanta gente aconseja que no se prueben los métodos privados, parece que me he arrinconado aquí. ¿Pero dónde fracasé exactamente?
Supongo que podría haber comenzado en un nivel superior, escribiendo una prueba que prueba lo que eventualmente se convertirá en mi método público (es decir, findAndFilterOutAllNonNumericalColumns), pero eso se siente algo contrario al punto completo de TDD (al menos según el tío Bob) : Que debe cambiar constantemente entre escribir pruebas y código de producción, y que en cualquier momento, todas sus pruebas funcionaron en el último minuto más o menos. Porque si empiezo escribiendo una prueba para un método público, habrá varios minutos (u horas, o incluso días en casos muy complejos) antes de obtener todos los detalles en los métodos privados para que la prueba pruebe al público método pasa.
¿Entonces lo que hay que hacer? ¿TDD (con el ciclo rápido de refactorización rojo-verde) simplemente no es compatible con métodos privados? ¿O hay una falla en mi diseño?
fuente
private
si tuviera sentido hacerlo.Respuestas:
Unidades
Creo que puedo determinar exactamente dónde comenzó el problema:
Esto debe seguirse inmediatamente preguntándose "¿Será una unidad comprobable por separado
gatherNonNumericColumns
o parte de la misma?"Si la respuesta es " sí, separar ", 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 así como "Necesito probar un método de prueba y también tengo que probar otro método"
Sin embargo, por lo que dices, pensaste que la respuesta es " no, parte de lo mismo ". En este punto, su plan ya no debería ser escribir y probar completamente y
findNonNumericFields
luego escribirgatherNonNumericColumns
. En cambio, debería ser simplemente escribirgatherNonNumericColumns
. Por ahora,findNonNumericFields
debería ser una parte probable del destino que tiene en mente cuando elige su próximo caso de prueba roja y realiza su refactorización. Esta vez su mentalidad es "Necesito probar un método, y mientras lo hago, debo tener en cuenta que mi implementación final probablemente incluirá este otro método".Manteniendo un ciclo corto
Hacer lo anterior no debería conducir a los problemas que describe en su penúltimo párrafo:
En ningún momento esta técnica requiere que escriba una prueba roja que solo se volverá verde cuando implemente la totalidad
findNonNumericFields
desde cero. Es mucho más probablefindNonNumericFields
que comience como un código en línea en el método público que está probando, que se desarrollará en el transcurso de varios ciclos y finalmente se extraerá durante una refactorización.Mapa vial
Para dar una hoja de ruta aproximada para este ejemplo en particular, no sé los casos de prueba exactos que usó, pero digamos que estaba escribiendo
gatherNonNumericColumns
como su método público. Entonces, lo más probable es que los casos de prueba sean los mismos para los que escribiófindNonNumericFields
, cada uno usando una tabla con solo una fila. Cuando ese escenario de una fila se implementó por completo, y quería escribir una prueba para obligarlo a extraer el método, escribiría un caso de dos filas que requeriría que agregue su iteración.fuente
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 debería probar como una entidad completa. No métodos individuales en él.
Ahora, obviamente, invocará métodos en 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 necesita 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. Algunas clases pueden requerir estar en un cierto estado antes de llamar a un método, por ejemplo, una clase de red que necesita una conexión configurada antes de enviar datos. El método de envío de datos no puede considerarse independientemente de toda la clase.
Entonces 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 se utilizarán de todos modos.
Creo que muchas personas intentan convertir los métodos privados en unidades comprobables porque parece fácil ejecutar pruebas para ellos, pero esto lleva la granularidad de la prueba demasiado lejos. Martin Fowler dice
lo que tiene mucho sentido para un sistema orientado a objetos, los objetos están diseñados para ser unidades. Si desea probar métodos individuales, tal vez debería crear un sistema de procedimiento como C, o una clase compuesta en su totalidad de funciones estáticas.
fuente
El hecho de que sus métodos de recopilación de datos sean lo suficientemente complejos como para merecer pruebas y lo suficientemente separados de su objetivo principal como para ser métodos propios en lugar de parte de algunos puntos de bucle a la solución: haga que estos métodos no sean privados, sino miembros de otra clase que proporciona la funcionalidad de recopilación / filtrado / tabulación.
Luego, escribe pruebas para los aspectos tontos de mezcla 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 usted no No tiene que repetir las pruebas de filtrado básicas en las pruebas para su lógica comercial normal.
En general, si su clase que hace una cosa contiene un código extenso para hacer otra cosa que se requiere, pero aparte de su propósito principal, ese código debería vivir en otra clase y ser llamado a través de métodos públicos. No debe ocultarse en los rincones privados de una clase que solo contiene accidentalmente ese código. Esto mejora la capacidad de prueba y la comprensión al mismo tiempo.
fuente
Personalmente, creo que llegaste muy lejos en la mentalidad de implementación cuando escribiste las pruebas. Usted asumió que tendría que ciertos métodos. ¿Pero realmente necesita que hagan lo que se supone que debe hacer la clase? ¿Fallaría la clase si alguien viniera y los refactorizara internamente? Si estaba usando 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.
Debe 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ó. Puede crear casos de prueba para la interfaz pública que cubran 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 desecharlos todos juntos si de todos modos no se pueden alcanzar.
fuente
No haces TDD en función de lo que esperas que la clase haga internamente.
Sus casos de prueba deben basarse en lo que la clase / funcionalidad / programa tiene que hacer con el mundo externo. En su ejemplo, ¿alguna vez el usuario llamará a su clase de lector para
find all the non-numerical fields in a line?
Si la respuesta es "no", entonces es una mala prueba escribir en primer lugar. Desea escribir la prueba de funcionalidad a nivel de clase / interfaz , no el nivel "qué deberá implementar el método de clase para que esto funcione", que es lo que es su prueba.
El flujo de TDD es:
NO es para hacer "porque necesitaré X en el futuro como método privado, déjenme implementarlo y probarlo primero". Si te encuentras haciendo esto, estás haciendo la etapa "roja" incorrectamente. Este parece ser tu problema aquí.
Si te encuentras escribiendo frecuentemente pruebas para métodos que se convierten en métodos privados, estás haciendo una de las siguientes cosas:
fuente
Estás encontrando un error común con las pruebas en general.
La mayoría de las personas que son nuevas en las pruebas comienzan pensando de esta manera:
y así.
El problema aquí es que, de hecho, no tiene una prueba unitaria para la función H. La prueba que se supone que prueba H realmente prueba H, G y F al mismo tiempo.
Para resolver esto, debe darse 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 se pueda usar con cualquier función que tenga la misma firma que F.
Cómo exactamente esto se puede hacer depende de su lenguaje de programación. En muchos idiomas, puede pasar funciones (o punteros a ellas) como argumentos a otras funciones. Esto le permitirá probar cada función de forma aislada.
fuente
Se supone que las pruebas que usted escribe durante el Desarrollo conducido por pruebas se aseguran de que una clase implemente correctamente su API pública, al tiempo que se asegura de que esa API pública sea fácil de probar y usar.
Por supuesto, puede 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 merecer 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, una que su clase original aprovecha en su propia implementación.
Al probar solo la API pública, hace que sea mucho más fácil modificar los detalles de implementación en el futuro. Las pruebas inútiles solo te molestarán más tarde cuando necesiten reescribirse para admitir algunas refactorizaciones elegantes que acabas de descubrir.
fuente
Creo que la respuesta correcta es la conclusión a la que llegó acerca de comenzar con los métodos públicos. Comenzaría escribiendo una prueba que llame a ese método. Fallaría, por lo que crea un método con ese nombre que no hace nada. Entonces, tal vez haga una prueba que verifique el valor de retorno.
(No estoy del todo claro en cuanto a 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, entonces verifica ese valor de retorno. Así que continúas construyéndolo.
Creo que cualquier cosa que suceda en un método privado debería estar en el método público en algún momento durante su proceso, y luego solo pasar al método privado como parte de un paso de refactorización. Refactorizar no requiere tener pruebas fallidas, que yo sepa. Solo necesita pruebas fallidas al agregar funcionalidad. Solo necesita ejecutar sus pruebas después del refactor para asegurarse de que todas pasen.
fuente
Hay un viejo adagio.
La gente parece pensar que cuando TDD, simplemente te sientas, escribes pruebas, y el diseño simplemente ocurrirá mágicamente. Esto no es verdad Necesitas tener un plan de alto nivel. He descubierto que obtengo mis mejores resultados de TDD cuando diseño la interfaz (API pública) primero. Personalmente, creo un actual
interface
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 anotando un diagrama UML en papel cuadriculado. El punto es que debes tener un plan. TDD no es una licencia para ir pirateando un código.
Realmente siento que "Test First" es un nombre inapropiado. Diseño Primero luego prueba.
Por supuesto, siga los consejos que otros han dado sobre 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ácilmente probada e inyéctela.
fuente
¡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 descartar algunas pruebas correspondientes para esa "funcionalidad perdida" (también conocida como complejidad reducida).
Otros han dicho que su método privado será llamado como parte de sus otras pruebas de API, o será inalcanzable y, por lo tanto, borrable. De hecho, las cosas son más precisas si pensamos en las 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 da como resultado la división por cero. Si hacemos que el método sea privado, tenemos una opción: podemos considerar la ruta de división por cero, o podemos eliminar esa ruta considerando cómo se llama por los otros métodos.
De esta manera, podemos descartar algunas pruebas (por ejemplo, dividir por cero) y refactorizar las demás 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;)
fuente
Hay momentos en que un método privado podría convertirse en un método público de otra clase.
Por ejemplo, puede tener métodos privados que no sean seguros para subprocesos y dejar la clase en un estado temporal. Estos métodos se pueden mover a una clase separada que se mantiene en privado por su primera clase. Entonces, si su clase es una Cola, podría tener una clase InternalQueue que tenga métodos públicos, y la clase Queue mantenga la instancia InternalQueue de forma privada. 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 intenta implementar las funciones de Lista como métodos privados en la clase que las usa).
fuente
Me pregunto por qué su idioma solo tiene dos niveles de privacidad, totalmente público y completamente privado.
¿Puede organizar que sus métodos no públicos sean accesibles para paquetes o algo así? Luego, coloque sus pruebas en el mismo paquete y disfrute probando el funcionamiento interno que no forma parte de la interfaz pública. Su sistema de compilación excluirá las pruebas al compilar una versión binaria.
Por supuesto, a veces necesita tener métodos verdaderamente privados, no accesibles para nada más que la clase definitoria. Espero que todos estos métodos sean muy pequeños. En general, mantener los métodos pequeños (por ejemplo, menos de 20 líneas) ayuda mucho: las pruebas, el mantenimiento y la simple comprensión del código se vuelven más fáciles.
fuente
De vez en cuando he topado con métodos privados para protegerlos para permitir pruebas más finas (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 que puede encontrar. Además, eso es algo que no querría considerar en absoluto al construir una API pública, más de "trampa" que uno puede usar en el software de uso interno en esas situaciones raras.
fuente
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 funcionalidades, no tiene que mantener todas estas pruebas , especialmente cuando esto se convierte en parte de otra cosa.
Por ejemplo, a menudo tengo:
entonces tengo
Sin embargo, si ahora agrego funciones de nivel superior que lo llaman, que tienen muchas pruebas, ahora podría reducir esas pruebas de bajo nivel para que sean:
El diablo está en los detalles y la capacidad de hacerlo dependerá de las circunstancias.
fuente
¿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 por el punto de vista, del mismo modo, la encapsulación y el desarrollo impulsado por pruebas solo están en conflicto porque creemos que lo están. Nos sentamos aquí como Galileo y el papa, lanzándonos insultos el uno al otro: tonto, ¿no ves que los métodos privados también necesitan pruebas; hereje, no rompa la encapsulación! Del mismo modo, cuando reconocemos que la verdad es más grandiosa de lo que pensamos, ¿podemos intentar algo como encapsular las pruebas de las interfaces privadas para que las pruebas de las interfaces públicas no rompan la encapsulación?
Intente esto: agregue dos métodos, uno que no tiene entrada pero que justs devuelve el número de pruebas privadas y otro que toma un número de prueba como parámetro y devuelve pasar / fallar.
fuente