Dificultades con TDD y refactorización (o ¿por qué es esto más doloroso de lo que debería ser?)

20

Quería aprender a usar el enfoque TDD y tenía un proyecto en el que quería trabajar durante un tiempo. No era un proyecto grande, así que pensé que sería un buen candidato para TDD. Sin embargo, siento que algo salió mal. Déjame dar un ejemplo:

En un nivel superior, mi proyecto es un complemento para Microsoft OneNote que me permitirá rastrear y administrar proyectos más fácilmente. Ahora, también quería mantener la lógica de negocios para esto tan desacoplada de OneNote como sea posible en caso de que decidiera construir mi propio almacenamiento personalizado y algún día.

Primero comencé con una prueba básica de aceptación de palabras simples para describir lo que quería que hiciera mi primera función. Se ve más o menos así (para simplificarlo):

  1. Clics del usuario crear proyecto
  2. Tipos de usuario en el título del proyecto
  3. Verifique que el proyecto se haya creado correctamente

Saltando sobre las cosas de la interfaz de usuario y algunos planes intermedios, llego a mi primera prueba de unidad:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

Hasta aquí todo bien. Rojo, verde, refactor, etc. Bien, ahora realmente necesita guardar cosas. Cortando algunos pasos aquí termino con esto.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

Todavía me siento bien en este punto. Todavía no tengo un almacén de datos concreto, pero creé la interfaz como esperaba.

Voy a omitir algunos pasos aquí porque esta publicación es lo suficientemente larga, pero seguí procesos similares y finalmente llego a esta prueba para mi almacén de datos:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

Esto fue bueno hasta que intenté implementarlo:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

Y ahí está el problema justo donde está el "...". Ahora me doy cuenta en ESTE punto que CreatePage requiere una ID de sección. No me di cuenta de esto cuando estaba pensando en el nivel del controlador porque solo me preocupaba probar los bits relevantes para el controlador. Sin embargo, hasta aquí ahora me doy cuenta de que tengo que pedirle al usuario una ubicación para almacenar el proyecto. Ahora tengo que agregar un ID de ubicación al almacén de datos, luego agregar uno al proyecto, luego agregar uno al controlador y agregarlo a TODAS las pruebas que ya están escritas para todas esas cosas. Se ha vuelto tedioso muy rápidamente y no puedo evitar sentir que habría captado esto más rápido si esbozara el diseño con anticipación en lugar de dejar que se diseñara durante el proceso TDD.

¿Puede alguien explicarme si he hecho algo mal en este proceso? ¿Hay alguna forma de evitar este tipo de refactorización? ¿O es esto común? Si es común, ¿hay alguna forma de hacerlo más indoloro?

¡Gracias a todos!

Aterrizar
fuente
Recibiría algunos comentarios muy perspicaces si publicara este tema en este foro de discusión: groups.google.com/forum/#!forum/… que es específicamente para temas de TDD.
Chuck Krutsinger el
1
Si necesita agregar algo a todas sus pruebas, parece que sus pruebas están mal escritas. Debe refactorizar sus pruebas y considerar el uso de un accesorio sensible.
Dave Hillier

Respuestas:

19

Si bien TDD se promociona (correctamente) como una forma de diseñar y hacer crecer su software, sigue siendo una buena idea pensar de antemano en el diseño y la arquitectura. En mi opinión, "esbozar el diseño con anticipación" es un juego justo. Sin embargo, a menudo esto será a un nivel más alto que las decisiones de diseño a las que se lo llevará a través de TDD.

También es cierto que cuando las cosas cambian, generalmente tendrá que actualizar las pruebas. No hay forma de eliminar esto por completo, pero hay algunas cosas que puede hacer para que sus pruebas sean menos frágiles y minimizar el dolor.

  1. En la medida de lo posible, mantenga los detalles de implementación fuera de sus pruebas. Esto significa probar solo a través de métodos públicos y, cuando sea posible, favorecer la verificación basada en el estado sobre la interacción . En otras palabras, si prueba el resultado de algo en lugar de los pasos para llegar allí, sus pruebas deberían ser menos frágiles.

  2. Minimice la duplicación en su código de prueba, tal como lo haría en el código de producción. Esta publicación es una buena referencia. En su ejemplo, parece que fue doloroso agregar la IDpropiedad a su constructor porque invocó al constructor directamente en varias pruebas diferentes. En su lugar, intente extraer la creación del objeto a un método o inicializarlo una vez para cada prueba en un método de inicialización de prueba.

jhewlett
fuente
He leído los méritos de los basados ​​en el estado y los basados ​​en la interacción y lo entiendo la mayor parte del tiempo. Sin embargo, no veo cómo es posible en todos los casos sin exponer las propiedades EXPLÍCITAMENTE para la prueba. Toma mi ejemplo de arriba. No estoy seguro de cómo verificar que en realidad se llamó al almacén de datos sin usar una aserción para "MustHaveBeenCalled". En cuanto al punto 2, tienes toda la razón. Terminé haciendo eso después de todas las ediciones, pero solo quería asegurarme de que mi enfoque fuera generalmente consistente con las prácticas TDD aceptadas. ¡Gracias!
Landon
@Landon Hay casos en los que las pruebas de interacción son más apropiadas. Por ejemplo, verificar que se realizó una llamada a una base de datos o servicio web. Básicamente, siempre que necesite aislar su prueba, especialmente de un servicio externo.
jhewlett
@Landon Soy un "clasicista convencido", así que no tengo mucha experiencia con las pruebas basadas en la interacción ... Pero no es necesario hacer una afirmación para "MustHaveBeenCalled". Si está probando una inserción, puede usar una consulta para ver si se insertó. PD: utilizo stubs debido a consideraciones de rendimiento al probar todo menos la capa de base de datos.
Hbas
@jhewlett Esa es la conclusión a la que he llegado también. ¡Gracias!
Landon
@Hbas No hay base de datos para consultar. Estoy de acuerdo en que sería el camino más sencillo si tuviera uno, pero estoy agregando esto a un cuaderno de OneNote. Lo mejor que puedo hacer es agregar un método Get a mi clase de interoperabilidad para intentar tirar de la página. PODRÍA escribir la prueba para hacer eso, pero sentí que estaría probando dos cosas a la vez: ¿guardé esto? y ¿Mi clase auxiliar recupera páginas correctamente? Aunque, supongo que en algún momento sus pruebas pueden depender de otro código que se haya probado en otro lugar. ¡Gracias!
Landon
10

... No puedo evitar sentir que habría captado esto más rápido si esbozara el diseño con anticipación en lugar de dejar que se diseñara durante el proceso TDD ...

Tal vez tal vez no

Por un lado, TDD funcionó bien, brindándole pruebas automatizadas a medida que desarrolló la funcionalidad e inmediatamente interrumpiendo cuando tuvo que cambiar la interfaz.

Por otro lado, quizás si hubiera comenzado con la función de alto nivel (SaveProject) en lugar de una función de nivel inferior (CreateProject), habría notado la falta de parámetros antes.

Por otra parte, tal vez no lo hubieras hecho. Es un experimento irrepetible.

Pero si está buscando una lección para la próxima vez: comience desde arriba. Y piense en el diseño tanto como quiera primero.

Steven A. Lowe
fuente
0

https://frontendmasters.com/courses/angularjs-and-code-testability/ Desde aproximadamente las 2:22:00 hasta el final (aproximadamente 1 hora). Lamento que el video no sea gratuito, pero no he encontrado uno gratuito que lo explique tan bien.

Una de las mejores presentaciones de escribir código comprobable es en esta lección. Es una clase AngularJS, pero la parte de la prueba está en torno al código java, principalmente porque lo que está hablando no tiene nada que ver con el lenguaje, y todo lo que tiene que ver con escribir un buen código comprobable en primer lugar.

La magia está en escribir código comprobable, en lugar de escribir pruebas de código. No se trata de escribir código que pretende ser un usuario.

También pasa algún tiempo escribiendo la especificación en forma de afirmaciones de prueba.

codificador de barcos
fuente