¿Dónde está la línea entre la lógica de aplicación de prueba de unidad y las construcciones de lenguaje de desconfianza?

87

Considere una función como esta:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Se podría usar así:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Vamos a suponer que Storetiene sus propias pruebas unitarias, o es proporcionado por el proveedor. En cualquier caso, confiamos Store. Y supongamos además que el manejo de errores, por ejemplo, los errores de desconexión de la base de datos, no es responsabilidad de savePeople. De hecho, supongamos que la tienda en sí es una base de datos mágica que no puede tener errores de ninguna manera. Dados estos supuestos, la pregunta es:

¿Debería savePeople()probarse la unidad, o tales pruebas equivaldrían a probar la construcción del forEachlenguaje incorporado ?

Podríamos, por supuesto, pasar una burla dataStorey afirmar que dataStore.savePerson()se llama una vez por cada persona. Ciertamente, podría argumentar que dicha prueba proporciona seguridad contra los cambios de implementación: por ejemplo, si decidimos reemplazarlo forEachcon un forbucle tradicional o algún otro método de iteración. Entonces la prueba no es del todo trivial. Y sin embargo, parece terriblemente cerca ...


Aquí hay otro ejemplo que puede ser más fructífero. Considere una función que no hace más que coordinar otros objetos o funciones. Por ejemplo:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

¿Cómo debería una función como esta ser probada por unidad, suponiendo que piense que debería? Es difícil para mí imaginar cualquier tipo de prueba de unidad que no se limita a burlan dough, pany oven, a continuación, afirman que los métodos se llaman en ellos. Pero tal prueba no hace más que duplicar la implementación exacta de la función.

¿Esta incapacidad para probar la función de manera significativa en una caja negra indica un defecto de diseño con la función misma? Si es así, ¿cómo podría mejorarse?


Para dar aún más claridad a la pregunta que motiva el bakeCookiesejemplo, agregaré un escenario más realista, que es el que he encontrado al intentar agregar pruebas y refactorizar el código heredado.

Cuando un usuario crea una nueva cuenta, deben suceder varias cosas: 1) se debe crear un nuevo registro de usuario en la base de datos 2) se debe enviar un correo electrónico de bienvenida 3) se debe registrar la dirección IP del usuario por fraude propósitos

Por lo tanto, queremos crear un método que vincule todos los pasos del "nuevo usuario":

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.insertUserRecord(validateduserData);
  emailService.sendWelcomeEmail(validatedUserData);
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Tenga en cuenta que si alguno de estos métodos arroja un error, queremos que el error aparezca en el código de llamada, para que pueda manejar el error como mejor le parezca. Si el código API lo invoca, puede traducir el error en un código de respuesta http apropiado. Si está siendo llamado por una interfaz web, puede traducir el error en un mensaje apropiado que se mostrará al usuario, y así sucesivamente. El punto es que esta función no sabe cómo manejar los errores que se pueden generar.

La esencia de mi confusión es que para probar una función de este tipo, parece necesario repetir la implementación exacta en la prueba misma (especificando que los métodos se invocan en simulacros en un cierto orden) y eso parece incorrecto.

Jonás
fuente
44
Después de que se ejecute. ¿Tienes galletas
Ewan
66
con respecto a su actualización: ¿por qué querría burlarse de una sartén? o masa? Suenan como simples objetos en memoria que deberían ser triviales de crear, por lo que no hay razón para que no los pruebes como una sola unidad. recuerde, la "unidad" en "prueba de unidad" no significa "una sola clase". significa "la unidad de código más pequeña posible que se utiliza para hacer algo". una sartén probablemente no sea más que un contenedor para objetos de masa, por lo que sería ideado probarlo de forma aislada en lugar de simplemente probar el método de hornear galletas desde afuera hacia adentro.
Sára
11
Al final del día, el principio fundamental en el trabajo aquí es que escribes suficientes pruebas para asegurarte de que el código funciona y que es un "canario adecuado en la mina de carbón" cuando alguien cambia algo. Eso es. No hay encantamientos mágicos, suposiciones formuladas o afirmaciones dogmáticas, por lo que la cobertura del código del 85% al ​​90% (no el 100%) se considera ampliamente excelente.
Robert Harvey
55
@RobertHarvey, desafortunadamente, tópicos formulados y fragmentos de sonido TDD, aunque seguro que le otorgarán un entusiasmo entusiasta, no ayudan a resolver problemas del mundo real. para eso necesitas ensuciarte las manos y arriesgarte a responder una pregunta real
Jonás
44
Prueba unitaria en orden decreciente de complejidad ciclomática. Confía en mí, te quedarás sin tiempo antes de llegar a esta función
Neil McGuigan

Respuestas:

118

¿Se savePeople()debe probar la unidad? Si. No está probando que dataStore.savePersonfuncione, o que la conexión db funcione, o incluso que foreachfuncione. Está realizando pruebas que savePeoplecumplen la promesa que hace a través de su contrato.

Imagine este escenario: alguien realiza una gran refactorización de la base del código y elimina accidentalmente la forEachparte de la implementación para que siempre guarde solo el primer elemento. ¿No le gustaría una prueba de unidad para atrapar eso?

Bryan Oakley
fuente
20
@RobertHarvey: Hay mucha área gris, y hacer la distinción, OMI, no es importante. Sin embargo, tiene razón: no es realmente importante probar que "llama a las funciones correctas", sino "hace lo correcto", independientemente de cómo lo haga. Lo importante es probar eso, dado un conjunto específico de entradas para la función, obtienes un conjunto específico de salidas. Sin embargo, puedo ver cómo esa última oración puede ser confusa, así que la eliminé.
Bryan Oakley
64
"Estás probando que savePeople cumple la promesa que hace a través de su contrato". Esta. Tanto esto
Lovis
2
A menos que tenga una prueba de sistema de "extremo a extremo" que lo cubra.
Ian
66
@Ian Las pruebas de extremo a extremo no reemplazan las pruebas unitarias, son complementarias. El hecho de que pueda tener una prueba de extremo a extremo que le garantiza guardar una lista de personas no significa que no deba hacerse una prueba unitaria para cubrirla también.
Vincent Savard
44
@VincentSavard, pero el costo / beneficio de una prueba unitaria se reduce si el riesgo es el control de otra manera.
Ian
36

Por lo general, este tipo de pregunta surge cuando las personas realizan el desarrollo "prueba después". Aborde este problema desde el punto de vista de TDD, donde las pruebas vienen antes de la implementación, y hágase esta pregunta nuevamente como ejercicio.

Al menos en mi aplicación de TDD, que generalmente es de afuera hacia adentro, no estaría implementando una función como savePeopledespués de haberlo implementado savePerson. Las funciones savePeopley savePersoncomenzarían como una y serían controladas por las mismas pruebas unitarias; la separación entre los dos surgiría después de algunas pruebas, en el paso de refactorización. Este modo de trabajo también plantearía la cuestión de dónde savePeopledebería estar la función , si es una función libre o parte de ella dataStore.

Al final, las pruebas no solo verificarían si puede guardar correctamente un archivo Personen Store, sino también a muchas personas. Esto también me llevaría a preguntarme si son necesarias otras verificaciones, por ejemplo, "¿Necesito asegurarme de que la savePeoplefunción sea atómica, ya sea guardando todo o nada?", "¿Puede de alguna manera devolver errores a las personas que no pudieron t ser guardado? ¿Cómo se verían esos errores? ", y así sucesivamente. Todo esto equivale a mucho más que simplemente verificar el uso de una forEachu otras formas de iteración.

Sin embargo, si el requisito de salvar a más de una persona a la vez se produjo solo después de que savePersonya se entregó, entonces actualizaría las pruebas existentes savePersonpara ejecutar la nueva función savePeople, asegurándome de que aún pueda salvar a una persona simplemente delegando al principio, luego pruebe el comportamiento de más de una persona a través de nuevas pruebas, pensando si sería necesario hacer que el comportamiento sea atómico o no.

MichelHenrich
fuente
44
Básicamente prueba la interfaz, no la implementación.
Snoop
8
Puntos justos y perspicaces. Sin embargo, siento que de alguna manera mi verdadera pregunta está siendo esquivada :) Su respuesta es: "En el mundo real, en un sistema bien diseñado, no creo que esta versión simplificada de su problema exista". Nuevamente, es justo, pero creé específicamente esta versión simplificada para resaltar la esencia de un problema más general. Si no puede superar la naturaleza artificial del ejemplo, tal vez podría imaginar otro ejemplo en el que tuviera una buena razón para una función similar, que solo hiciera iteración y delegación. ¿O tal vez piensas que es simplemente imposible?
Jonás
@Jonah actualizado. Espero que responda tu pregunta un poco mejor. Todo esto está muy basado en la opinión y puede estar en contra del objetivo de este sitio, pero sin duda es una discusión muy interesante. Por cierto, traté de responder desde el punto de vista del trabajo profesional, donde debemos esforzarnos por dejar las pruebas unitarias para todo el comportamiento de la aplicación, independientemente de cuán trivial sea la implementación, porque tenemos el deber de construir una prueba bien probada y sistema documentado para nuevos mantenedores si nos vamos. Para proyectos personales o, por ejemplo, no críticos (el dinero también es crítico), tengo una opinión muy diferente.
MichelHenrich
Gracias por la actualización. ¿Cómo exactamente lo probarías savePeople? Como describí en el último párrafo de OP o de alguna otra manera?
Jonás
1
Lo siento, no me puse en claro con la parte de "no hay burlas involucradas". Quise decir que no usaría un simulacro para la savePersonfunción como usted sugirió, sino que lo probaría de manera más general savePeople. Las pruebas unitarias Storese cambiarían para ejecutarse en savePeoplelugar de llamar directamente savePerson, por lo que para esto no se utilizan simulacros. Pero la base de datos, por supuesto, no debería estar presente, ya que nos gustaría aislar los problemas de codificación de los diversos problemas de integración que ocurren con las bases de datos reales, por lo que aquí todavía tenemos una simulación.
MichelHenrich
21

Debería savePeople () ser probado en la unidad

Si, deberia. Pero trate de escribir sus condiciones de prueba de manera independiente de la implementación. Por ejemplo, convirtiendo su ejemplo de uso en una prueba unitaria:

function testSavePeople() {
    myDataStore = new Store('some connection string', 'password');
    myPeople = ['Joe', 'Maggie', 'John'];
    savePeople(myDataStore, myPeople);
    assert(myDataStore.containsPerson('Joe'));
    assert(myDataStore.containsPerson('Maggie'));
    assert(myDataStore.containsPerson('John'));
}

Esta prueba hace varias cosas:

  • verifica el contrato de la función savePeople()
  • no le importa la implementación de savePeople()
  • documenta el uso de ejemplo de savePeople()

Tenga en cuenta que aún puede simular / resguardar / falsificar el almacén de datos. En este caso, no buscaría llamadas explícitas a funciones, sino el resultado de la operación. De esta manera, mi prueba está preparada para futuros cambios / refactores.

Por ejemplo, la implementación de su almacén de datos podría proporcionar un saveBulkPerson()método en el futuro; ahora, un cambio en la implementación de savePeople()usar saveBulkPerson()no interrumpiría la prueba unitaria mientras saveBulkPerson()funcione como se esperaba. Y si de saveBulkPerson()alguna manera no funciona como se esperaba, su prueba de unidad lo captará.

¿o tales pruebas equivaldrían a probar el constructo de lenguaje incorporado para cada lenguaje?

Como se dijo, intente probar los resultados esperados y la interfaz de la función, no para la implementación (a menos que esté haciendo pruebas de integración, entonces podría ser útil detectar llamadas a funciones específicas). Si hay varias formas de implementar una función, todas deberían funcionar con la prueba de su unidad.

Con respecto a su actualización de la pregunta:

Prueba de cambios de estado! Por ejemplo, se utilizará parte de la masa. De acuerdo con su implementación, afirme que la cantidad de utilizado doughencaja pano afirme que se doughestá agotando. Afirma que pancontiene cookies después de la llamada a la función. Afirma que ovenestá vacío / en el mismo estado que antes.

Para pruebas adicionales, verifique los casos límite: ¿Qué sucede si ovenno está vacío antes de la llamada? ¿Qué pasa si no hay suficiente dough? Si el panya está lleno?

Debería poder deducir todos los datos requeridos para estas pruebas de los propios objetos de masa, sartén y horno. No es necesario capturar las llamadas a funciones. ¡Trate la función como si su implementación no estuviera disponible para usted!

De hecho, la mayoría de los usuarios de TDD escriben sus pruebas antes de escribir la función para que no dependan de la implementación real.


Para su última incorporación:

Cuando un usuario crea una nueva cuenta, deben suceder varias cosas: 1) se debe crear un nuevo registro de usuario en la base de datos 2) se debe enviar un correo electrónico de bienvenida 3) se debe registrar la dirección IP del usuario por fraude propósitos

Por lo tanto, queremos crear un método que vincule todos los pasos del "nuevo usuario":

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Para una función como esta, me burlaría / stub / fake (lo que parezca más general) los parámetros dataStorey emailService. Esta función no realiza ninguna transición de estado en ningún parámetro por sí sola, los delega a los métodos de algunos de ellos. Intentaría verificar que la llamada a la función hizo 4 cosas:

  • insertó un usuario en el almacén de datos
  • envió (o al menos llamó al método correspondiente) un correo electrónico de bienvenida
  • grabó la IP de los usuarios en el almacén de datos
  • delegó cualquier excepción / error que encontró (si lo hubiera)

Los primeros 3 controles se pueden hacer con simulacros, talones o falsificaciones de dataStorey emailService(realmente no desea enviar correos electrónicos durante la prueba). Como tuve que buscar esto para algunos de los comentarios, estas son las diferencias:

  • Un falso es un objeto que se comporta igual que el original y es, hasta cierto punto, indistinguible. Su código normalmente se puede reutilizar en las pruebas. Esto puede ser, por ejemplo, una simple base de datos en memoria para un contenedor de base de datos.
  • Un trozo simplemente implementa todo lo necesario para cumplir con las operaciones requeridas de esta prueba. En la mayoría de los casos, un trozo es específico para una prueba o un grupo de pruebas que requieren solo un pequeño conjunto de los métodos del original. En este ejemplo, podría ser un dataStoreque simplemente implementa una versión adecuada de insertUserRecord()y recordIpAddress().
  • Un simulacro es un objeto que le permite verificar cómo se usa (la mayoría de las veces le permite evaluar las llamadas a sus métodos). Intentaría usarlos con moderación en las pruebas unitarias, ya que al usarlos, realmente intenta probar la implementación de la función y no la adherencia a su interfaz, pero todavía tienen sus usos. Existen muchos marcos simulados para ayudarlo a crear el simulacro que necesita.

Tenga en cuenta que si alguno de estos métodos arroja un error, queremos que el error aparezca en el código de llamada, para que pueda manejar el error como mejor le parezca. Si el código API lo invoca, puede traducir el error en un código de respuesta HTTP apropiado. Si está siendo llamado por una interfaz web, puede traducir el error en un mensaje apropiado que se mostrará al usuario, y así sucesivamente. El punto es que esta función no sabe cómo manejar los errores que se pueden generar.

Las excepciones / errores esperados son casos de prueba válidos: Usted confirma que, en caso de que ocurra tal evento, la función se comporta de la manera que espera. Esto se puede lograr dejando que el objeto simulado / falso / trozo correspondiente se lance cuando se desee.

La esencia de mi confusión es que para probar una función de este tipo, parece necesario repetir la implementación exacta en la prueba misma (especificando que los métodos se invocan en simulacros en un cierto orden) y eso parece incorrecto.

A veces esto tiene que hacerse (aunque en general te preocupas por esto en las pruebas de integración). Más a menudo, hay otras formas de verificar los efectos secundarios / cambios de estado esperados.

La verificación de las llamadas a funciones exactas hace que las pruebas unitarias sean bastante frágiles: solo pequeños cambios en la función original hacen que fallen. Esto puede desearse o no, pero requiere un cambio en la (s) prueba (s) de unidad correspondiente (s) cada vez que cambie una función (ya sea refactorización, optimización, corrección de errores, ...).

Lamentablemente, en ese caso, la prueba unitaria pierde parte de su credibilidad: desde que se modificó, no confirma la función después de que el cambio se comporta de la misma manera que antes.

Por ejemplo, considere a alguien agregando una llamada a oven.preheat()(¡optimización!) En su ejemplo de horneado de galletas:

  • Si se burló del objeto del horno, no esperará esa llamada y no pasará la prueba, aunque el comportamiento observable del método no cambió (todavía tiene una bandeja de cookies, con suerte).
  • Un código auxiliar puede o no fallar, dependiendo de si solo agregó los métodos que se probarán o la interfaz completa con algunos métodos ficticios.
  • Un falso no debe fallar, ya que debe implementar el método (de acuerdo con la interfaz)

En mis pruebas unitarias, trato de ser lo más general posible: si la implementación cambia, pero el comportamiento visible (desde la perspectiva de la persona que llama) sigue siendo el mismo, mis pruebas deberían pasar. Idealmente, el único caso que necesito para cambiar una prueba de unidad existente debería ser una corrección de errores (de la prueba, no la función bajo prueba).

hoffmale
fuente
1
El problema es que tan pronto como escribe myDataStore.containsPerson('Joe'), asume la existencia de una base de datos de prueba funcional. Una vez que hace eso, está escribiendo una prueba de integración y no una prueba unitaria.
Jonás
Supongo que puedo confiar en tener un almacén de datos de prueba (no me importa si es real o falso) y que todo funciona como está configurado (ya que ya debería tener pruebas unitarias para esos casos). Lo único que la prueba quiere probar es que savePeople()realmente agrega a esas personas a cualquier almacén de datos que proporcione siempre que ese almacén de datos implemente la interfaz esperada. Una prueba de integración sería, por ejemplo, verificar que mi contenedor de base de datos realmente haga las llamadas correctas a la base de datos para una llamada al método.
hoffmale
Para aclarar, si está utilizando un simulacro, todo lo que puede hacer es afirmar que se llamó a un método en ese simulacro , tal vez con algún parámetro específico. No puedes afirmar el estado del simulacro después. Entonces, si desea hacer afirmaciones sobre el estado de la base de datos después de llamar al método bajo prueba, como en myDataStore.containsPerson('Joe'), debe usar un db funcional de algún tipo. Una vez que das ese paso, ya no es una prueba unitaria.
Jonás
1
no tiene que ser una base de datos real, solo un objeto que implemente la misma interfaz que el almacén de datos real (léase: pasa las pruebas unitarias relevantes para la interfaz del almacén de datos). Todavía lo consideraría una burla. Deje que el simulacro almacene todo lo que se agrega por cualquier método para hacerlo en una matriz y verifique si los datos de prueba (elementos de myPeople) están en la matriz. En mi humilde opinión, un simulacro aún debe tener el mismo comportamiento observable que se espera de un objeto real, de lo contrario está probando el cumplimiento de la interfaz simulada, no la interfaz real.
hoffmale
"Deje que el simulacro almacene todo lo que se agrega por cualquier método para hacerlo en una matriz y verifique si los datos de prueba (elementos de myPeople) están en la matriz", sigue siendo una base de datos "real", solo un ad-hoc, en memoria uno que hayas construido. "En mi humilde opinión, un simulacro aún debe tener el mismo comportamiento observable que se espera de un objeto real". Supongo que puede abogar por eso, pero eso no es lo que significa "simulacro" en la literatura de prueba o en cualquiera de las bibliotecas populares. Has visto. Un simulacro simplemente verifica que los métodos esperados se invocan con los parámetros esperados.
Jonás
13

El valor principal que proporciona esta prueba es que hace que su implementación sea refactible.

Solía ​​hacer muchas optimizaciones de rendimiento en mi carrera y a menudo encontraba problemas con el patrón exacto que demostró: para guardar N entidades en la base de datos, realice N inserciones. Por lo general, es más eficiente hacer una inserción masiva con una sola declaración.

Por otro lado, tampoco queremos optimizar prematuramente. Si por lo general solo guarda de 1 a 3 personas a la vez, escribir un lote optimizado puede ser excesivo.

Con una prueba de unidad adecuada, puede escribirla de la manera en que la implementó anteriormente, y si considera que necesita optimizarla, puede hacerlo con la red de seguridad de una prueba automatizada para detectar cualquier error. Naturalmente, esto varía en función de la calidad de las pruebas, así que pruebe libremente y pruebe bien.

La ventaja secundaria de las pruebas unitarias de este comportamiento es servir como documentación de cuál es su propósito. Este ejemplo trivial puede ser obvio, pero dado el siguiente punto a continuación, podría ser muy importante.

La tercera ventaja, que otros han señalado, es que puede probar detalles ocultos que son muy difíciles de probar con pruebas de integración o aceptación. Por ejemplo, si existe un requisito de que todos los usuarios se guarden atómicamente, entonces puede escribir un caso de prueba para eso, que le brinda una forma de saber que se comporta como se esperaba, y también sirve como documentación para un requisito que puede no ser obvio a nuevos desarrolladores.

Agregaré un pensamiento que recibí de un instructor de TDD. No pruebes el método. Prueba el comportamiento. En otras palabras, no prueba que savePeoplefunciona, está probando que se pueden guardar múltiples usuarios en una sola llamada.

Descubrí que mi capacidad para realizar pruebas unitarias de calidad y TDD mejoraba cuando dejé de pensar en las pruebas unitarias como la verificación de que un programa funciona, sino que verificaron que una unidad de código hace lo que esperaba . Esos son diferentes. No verifican que funcione, pero verifican que hace lo que creo que hace. Cuando comencé a pensar de esa manera, mi perspectiva cambió.

Brandon
fuente
El ejemplo de refactorización de inserción masiva es bueno. savePersonSin embargo, la posible prueba unitaria que sugerí en el OP, que una simulación de dataStore ha solicitado para cada persona en la lista, se rompería con una refactorización de inserción masiva. Lo que para mí indica que es una prueba unitaria pobre. Sin embargo, no veo una alternativa que pasaría tanto las implementaciones masivas como las de guardar por persona, sin usar una base de datos de prueba real y afirmar eso, lo que parece incorrecto. ¿Podría proporcionar una prueba que funcione para ambas implementaciones?
Jonás
1
@ jpmc26 ¿Qué pasa con una prueba que prueba que las personas se salvaron ...?
user253751
1
@immibis No entiendo lo que eso significa. Presumiblemente, la tienda real está respaldada por una base de datos, por lo que tendrías que burlarte o tropezar para una prueba unitaria. Entonces, en ese momento, estaría probando que su simulacro o trozo puede almacenar objetos. Eso es completamente inútil. Lo mejor que puede hacer es afirmar que savePersonse llamó al método para cada entrada, y si reemplazara el bucle con una inserción masiva, ya no llamaría a ese método. Entonces tu prueba se rompería. Si tienes algo más en mente, estoy abierto a ello, pero aún no lo veo. (Y no ver que era mi punto.)
jpmc26
1
@immibis No considero que sea una prueba útil. Usar el almacén de datos falsos no me da ninguna confianza de que funcionará con lo real. ¿Cómo sé que mi falsificación funciona como la verdadera? Prefiero dejar que un conjunto de pruebas de integración lo cubra. (Probablemente debería aclarar que quise decir "cualquier prueba de unidad " en mi primer comentario aquí.)
jpmc26
1
@immibis Realmente estoy reconsiderando mi posición. He sido escéptico sobre el valor de las pruebas unitarias debido a problemas como este, pero tal vez estoy subestimando el valor incluso si falsifica una entrada. Yo no sé que las pruebas de entrada / salida tiende a ser mucho más útiles que las pruebas simuladas pesados, pero tal vez la negativa a reemplazar una entrada con una falsa realidad, es parte del problema aquí.
jpmc26
6

Debe bakeCookies()ser probado? Si.

¿Cómo debería una función como esta ser probada por unidad, suponiendo que piense que debería? Es difícil para mí imaginar cualquier tipo de prueba unitaria que no solo se burle de la masa, la sartén y el horno, y luego afirme que se les aplican métodos.

Realmente no. Mire detenidamente QUÉ se supone que debe hacer la función: se supone que debe establecer el ovenobjeto en un estado específico. Al observar el código, parece que los estados de los objetos pany doughno importan mucho. Por lo tanto, debe pasar un ovenobjeto (o simularlo) y afirmar que se encuentra en un estado particular al final de la llamada a la función.

En otras palabras, debe afirmar que bakeCookies()horneó las cookies .

Para funciones muy cortas, las pruebas unitarias pueden parecer poco más que tautología. Pero no olvide que su programa durará mucho más que el tiempo que está empleado escribiendo. Esa función puede o no cambiar en el futuro.

Las pruebas unitarias cumplen dos funciones:

  1. Prueba que todo funciona. Esta es la función de prueba de unidad menos útil y parece que parece que solo tiene en cuenta esta funcionalidad al hacer la pregunta.

  2. Comprueba que las modificaciones futuras del programa no rompan la funcionalidad que se implementó anteriormente. Esta es la función más útil de las pruebas unitarias y evita la introducción de errores en programas grandes. Es útil en la codificación normal cuando se agregan características al programa, pero es más útil en la refactorización y optimizaciones donde los algoritmos centrales que implementan el programa cambian drásticamente sin cambiar ningún comportamiento observable del programa.

No pruebe el código dentro de la función. En su lugar, pruebe que la función hace lo que dice que hace. Cuando observa las pruebas unitarias de esta manera (funciones de prueba, no código), se dará cuenta de que nunca prueba las construcciones de lenguaje o incluso la lógica de la aplicación. Estás probando una API.

slebetman
fuente
Hola, gracias por tu respuesta. ¿Te importaría mirar mi segunda actualización y dar tu opinión sobre cómo probar la función de la unidad en ese ejemplo?
Jonás
Creo que esto puede ser efectivo cuando puede usar un horno real o un horno falso, pero es significativamente menos efectivo con un horno simulado. Los simulacros (según las definiciones de Meszaros) no tienen ningún estado, aparte de un registro de los métodos que se les han llamado. Mi experiencia es que cuando las funciones como bakeCookiesse prueban de esta manera, tienden a romperse durante los refactores que no afectarían el comportamiento observable de la aplicación.
James_pic
@James_pic, exactamente. Y sí, esa es la definición simulada que estoy usando. Entonces, dado tu comentario, ¿qué haces en un caso como este? ¿Renunciar a la prueba? ¿Escribir la frágil prueba de repetición de implementación de todos modos? ¿Algo más?
Jonás
@Jonah Por lo general, miraré probar ese componente con pruebas de integración (he encontrado que las advertencias sobre que es más difícil de depurar son exageradas, posiblemente debido a la calidad de las herramientas modernas), o me tomo la molestia de crear un falsa semi-convincente.
James_pic
3

¿Debe savePeople () ser probado en la unidad, o tales pruebas equivaldrían a probar la construcción incorporada para cada lenguaje?

Si. Pero puede hacerlo de una manera que simplemente vuelva a probar la construcción.

Lo que hay que tener en cuenta aquí es cómo se comporta esta función cuando una savePersonfalla a mitad de camino. Como se supone que funciona?

Ese es el tipo de comportamiento sutil que proporciona la función que debe aplicar con las pruebas unitarias.

Telastyn
fuente
Sí, estoy de acuerdo en que se deben probar las condiciones de error sutiles , pero eso no es una pregunta interesante: la respuesta es clara. De ahí la razón por la que expresé específicamente que, a los fines de mi pregunta, savePeopleno debería ser responsable del manejo de errores. Para aclarar de nuevo, suponiendo que savePeoplees responsable solo de recorrer la lista y delegar el guardado de cada elemento en otro método, ¿aún debería probarse?
Jonás
@ Jonás: Si vas a insistir en limitar tu prueba de unidad únicamente a la foreachconstrucción, y no a ninguna condición, efecto secundario o comportamiento fuera de ella, entonces tienes razón; La nueva prueba de unidad no es realmente tan interesante.
Robert Harvey
1
@jonah: ¿debería iterar y guardar la mayor cantidad posible o detenerse en caso de error? El único guardado no puede decidir eso, ya que no puede saber cómo se está utilizando.
Telastyn
1
@jonah: bienvenido al sitio. Uno de los componentes clave de nuestro formato Q & A es que no estamos aquí para ayudar a usted . Por supuesto, su pregunta le ayuda, pero también ayuda a muchas otras personas que visitan el sitio en busca de respuestas a sus preguntas. Respondí la pregunta que hiciste. No es mi culpa si no te gusta la respuesta o prefieres cambiar los postes. Y, francamente, parece que las otras respuestas dicen lo mismo, aunque de manera más elocuente.
Telastyn
1
@Telastyn, estoy tratando de obtener información sobre las pruebas unitarias. Mi pregunta inicial no era lo suficientemente clara, así que agrego aclaraciones para dirigir la conversación hacia mi pregunta real . Eliges interpretar eso como yo de alguna manera te engaño en el juego de "estar en lo cierto". He pasado cientos de horas respondiendo preguntas sobre revisión de código y SO. Mi propósito siempre es ayudar a las personas a las que respondo. Si el tuyo no lo es, esa es tu elección. No tienes que responder mis preguntas.
Jonás
3

La clave aquí es su perspectiva sobre una función particular como trivial. La mayor parte de la programación es trivial: asigne un valor, haga algunos cálculos, tome una decisión: si esto es eso, continúe un ciclo hasta ... De forma aislada, todo trivial. Acabas de leer los primeros 5 capítulos de cualquier libro que enseña un lenguaje de programación.

El hecho de que escribir una prueba sea tan fácil debería ser una señal de que su diseño no es tan malo. ¿Prefieres un diseño que no sea fácil de probar?

"Eso nunca cambiará". así es como comienzan la mayoría de los proyectos fallidos. Una prueba de unidad solo determina si la unidad funciona como se espera en un determinado conjunto de circunstancias. Haga que pase y luego puede olvidarse de los detalles de su implementación y simplemente usarlo. Usa ese espacio cerebral para la próxima tarea.

Saber que las cosas funcionan como se espera es muy importante y no trivial en proyectos grandes y especialmente en equipos grandes. Si hay algo que los programadores tienen en común, es el hecho de que todos hemos tenido que lidiar con el terrible código de otra persona. Lo menos que podemos hacer es tener algunas pruebas. En caso de duda, escriba una prueba y siga adelante.

JeffO
fuente
Gracias por tus comentarios. Buenos puntos. La pregunta que realmente quiero que se responda (acabo de agregar otra actualización para aclarar) es la forma adecuada de probar funciones que no hacen más que llamar a una secuencia de otros servicios a través de la delegación. En tales casos, parece que las pruebas unitarias apropiadas para "documentar el contrato" son solo una reformulación de la implementación de la función, afirmando que los métodos se invocan en varios simulacros. Sin embargo, la prueba de ser idéntica a la implementación en esos casos se siente mal ...
Jonah
1

¿Debe savePeople () ser probado en la unidad, o tales pruebas equivaldrían a probar la construcción incorporada para cada lenguaje?

Esto ya ha sido respondido por @BryanOakley, pero tengo algunos argumentos adicionales (supongo):

Primero, una prueba unitaria es para probar el cumplimiento de un contrato, no la implementación de una API; la prueba debe establecer condiciones previas y luego llamar, luego verificar los efectos, los efectos secundarios, cualquier invariante y las condiciones posteriores. Cuando decides qué probar, la implementación de la API no importa (y no debería) .

En segundo lugar, su prueba estará allí para verificar los invariantes cuando cambie la función . El hecho de que no cambie ahora no significa que no deba hacerse la prueba.

En tercer lugar, es valioso haber implementado una prueba trivial, tanto en un enfoque TDD (que lo exige) como fuera de él.

Cuando escribo C ++, para mis clases, tiendo a escribir una prueba trivial que crea una instancia de un objeto y verifica invariantes (asignables, regulares, etc.). Me sorprendió cuántas veces esta prueba se rompe durante el desarrollo (por ejemplo, al agregar un miembro no móvil en una clase, por error).

utnapistim
fuente
1

Creo que su pregunta se reduce a:

¿Cómo pruebo la unidad una función vacía sin que sea una prueba de integración?

Si cambiamos su función de horneado de galletas para devolver cookies, por ejemplo, se vuelve inmediatamente obvio cuál debería ser la prueba.

Si tenemos que llamar a pan.GetCookies después de llamar a la función, aunque podemos preguntarnos si es 'realmente una prueba de integración' o 'pero ¿no estamos probando el objeto pan?'

Creo que tiene razón en que tener pruebas unitarias con todo burlado y solo verificar las funciones xy y z se denominó falta de valor.

¡Pero! Yo diría que en este caso deberías refactorizar tus funciones vacías para devolver un resultado comprobable O usar objetos reales y hacer una prueba de integración

--- Actualización para el ejemplo createNewUser

  • Se necesita crear un nuevo registro de usuario en la base de datos
  • se debe enviar un correo electrónico de bienvenida
  • la dirección IP del usuario debe registrarse con fines de fraude.

OK, esta vez el resultado de la función no se devuelve fácilmente. Queremos cambiar el estado de los parámetros.

Aquí es donde me pongo un poco controvertido. Creo implementaciones simuladas concretas para los parámetros con estado

por favor, queridos lectores, ¡intenten controlar su ira!

entonces...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Esto separa el detalle de implementación del método bajo prueba del comportamiento deseado. Una implementación alternativa:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Aún pasará la prueba de la unidad. Además, tiene la ventaja de poder reutilizar los objetos simulados en las pruebas y también inyectarlos en su aplicación para UI o pruebas de integración.

Ewan
fuente
no se trata de una prueba de integración simplemente porque menciona los nombres de 2 clases concretas ... las pruebas de integración se tratan de probar integraciones con sistemas externos, como el disco IO, la base de datos, los servicios web externos, etc. llamando a pan.getCookies () -memoria, rápida, verifica lo que nos interesa, etc. Estoy de acuerdo en que hacer que el método devuelva las cookies directamente parece un mejor diseño.
Sara
3
Espere. Por lo que sabemos, pan.getcookies envía un correo electrónico a un cocinero pidiéndoles que saquen las galletas del horno cuando tengan la oportunidad
Ewan
Supongo que es teóricamente posible, pero sería un nombre bastante engañoso. ¿Quién ha oído hablar del equipo del horno que envió correos electrónicos? pero te veo señalar, depende. Supongo que estas clases colaborativas son objetos de hoja o simplemente cosas en memoria, pero si hacen cosas furtivas, entonces se necesita precaución. Sin embargo, creo que el envío de correos electrónicos definitivamente debe hacerse a un nivel superior. Esto parece ser la lógica de negocios descuidada y sucia en las entidades.
sara
2
Era una pregunta retórica, pero: "¿Quién ha oído hablar del equipo del horno que envió correos electrónicos?" venturebeat.com/2016/03/08/…
clacke
Hola Ewan, creo que esta respuesta se acerca más a lo que realmente estoy preguntando. Creo que su punto sobre bakeCookiesdevolver las galletas horneadas es acertado, y pensé un poco después de publicarlo. Así que creo que una vez más no es un gran ejemplo. Agregué una actualización más que, con suerte, proporciona un ejemplo más realista de lo que motiva mi pregunta. Agradecería su aporte.
Jonás
0

También debe probar bakeCookies: ¿qué debería / debería, por ejemplo, bakeCookies(egg, pan, oven)producir? Huevo frito o una excepción? Por sí solos, panni ovense preocuparán por los ingredientes reales, ya que se supone que ninguno de ellos debe hacerlo, pero por bakeCookieslo general deberían producir cookies. De manera más general que puede depender de cómo doughse obtiene y si es alguna posibilidad de que se convierta en mera eggo, por ejemplo wateren su lugar.

Tobias Kienzler
fuente