¿Qué tan granulares deben ser las pruebas de TDD?

18

Durante la capacitación TDD basada en un caso de software médico, estamos implementando la siguiente historia: "Cuando el usuario presiona el botón Guardar, el sistema debe agregar pacientes, agregar dispositivos y agregar registros de datos del dispositivo".

La implementación final se verá más o menos así:

if (_importDialog.Show() == ImportDialogResult.SaveButtonIsPressed)
{
   AddPatient();
   AddDevice();
   AddDeviceDataRecords();
}

Tenemos dos formas de implementarlo:

  1. Se llamaron tres pruebas donde cada una verifica un método (AddPatient, AddDevice, AddDeviceDataRecords)
  2. Una prueba que verifica que los tres métodos fueron llamados

En el primer caso, si sucede algo incorrecto a la condición de la cláusula if, las tres pruebas fallarán. Pero en el segundo caso, si la prueba falla, no estamos seguros de qué está exactamente mal. ¿De qué manera preferirías?

SiberianGuy
fuente

Respuestas:

8

Pero en el segundo caso, si la prueba falla, no estamos seguros de qué está exactamente mal.

Creo que eso dependerá en gran medida de cuán buenos mensajes de error produzca la prueba. En general, hay diferentes formas de verificar que se haya llamado a un método; por ejemplo, si usa un objeto simulado, le dará un mensaje de error preciso que describe qué método esperado no se llamó durante la prueba. Si verifica que se llamó al método mediante la detección de los efectos de la llamada, depende de usted generar un mensaje de error descriptivo.

En la práctica, la elección entre las opciones 1 y 2 también depende de la situación. Si veo el código que muestra arriba en un proyecto heredado, elijo el enfoque pragmático del Caso # 2 solo para verificar que cada uno de los 3 métodos se invoque correctamente cuando se cumpla la condición. Si estoy desarrollando este fragmento de código en este momento, las 3 llamadas al método probablemente se agregarían una por una, en puntos separados en el tiempo (posiblemente días o meses separados), por lo que agregaría una nueva prueba unitaria separada para verificar cada llamada.

Tenga en cuenta también que, de cualquier manera, también debe tener pruebas unitarias separadas para verificar que cada uno de los métodos individuales haga lo que se supone que debe hacer.

Péter Török
fuente
¿No le parece razonable combinar esas tres pruebas en una?
SiberianGuy
@Idsa, puede ser una decisión razonable, aunque en la práctica rara vez me molesto con este tipo de refactorización. Por otra parte, estoy trabajando con código heredado, donde las prioridades son diferentes: nos enfocamos en aumentar la cobertura de prueba del código existente y mantener la creciente cantidad de pruebas unitarias mantenibles.
Péter Török
30

La Granularidad en su ejemplo parece ser la diferencia entre las pruebas unitarias y de aceptación.

Una prueba de unidad prueba una sola unidad de funcionalidad, con la menor cantidad de dependencias posible. En su caso, podría haber 4 pruebas unitarias

  • ¿AddPatient agrega un paciente (es decir, llama a las funciones relevantes de la base de datos)?
  • ¿AddDevice agrega un dispositivo?
  • ¿AddDeviceDataRecords agrega los registros?
  • la función principal de una enmend en su ejemplo, llama AddPatient, AddDevice y AddDeviceFunctions

Las pruebas unitarias son para los desarrolladores , por lo que obtienen la confianza de que su código es técnicamente correcto

Las pruebas de aceptación deben probar la funcionalidad combinada, desde la perspectiva del usuario. Deben modelarse a lo largo de las historias de los usuarios y tener el mayor nivel posible. Por lo tanto, no tiene que verificar si se llaman funciones, pero si se logra un beneficio visible para el usuario :

cuando el usuario ingresa los datos, hace clic en Aceptar y ...

  • ... va a la lista de pacientes, debería ver un nuevo paciente con el nombre
  • ... va a la lista de dispositivos, debería ver un nuevo dispositivo
  • ... va a los detalles del nuevo dispositivo, debería ver nuevos registros de datos

Las pruebas de aceptación son para los clientes o para construir una mejor comunicación con ellos.

Para responder a su pregunta "¿qué preferiría?": Cuál es un problema mayor para usted en este momento, errores y regresión (=> más pruebas unitarias) o comprender y formalizar el panorama general (=> más pruebas de aceptación)

keppla
fuente
13

Tenemos dos formas de implementarlo:

Eso es falso

Se llamaron tres pruebas donde cada una verifica un método (AddPatient, AddDevice, AddDeviceDataRecords)

Usted debe hacer para estar seguro de que funciona esto.

Una prueba que verifica que los tres métodos fueron llamados

Usted debe también hacer para estar seguro de las obras de la API de esto.

La clase, como unidad, debe ser completamente probada. Cada método

Puede comenzar con una prueba que cubra los tres métodos, pero no le dice mucho.

Si la prueba falla, no estamos seguros de qué es exactamente lo que está mal.

Correcto. Por eso pruebas todos los métodos.

Usted debe probar la interfaz pública. Dado que esta clase hace tres cosas más una (incluso si están agrupadas en un método debido a la historia del usuario), debe probar las cuatro cosas. Tres de bajo nivel y un paquete.

S.Lott
fuente
2

Escribimos nuestras pruebas unitarias para oraciones significativas de funcionalidad que muchas veces se asignan a un método (si ha escrito bien su código), pero a veces se hacen más grandes, abarcando muchos métodos.

Por ejemplo, imagine que agregar un paciente a su sistema necesita que se llamen algunas subrutinas (funciones secundarias):

  1. VerifyPatientQualification
  2. CalculateDoctorExistence
  3. CheckInsuranceHistory
  4. Asegúrese de vacío

También podríamos escribir pruebas unitarias para cada una de estas funciones.

Saeed Neamati
fuente
2

Una regla general simple que he estado siguiendo es nombrar la prueba para que describa exactamente lo que hace la prueba. Si el nombre de la prueba se vuelve demasiado complejo, es una señal de que tal vez la prueba está haciendo demasiado. Entonces, por ejemplo, nombrar una prueba para hacer lo que propone en la opción 2 puede parecer PatientIsAddedDeviceIsAddedAndDeviceDataRecordsWhenSaved, que es mucho más complejo que tres pruebas separadas PatientIsAddedWhenSaved, DeviceIsAddedWhenSaved, DataRecordsWhenSaved. También creo que las lecciones que se pueden aprender de BDD son bastante interesantes, donde cada prueba es realmente representativa de un único requisito que podría describirse en un lenguaje natural.

jpierson
fuente