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 dough
encaja pan
o afirme que se dough
está agotando. Afirma que pan
contiene cookies después de la llamada a la función. Afirma que oven
está vacío / en el mismo estado que antes.
Para pruebas adicionales, verifique los casos límite: ¿Qué sucede si oven
no está vacío antes de la llamada? ¿Qué pasa si no hay suficiente dough
? Si el pan
ya 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 dataStore
y 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 dataStore
y 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
dataStore
que 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).