TDD: burlarse de objetos estrechamente acoplados

10

A veces los objetos solo necesitan estar estrechamente acoplados. Por ejemplo, una CsvFileclase probablemente necesitará trabajar estrechamente con la CsvRecordclase (o ICsvRecordinterfaz).

Sin embargo, de lo que aprendí en el pasado, uno de los principios principales del desarrollo basado en pruebas es "Nunca pruebe más de una clase a la vez". Lo que significa que debe usar ICsvRecordsimulacros o trozos en lugar de instancias reales de CsvRecord.

Sin embargo, después de probar este enfoque, noté que burlarse de la CsvRecordclase puede ser un poco difícil. Lo que me lleva a una de dos conclusiones:

  1. ¡Es difícil escribir pruebas unitarias! ¡Eso es un olor a código! Refactor!
  2. Burlarse de cada dependencia es irracional.

Cuando reemplacé mis simulacros con CsvRecordinstancias reales , las cosas salieron mucho mejor. Cuando busqué los pensamientos de otras personas, me encontré con esta publicación de blog , que parece respaldar el n. ° 2 anterior. Para los objetos que están naturalmente estrechamente unidos, no debemos preocuparnos tanto por burlarse.

¿Estoy fuera de lugar? ¿Hay alguna desventaja en el supuesto # 2 anterior? ¿Debería estar pensando en refactorizar mi diseño?

Phil
fuente
1
Creo que es un error común pensar que la "unidad" en las "pruebas unitarias" debe ser necesariamente una clase. Creo que su ejemplo muestra un caso en el que puede ser mejor que esas dos clases formen una unidad. Pero no me malinterpreten, estoy totalmente de acuerdo con la respuesta de Robert Harvey.
Doc Brown

Respuestas:

11

Si realmente necesita coordinación entre esas dos clases, escriba una CsvCoordinatorclase que encapsule sus dos clases y pruébelo.

Sin embargo, disputo la noción que CsvRecordno se puede comprobar de forma independiente. CsvRecordes básicamente una clase DTO , ¿no es así? Es solo una colección de campos, con quizás un par de métodos auxiliares. Y CsvRecordse puede usar en otros contextos además CsvFile; puede tener una colección o conjunto de CsvRecords, por ejemplo.

Prueba CsvRecordprimero. Asegúrese de que pase todas sus pruebas. Luego, adelante y utilícelo CsvRecordcon su CsvFileclase durante el examen. Úselo como un trozo / prueba simulada; llénelo con datos de prueba relevantes, páselo CsvFiley escriba sus casos de prueba en contra de eso.

Robert Harvey
fuente
1
Sí, CsvRecord definitivamente se puede probar de forma independiente. El problema es que si algo se rompe en CsvRecord, hará que las pruebas de CsvData fallen. Pero no creo que sea un problema importante.
Phil
1
Creo que quieres que eso suceda. :)
Robert Harvey
1
@RobertHarvey: en teoría, podría convertirse en un problema si CsvRecord y CsvFile se están convirtiendo en clases bastante complejas, y si una prueba se rompe para CsvFile, ahora no sabe de inmediato si es un problema en CsvFile o CsvRecord. Pero supongo que es más un caso hipotético: si tuviera la tarea de programar tales clases para un programa del mundo real, lo haría exactamente como lo describe.
Doc Brown
2
@ Phil: Si se CsvRecordrompe, entonces obviamente CsvDatafalla; pero esto está bien, porque prueba CsvRecordprimero, y si eso falla, sus CsvFilepruebas no tienen sentido. Todavía puede distinguir entre errores en CsvRecordy en CsvFile.
tdammers
5

La razón para probar una clase a la vez es que no desea que las pruebas para una clase tengan dependencias en el comportamiento de una segunda clase. Eso significa que si su prueba para la Clase A ejerce alguna de las funciones de la Clase B, entonces debe burlarse de la Clase B para eliminar la dependencia de una funcionalidad particular dentro de la Clase B.

Una clase como CsvRecordme parece que es principalmente para almacenar datos, no es una clase con demasiada funcionalidad propia. Es decir, puede tener constructores, captadores, establecedores, pero no métodos con lógica sustancial real. Por supuesto, supongo que aquí, tal vez hayas escrito una clase llamada CsvRecordque hace numerosos cálculos complejos.

Pero si CsvRecordno tiene una lógica propia, no se gana nada burlándose de ella. Esto es realmente la vieja máxima: "no te burles de los objetos de valor" .

Entonces, al considerar si se burlará de una clase en particular (para una prueba de una clase diferente), debe tener en cuenta cuánta lógica tiene esa clase y cuánta lógica se ejecutará en el transcurso de su prueba.

Dawood ibn Kareem
fuente
+1. Cualquier prueba cuyo resultado depende de la exactitud del comportamiento de más de un objeto es una prueba de integración, no una prueba unitaria. Tienes que burlarte de uno de estos objetos para obtener una prueba de unidad real. Sin embargo, esto no se aplica a los objetos que no tienen un comportamiento real, solo con getters y setters, por ejemplo.
guillaume31
1

No. # 2 está bien. Las cosas pueden y deben estar estrechamente acopladas si sus conceptos están estrechamente acoplados. Esto debería ser raro y generalmente evitado, pero en el ejemplo que proporcionó tiene sentido.

Telastyn
fuente
0

Las clases "acopladas" dependen mutuamente entre sí. Este no debería ser el caso en lo que está describiendo: un CsvRecord realmente no debería preocuparse por el CsvFile que lo contiene, por lo que la dependencia solo va en un sentido. Eso está bien, y no es un acoplamiento apretado.

Después de todo, si una clase contiene el nombre de cadena variable, no diría que está estrechamente acoplado a la cadena, ¿verdad?

Entonces, la unidad prueba el CsvRecord para ver su comportamiento deseado.

Luego use un marco de imitación (Mockito es genial) para probar si su unidad está interactuando con los objetos de los que depende correctamente. El comportamiento que desea probar, realmente, es que el CsvFile maneja los CsvRcords de la manera esperada. El funcionamiento interno de CvsRecord no debería importar: es cómo CvsFile se comunica con él.

Finalmente, TDD no se trata solo de pruebas unitarias. Ciertamente, puede (y debe) comenzar con pruebas funcionales que analicen el comportamiento funcional de cómo funcionan sus componentes más grandes, es decir, su historia o escenario de usuario. Sus pruebas de unidades establecen las expectativas y verifican las piezas, las pruebas funcionales hacen lo mismo para el conjunto.

Matthew Flynn
fuente
1
-1, el acoplamiento estrecho no necesariamente significa dependencias cíclicas, eso es un error. En el ejemplo, CsvFile está estrechamente acoplado a CsvRecord(pero no al revés). El OP pregunta si es una buena idea probar CsvFileal desacoplarlo a CsvRecordtravés de un ICsvRecord, no viceversa.
Doc Brown
2
@DocBrown: si el acoplamiento es estrecho o no, es una cuestión de cuánto CsvFiledepende del funcionamiento interno de CsvRecord, es decir, la cantidad de suposiciones que tiene el archivo sobre el registro. Las interfaces ayudan a documentar y hacer cumplir tales supuestos (o más bien la ausencia de otros supuestos), pero la cantidad de acoplamiento sigue siendo la misma, excepto que con una interfaz, puede conectar una clase de registro diferente CsvFile. Presentar la interfaz solo para que pueda decir que tiene un acoplamiento reducido es una tontería.
tdammers
0

Realmente hay dos preguntas aquí. La primera es si existen situaciones en las que no es aconsejable burlarse de un objeto. Eso es indudablemente cierto, como lo demuestran las otras excelentes respuestas. La segunda pregunta es si su caso particular es una de esas situaciones. Sobre esa cuestión no estoy convencido.

Probablemente la razón más común para no burlarse de una clase es si es una clase de valor. Sin embargo, debes mirar la razón detrás de la regla. No es porque la clase burlada sea mala de alguna manera, es porque será esencialmente idéntica a la original. Si ese fuera el caso, la prueba de su unidad no sería más fácil usando la clase original.

Es muy posible que su código sea una de las raras excepciones en las que la refactorización no ayudaría, pero solo debe declararlo después de que los esfuerzos diligentes de refactorización no hayan funcionado. Incluso los desarrolladores experimentados pueden tener problemas para ver alternativas a su propio diseño. Si no puede pensar en alguna forma posible de mejorarlo, pídale a alguien experimentado que lo revise por segunda vez.

La mayoría de la gente parece estar asumiendo que su CsvRecordes una clase de valor. Intenta hacerlo uno. Hazlo inmutable si puedes. Si tiene dos objetos con punteros entre sí, elimine uno de ellos y descubra cómo hacerlo funcionar. Busque lugares para dividir clases y funciones. Es posible que el mejor lugar para dividir una clase no siempre coincida con el diseño físico del archivo. Intente invertir la relación padre / hijo de las clases. Tal vez necesite una clase separada para leer y escribir archivos csv. Quizás necesite clases separadas para manejar el archivo de E / S y la interfaz para las capas superiores. Hay muchas cosas que probar antes de declarar que no es refactible.

Karl Bielefeldt
fuente