Las pruebas unitarias son una metodología en la que las unidades de código se prueban de forma aislada del resto de la aplicación. Una prueba de unidad podría probar una función, un objeto, una clase o un módulo en particular. Las pruebas unitarias son excelentes para saber si las partes individuales de una aplicación funcionan o no. Más vale que la NASA sepa si un escudo térmico funciona o no antes de lanzar el cohete al espacio.
Pero las pruebas unitarias no prueban si las unidades funcionan o no juntas cuando se componen para formar una aplicación completa. Para eso, necesitas pruebas de integración, que pueden ser pruebas de colaboración entre dos o más unidades, o pruebas funcionales completas de extremo a extremo de toda la aplicación en ejecución (también conocidas como pruebas de sistema). Finalmente, es necesario lanzar el cohete y ver lo que sucede cuando todas las partes se juntan.
Hay múltiples escuelas de pensamiento cuando se trata de pruebas del sistema, incluyendo el Desarrollo Dirigido por el Comportamiento (BDD), y las pruebas funcionales.
El Desarrollo Dirigido por el Comportamiento (BDD) es una rama del Desarrollo Dirigido por Pruebas (TDD). BDD utiliza descripciones legibles para el ser humano de los requisitos del usuario de software como base para las pruebas de software. Al igual que el Diseño Dirigido por el Dominio (DDD), un primer paso en el BDD es la definición de un vocabulario compartido entre las partes interesadas, los expertos del dominio y los ingenieros. Este proceso implica la definición de entidades, eventos y resultados que interesan a los usuarios, y darles nombres en los que todos puedan estar de acuerdo.
Los profesionales de BDD utilizan entonces ese vocabulario para crear un lenguaje específico del dominio que pueden utilizar para codificar las pruebas del sistema, como las pruebas de aceptación del usuario (UAT).
Cada prueba se basa en una historia de usuario escrita en el lenguaje ubicuo especificado formalmente y basado en el inglés. (Un lenguaje ubicuo es un vocabulario compartido por todas las partes interesadas.)
Una prueba para una transferencia en un monedero de criptomonedas podría tener este aspecto:
Story: Transfers change balancesAs a wallet user
In order to send money
I want wallet balances to updateGiven that I have $40 in my balance
And my friend has $10 is their balance
When I transfer $20 to my friend
Then I should have $20 in my balance
And my friend should have $30 in their balance.
Nótese que este lenguaje se centra exclusivamente en el valor de negocio que un cliente debe obtener del software en lugar de describir la interfaz de usuario del software, o cómo el software debe lograr los objetivos. Este es el tipo de lenguaje que se podría utilizar como entrada para el proceso de diseño de UX. El diseño de este tipo de requisitos de usuario por adelantado puede ahorrar mucho trabajo posterior en el proceso, ya que ayuda al equipo y a los clientes a ponerse de acuerdo sobre el producto que se está construyendo.
A partir de esta etapa, hay dos caminos que se pueden aventurar:
- Dar a la prueba un significado técnico concreto convirtiendo la descripción en un lenguaje específico del dominio (DSL) para que la descripción legible por el ser humano se duplique como código legible por la máquina, (continuar en el camino de BDD) o
- Traducir las historias de usuario en pruebas automatizadas en un lenguaje de propósito general, como JavaScript, Rust o Haskell.
De cualquier manera, es generalmente una buena idea tratar sus pruebas como pruebas de caja negra, lo que significa que el código de prueba no debe preocuparse por los detalles de implementación de la característica que está probando. Las pruebas de caja negra son menos frágiles que las pruebas de caja blanca porque, a diferencia de las pruebas de caja blanca, las pruebas de caja negra no estarán acopladas a los detalles de la implementación, que es probable que cambien a medida que los requisitos se añadan o ajusten, o que el código se refactorice.
Los defensores de BDD utilizan herramientas personalizadas como Cucumber para crear y mantener sus DSL personalizados.
Por el contrario, los defensores de las pruebas funcionales generalmente prueban la funcionalidad simulando las interacciones del usuario con la interfaz y comparando la salida real con la salida esperada. En el software web, eso suele significar el uso de un marco de pruebas que interactúa con el navegador web para simular la escritura, la pulsación de botones, el desplazamiento, el zoom, el arrastre, etc, y luego la selección de la salida de la vista.
Típicamente traduzco los requisitos del usuario en pruebas funcionales en lugar de mantener las pruebas BDD, sobre todo debido a la complejidad de la integración de los marcos BDD con las aplicaciones modernas, y el costo de mantener DSL personalizados y en inglés cuyas definiciones pueden terminar abarcando varios sistemas, e incluso varios lenguajes de implementación.
Encuentro el DSL legible para el lego útil para las especificaciones de muy alto nivel como una herramienta de comunicación entre las partes interesadas, pero un sistema de software típico requerirá órdenes de magnitud más pruebas de bajo nivel con el fin de producir el código adecuado y la cobertura de los casos para evitar que los errores de show-stopping lleguen a la producción.
En la práctica, hay que traducir «transfiero 20 dólares a mi amigo» en algo como:
- Abrir monedero
- Hacer clic en transferir
- Rellenar el importe
- Rellenar la dirección del monedero receptor
- Hacer clic
- Esperar a que aparezca un diálogo de confirmación
- Hacer clic en «Confirmar transacción»
- Las pruebas unitarias pueden probar que el estado del cliente local se actualiza correctamente y se presenta correctamente en la vista del cliente.
- Las pruebas funcionales pueden probar las interacciones de la UI y asegurar que los requisitos del usuario se cumplen en la capa de la UI. Esto también asegura que los elementos de la interfaz de usuario están conectados adecuadamente.
- Las pruebas de integración pueden probar que las comunicaciones de la API se producen adecuadamente y que las cantidades de la cartera del usuario se actualizaron realmente de forma correcta en la cadena de bloques.
Una capa por debajo, estás manteniendo el estado para el flujo de trabajo de «transferencia de dinero», y querrás pruebas unitarias que aseguren que la cantidad correcta se está transfiriendo a la dirección correcta de la cartera, y una capa por debajo de eso, querrás golpear las API de blockchain para asegurar que los saldos de la cartera fueron realmente ajustados apropiadamente (algo que el cliente puede ni siquiera tener una vista).
Estas diferentes necesidades de pruebas son mejor servidas por diferentes capas de pruebas:
Nunca he conocido a un interesado lego que sea remotamente consciente de todas las pruebas funcionales que verifican incluso el comportamiento de la interfaz de usuario de más alto nivel, y mucho menos uno que se preocupe por todos los comportamientos de nivel inferior. Dado que los legos no están interesados, ¿por qué pagar el costo de mantener un DSL para traducir para ellos?
Independientemente de si usted practica o no el proceso completo de BDD, tiene un montón de grandes ideas y prácticas que no debemos perder de vista. En concreto:
- La formación de un vocabulario compartido que los ingenieros y las partes interesadas pueden utilizar para comunicarse eficazmente sobre las necesidades del usuario y las soluciones de software.
- La creación de historias de usuario y escenarios que ayudan a formular los criterios de aceptación y una definición de hecho para una característica particular del software.
- La práctica de la colaboración entre los usuarios, el equipo de calidad, el equipo de producto y los ingenieros para llegar a un consenso sobre lo que el equipo está construyendo.
Otro enfoque de las pruebas del sistema son las pruebas funcionales.
¿Qué son las pruebas funcionales?
El término «pruebas funcionales» puede ser confuso porque ha tenido varios significados en la literatura de software.
ElIEEE 24765 da dos definiciones:
1. pruebas que ignoran el mecanismo interno de un sistema o componente y se centran únicamente en las salidas generadas en respuesta a entradas y condiciones de ejecución seleccionadas
2. pruebas realizadas para evaluar la conformidad de un sistema o componente con los requisitos funcionales especificados
La primera definición es lo suficientemente general como para aplicarse a casi todas las formas populares de pruebas, y ya tiene un nombre perfectamente adecuado que es bien entendido por los probadores de software: «pruebas de caja negra». Cuando hable de pruebas de caja negra, utilizaré ese término, en su lugar.
La segunda definición se suele utilizar en contraste con las pruebas que no están directamente relacionadas con las características y la funcionalidad de la aplicación, sino que se concentran en otras características de la misma, como los tiempos de carga, los tiempos de respuesta de la UI, las pruebas de carga del servidor, las pruebas de penetración de seguridad, etc. De nuevo, esta definición es demasiado vaga para ser muy útil por sí sola. Por lo general, queremos ser más específicos sobre el tipo de pruebas que estamos haciendo, por ejemplo, pruebas unitarias, pruebas de humo, pruebas de aceptación del usuario…
Por esas razones, prefiero otra definición que ha sido popular recientemente. Developer Works de IBM dice:
Las pruebas funcionales se escriben desde la perspectiva del usuario y se centran en el comportamiento del sistema en el que los usuarios están interesados.
Eso está mucho más cerca de la realidad, pero si vamos a automatizar pruebas, y esas pruebas van a probar desde la perspectiva del usuario, eso significa que tendremos que escribir pruebas que interactúen con la UI.
Dichas pruebas también pueden recibir los nombres de «pruebas de UI» o «pruebas E2E», pero esos nombres no sustituyen la necesidad del término «pruebas funcionales» porque hay una clase de pruebas de UI que prueban cosas como los estilos y los colores, que no están directamente relacionadas con los requisitos del usuario como «debería poder transferir dinero a mi amigo».
Utilizar «pruebas funcionales» para referirse a las pruebas de la interfaz de usuario para asegurar que cumple con los requisitos de usuario especificados se suele utilizar en contraste con las pruebas unitarias, que se definen como:
la prueba de unidades individuales de código (como funciones o módulos) aisladas del resto de la aplicación
En otras palabras, mientras que una prueba unitaria es para probar unidades individuales de código (funciones, objetos, clases, módulos) aisladas de la aplicación, un test funcional es para probar las unidades en integración con el resto de la app, desde la perspectiva del usuario que interactúa con la UI.
Me gusta la clasificación de «pruebas unitarias» para las unidades de código desde la perspectiva del desarrollador, y «pruebas funcionales» para las pruebas de la interfaz de usuario desde la perspectiva del usuario.
Pruebas unitarias frente a pruebas funcionales
Las pruebas unitarias suelen estar escritas por el programador de la aplicación, y prueban desde la perspectiva del programador.
Las pruebas funcionales se basan en los criterios de aceptación del usuario y deben probar la aplicación desde la perspectiva del usuario para garantizar que se cumplan los requisitos de éste. En muchos equipos, las pruebas funcionales pueden ser escritas o ampliadas por los ingenieros de calidad, pero cada ingeniero de software debe ser consciente de cómo se escriben las pruebas funcionales para el proyecto, y qué pruebas funcionales se requieren para completar la «definición de hecho» para un conjunto de características en particular.
Las pruebas de unidad se escriben para probar las unidades individuales aisladas del resto del código. Hay dos beneficios principales de este enfoque:
- Las pruebas unitarias se ejecutan muy rápido porque no dependen de otras partes del sistema, y como tal, normalmente no tienen E/S asíncronas que esperar. Es mucho más rápido y menos costoso encontrar y arreglar un fallo con pruebas unitarias que esperar a que se ejecute una suite de integración completa. Las pruebas unitarias suelen completarse en milisegundos, en lugar de minutos u horas.
- Las unidades deben ser modulares para que sea fácil probarlas aisladas de otras unidades. Esto tiene el beneficio añadido de ser muy bueno para la arquitectura de la aplicación. El código modular es más fácil de extender, mantener o reemplazar porque los efectos de cambiarlo se limitan generalmente a la unidad del módulo bajo prueba. Las aplicaciones modulares son más flexibles y más fáciles de trabajar para los desarrolladores a lo largo del tiempo.
- Tardan más en ejecutarse, porque deben probar el sistema de extremo a extremo, integrándose con todas las diversas partes y subsistemas en los que se basa la aplicación para permitir el flujo de trabajo del usuario que se está probando. Las grandes suites de integración a veces tardan horas en ejecutarse. He oído historias de suites de integración que tardaron días en ejecutarse. Recomiendo hiper-optimizar su tubería de integración para que se ejecute en paralelo para que pueda completar en menos de 10 minutos – pero eso es todavía demasiado tiempo para que los desarrolladores esperen en cada cambio.
- Asegúrese de que las unidades trabajan juntas como un sistema completo. Incluso si usted tiene una excelente cobertura de código de prueba de la unidad, usted todavía tiene que probar sus unidades integradas con el resto de la aplicación. No importa si los escudos térmicos de la NASA funcionan si no se mantienen unidos al cohete en la reentrada. Las pruebas funcionales son una forma de pruebas del sistema que garantizan que el sistema en su conjunto se comporta como se espera cuando está totalmente integrado.
- No altere el DOM. Si lo hace, su ejecutor de pruebas (por ejemplo, TestCafe) puede no ser capaz de entender cómo cambió el DOM, y esa alteración del DOM podría afectar a las otras aserciones que pueden estar confiando en la salida del DOM.
- No comparta el estado mutable entre las pruebas. Debido a que son tan lentas, es increíblemente importante que las pruebas funcionales puedan ejecutarse en paralelo, y no pueden hacerlo de forma determinista si están compitiendo por el mismo estado mutable compartido, lo que podría causar nondeterminismo debido a las condiciones de carrera. Como estás ejecutando pruebas del sistema, ten en cuenta que si estás modificando los datos del usuario, deberías tener diferentes datos de usuario de prueba en la base de datos para diferentes pruebas para que no fallen aleatoriamente debido a condiciones de carrera.
- No mezcles las pruebas funcionales con las pruebas unitarias. Las pruebas unitarias y las pruebas funcionales deben ser escritas desde diferentes perspectivas, y ejecutarse en diferentes momentos. Las pruebas unitarias deben ser escritas desde la perspectiva del desarrollador y ejecutarse cada vez que el desarrollador hace un cambio, y deben completarse en menos de 3 segundos. Las pruebas funcionales deben estar escritas desde la perspectiva del usuario, e implican una E/S asíncrona que puede hacer que las pruebas se ejecuten demasiado lentamente para que el desarrollador tenga una respuesta inmediata a cada cambio de código. Debería ser fácil ejecutar las pruebas unitarias sin desencadenar las ejecuciones de las pruebas funcionales.
- Ejecute las pruebas en modo headless, si puede, lo que significa que la interfaz de usuario del navegador no necesita realmente ser lanzada, y las pruebas pueden ejecutarse más rápido. El modo sin cabeza es una gran manera de acelerar la mayoría de las pruebas funcionales, pero hay un pequeño subconjunto de pruebas que no se pueden ejecutar en modo sin cabeza, simplemente porque la funcionalidad de la que dependen no funciona en modo sin cabeza. Algunos canales de CI/CD requieren que se ejecuten las pruebas funcionales en modo headless, así que si tiene algunas pruebas que no pueden ejecutarse en modo headless, puede que tenga que excluirlas de la ejecución de CI/CD. Asegúrese de que el equipo de calidad esté atento a ese escenario.
- Ejecute las pruebas en múltiples dispositivos. Sus pruebas siguen pasando en los dispositivos móviles? TestCafe puede ejecutarse en navegadores remotos sin necesidad de instalar TestCafe en los dispositivos remotos. Sin embargo, la funcionalidad de captura de pantalla no funciona en los navegadores remotos.
- Agarre capturas de pantalla en los fallos de las pruebas. Puede ser útil tomar una captura de pantalla si sus pruebas fallan para ayudar a diagnosticar lo que salió mal. TestCafe studio tiene una opción de configuración de ejecución para eso.
- Mantenga sus ejecuciones de pruebas funcionales por debajo de 10 minutos. Cualquier tiempo más largo creará demasiado retraso entre el desarrollador que trabaja en una característica y la fijación de algo que salió mal. 10 minutos es tiempo suficiente para que un desarrollador esté ocupado trabajando en la siguiente característica, y si la prueba falla después de más de 10 minutos, es probable que interrumpa al desarrollador que ha pasado a la siguiente tarea. Una tarea interrumpida tarda de media el doble de tiempo en completarse y contiene aproximadamente el doble de errores. TestCafe le permite ejecutar muchas pruebas de forma concurrente, y la opción de navegador remoto puede hacerlo a través de una flota de servidores de pruebas. Recomiendo aprovechar esas características para mantener sus ejecuciones de prueba tan cortas en la duración como sea posible.
- Detenga la tubería de entrega continua cuando las pruebas fallan. Uno de los grandes beneficios de las pruebas automatizadas es la capacidad de proteger a sus clientes contra las regresiones, es decir, errores en las características que solían funcionar. Este proceso de red de seguridad puede ser automatizado para que usted tenga una buena confianza en que su lanzamiento está relativamente libre de errores. Las pruebas en la tubería de CI/CD eliminan efectivamente el miedo al cambio de un equipo de desarrollo, que puede ser un serio drenaje en la productividad de los desarrolladores.
Las pruebas funcionales, por otro lado:
Las pruebas funcionales sin pruebas unitarias nunca pueden proporcionar una cobertura de código lo suficientemente profunda como para confiar en que se tiene una red de seguridad de regresión adecuada para la entrega continua. Las pruebas unitarias proporcionan una profundidad de cobertura de código. Las pruebas funcionales proporcionan una amplitud de cobertura de casos de prueba de requisitos de usuario.
Las pruebas funcionales nos ayudan a construir el producto correcto. (Validación)
Las pruebas unitarias nos ayudan a construir el producto correcto. (Verificación)Se necesitan ambas.
Nota: Ver Validación vs Verificación. Construir el producto correcto vs construir el producto correcto distinción fue descrita sucintamente por Barry Boehm.
Cómo escribir pruebas funcionales para aplicaciones web
Hay muchos frameworks que permiten crear pruebas funcionales para aplicaciones web. Muchos de ellos utilizan una interfaz llamada Selenium. Selenium es una solución de automatización multiplataforma y multinavegador creada en 2004 que permite automatizar las interacciones con el navegador web. El problema con Selenium es que es un motor externo a los navegadores que depende de Java, y conseguir que funcione junto con sus navegadores puede ser más difícil de lo necesario.
Más recientemente, ha aparecido una nueva familia de productos que se integran mucho más suavemente con los navegadores con menos piezas que preocuparse de instalar y configurar. Una de esas soluciones se llama TestCafe. Es la que actualmente uso y recomiendo.
Escribamos una prueba funcional para el sitio web del Día TDD. Primero, querrás crear un proyecto para ello. En un terminal:
mkdir tddday
cd tddday
npm init -y # initialize a package.json
npm install --save-dev testcafeAhora tendremos que añadir un
"testui"
script a nuestropackage.json
en el bloquescripts
:{
"scripts": {
"testui": "testcafe chrome src/functional-tests/"
}
// other stuff...
}Puedes ejecutar las pruebas escribiendo
npm run testui
, pero todavía no hay ninguna prueba que ejecutar.Crea un nuevo archivo en
src/functional-tests/index-test.js
:import { Selector } from 'testcafe';TestCafe pone a disposición automáticamente las funciones
fixture
ytest
. Puedes usarfixture
con la sintaxis literal de la plantilla etiquetada para crear títulos para grupos de pruebas:fixture `TDD Day Homepage`
.page('https://tddday.com');Ahora puedes seleccionar de la página y hacer afirmaciones usando las funciones
test
ySelect
. Cuando lo pones todo junto, queda así:TestCafe lanzará el navegador Chrome, cargará la página, esperará a que se cargue la página y esperará a que su selector coincida con una selección. Si no coincide con nada, la prueba terminará y fallará. Si coincide con algo, comprobará el valor seleccionado real contra el valor esperado, y la prueba fallará si no coinciden.
TestCafe proporciona métodos para probar todo tipo de interacciones de la interfaz de usuario, incluyendo hacer clic, arrastrar, escribir texto, etc.
TestCafe también suministra una rica API de selector para hacer selecciones de DOM sin dolor.
Vamos a probar el botón de registro para asegurar que navega a la página correcta al hacer clic. Primero, necesitaremos una forma de comprobar la ubicación actual de la página. Nuestro código de TestCafe se ejecuta en Node, pero necesitamos que se ejecute en el cliente. TestCafe nos proporciona una forma de ejecutar el código en el cliente. En primer lugar, tendremos que añadir
ClientFunction
a nuestra línea de importación:import { Selector, ClientFunction } from 'testcafe';Ahora podemos utilizarlo para probar la ubicación de la ventana:
Si no estás seguro de cómo hacer lo que estás tratando de hacer, TestCafe Studio te permite grabar y reproducir las pruebas. TestCafe Studio es un IDE visual para grabar y editar pruebas funcionales de forma interactiva. Está diseñado para que un ingeniero de pruebas que no sepa JavaScript pueda construir un conjunto de pruebas funcionales. Las pruebas que genera esperan automáticamente trabajos asíncronos como la carga de páginas. Al igual que el motor de TestCafe, TestCafe Studio puede producir pruebas que se pueden ejecutar de forma concurrente en muchos navegadores, e incluso en dispositivos remotos.
TestCafe Studio es un producto comercial con una prueba gratuita. No es necesario comprar TestCafe studio para utilizar el motor de código abierto de TestCafe, pero el editor visual con características de grabación incorporadas es definitivamente una herramienta que vale la pena explorar para ver si es adecuado para su equipo.
TestCafe ha establecido un nuevo nivel para las pruebas funcionales entre navegadores. Después de haber soportado muchos años de tratar de automatizar las pruebas entre plataformas, estoy feliz de decir que finalmente hay una manera bastante indolora para crear pruebas funcionales, y ahora no hay una buena excusa para descuidar sus pruebas funcionales, incluso si usted no tiene ingenieros de calidad dedicados para ayudarle a construir su conjunto de pruebas funcionales.
Dos y Don’ts de pruebas funcionales
Siguientes pasos
Únete a TDD Day.com – un plan de estudios de TDD de todo el día que ofrece 5 horas de contenido de vídeo grabado, proyectos para aprender las pruebas unitarias y las pruebas funcionales, cómo probar los componentes de React, y un cuestionario interactivo para asegurarse de que has dominado el material.