javascript-testing-best-practices
javascript-testing-best-practices
👇 Por qué esta guía puede hacerte llevar tus habilidades de testing al siguiente nivel
📗 46+ buenas practicas: Súper comprensiva y exhaustiva
Esta es una guía completa para JavaScript y Node.js de la A a la Z. Resume y selecciona docenas de los mejores post de blogs, libros, y herramientas ofrecidas en el mercado
🚢 Avanzado: Va 10.000 kilómetros más allá de lo básico
Súbete a un viaje que va más allá de lo básico, llegando a temas avanzados como testeando en producción, mutation testing, property-based testing y muchas otras herramientas estratégicas y profesionales. Si lees esta guía completamente es probable que tus habilidades de testing acaben por encima de la media
🌐 Full-stack: front, backend, CI, cualquier cosa
Empieza por comprender las técnicas de testing ubicuas que son la base de cualquier nivel de aplicación. Luego, profundiza en tu área de elección: frontend/UI, backend, CI o tal vez todos
Escrita por Yoni Goldberg
- Consultor JavaScript & Node.js
- 📗 Testing Node.js & JavaScript de la A a la Z - Mi curso completamente online con más de 10 horas de video, 14 tipos de test y más de 40 buenas practicas
- Sígueme en Twitter
Traducciones - leelas en tu propio idioma
- 🇨🇳Chino - cortesía de Yves yao
- 🇰🇷Coreano - cortesía de Rain Byun
- 🇵🇱Polaco - cortesía de Michal Biesiada
- 🇪🇸Español - cortesía de Miguel G. Sanguino
- 🇧🇷Portugués-BR - cortesía de Iago Angelim Costa Cavalcante, Douglas Mariano Valero y koooge
- 🇺🇦Ukrainian - cortesía de Serhii Shramko
- ¿Quieres traducir a tu propio lenguaje? por favor abre una issue 💜
Tabla de Contenidos
Sección 0: La Regla de Oro
Un solo consejo que inspira a todos los demás (1 apartado especial)
Sección 1: La Anatomía de un Test
La base - estructurando test claros (12 apartados)
Sección 2: Backend
Escribiendo test de backend y microservicios eficientemente (8 apartados)
Sección 3: Frontend
Escribiendo test para web UI incluyendo test de componente y E2E (11 apartados)
Sección 4: Midiendo la Efectividad de los Test
Vigilando al vigilante - midiendo la calidad de los test (4 apartados)
Sección 5: Integración Continua
Pautas para Integración Continua en el mundo de JS (9 apartados)
Sección 0️⃣: La Regla de Oro
⚪️ 0 La Regla de Oro: Diseñando testing ligero
✅ Haz: El código de los test no es como el código de producción - diséñalo para que sea simple, corto, sin abstracciones, plano, agradable de trabajar, ligero. Uno debe mirar un test y entender la intención al instante
Nuestras mentes están llenas por el código de producción, no tenemos espacio de cabeza para añadir más cosas complejas. Si intentamos que introducir otro código desafiante en nuestro cerebro, ralentizará todo el equipo, lo que va en contra de la razón por la que hacemos testing. Prácticamente esta una de las razones por la que muchos equipos simplemente abandonan el testing
Los test son una oportunidad de tener algo más: un asistente amable y sonriente, con el que es un placer trabajar y da mucho valor a cambio de una inversión muy pequeña. La ciencia nos dice que tenemos dos sistemas cerebrales: el sistema 1 se usa para actividades sin esfuerzo como conducir un automóvil en una carretera vacía y el sistema 2, que está destinado a operaciones complejas y conscientes como resolver una ecuación matemática. Diseña tus test para el sistema 1, cuando observes el código de un test, debería parecer tan fácil como modificar un documento HTML y no como resolver 2X(17 × 24)
Esto se puede lograr mediante la selección de técnicas cuidadosamente, herramientas y objetivos de test que son rentables y proporcionan un gran retorno de la inversión. Testea solo lo que sea necesario, esfuérzate por mantenerlo ágil, a veces incluso vale la pena abandonar algunos test y cambiar la confiabilidad por agilidad y simplicidad
La mayoría de los siguientes consejos son derivados de este principio
¿Listo para empezar?
Sección 1: La Anatomía de un Test
⚪ ️ 1.1 Incluye 3 partes en los nombres de tus test
✅ Haz: El reporte de un test debe indicar si la revisión de la aplicación actual cumple los requisitos para las personas que no están necesariamente familiarizadas con el código: el tester, el DevOps que está desplegángolo y el futuro tú de dentro de dos años. Esto se puede lograr si los test hablan al nivel de los requisitos e incluyen 3 partes:
(1) ¿Qué se está testeando? Por ejemplo, el método ProductsService.addNewProduct
(2) ¿Bajo qué escenario y circunstancias? Por ejemplo, no se pasa ningún precio al método
(3) ¿Cuál es el resultado esperado? Por ejemplo, el nuevo producto no está aprobado
❌ De lo contrario: Un despliegue falla, un test llamado "Agregar producto" ha fallado. ¿Esto te dice exactamente qué está funcionando mal?
👇 Nota: Cada apartado tiene ejemplos de código y, en ocasiones, también una imagen ilustrativa. Haga clic para ampliar
✏ Código de Ejemplo
👏 Ejemplo de cómo hacerlo correctamente: Un nombre de test que consta de 3 partes
//1. unidad que esta siendo testeada
describe('Products Service', function() {
describe('Add new product', function() {
//2. escenario y 3. quá se espera
it('When no price is specified, then the product status is pending approval', ()=> {
const newProduct = new ProductService().add(...);
expect(newProduct.status).to.equal('pendingApproval');
});
});
});
👏 Ejemplo de cómo hacerlo correctamente: Un nombre de test que consta de 3 partes
© Créditos y más información
⚪ ️ 1.2 Estructura tus test con el patron AAA
✅ Haz: Estructura tus test con 3 secciones bien separadas Ajustar, Actuar y Afirmar (AAA en inglés Arrange, Act & Assert). Seguir esta estructura garantiza que quien lea nuestro test no se estruje el cerebro en comprender los test:
1a A - Ajustar: configura el código, crea el escenario que el test pretende simular. Esto podría incluir crear instancias de la unidad bajo el constructor del test, agregar registros de base de datos, mocks y stubs de objetos y cualquier otro código necesario
2a A - Actuar: Ejecuta la unidad en test. Normalmente 1 línea de código
3a A - Afirmar: Comprobar que el valor recibido satisface las expectativas. Normalmente 1 línea de código
❌ De lo contrario: No solo emplearas horas comprendiendo el código principal, si no que lo que debería haber sido la parte más simple del día (testing) te ha estrujado el cerebro
✏ Código de Ejemplo
👏 Ejemplo de cómo hacerlo correctamente: Un test estructurado con el patron AAA
describe("Customer classifier", () => {
test("When customer spent more than 500$, should be classified as premium", () => {
//Ajustar
const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
//Actuar
const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
//Afirmar
expect(receivedClassification).toMatch("premium");
});
});
:thumbsdown: Ejemplo Anti Patrón: Sin separación, un bloque, más dificil de interpretar
test("Should be classified as premium", () => {
const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
expect(receivedClassification).toMatch("premium");
});
⚪ ️1.3 Describe las expectativas en el lenguaje del producto: usa aserciones estilo BDD
✅ Haz: Escribir el código de tus test de forma declarativa permite que aquel que lo lea tenga al instante la idea sin tener que estrujarse el cerebro. Cuando escribes el código de los test de forma imperativa estará lleno de condiciones lógicas y obligas al que lo lee a gastar mucho más tiempo en comprenderlo. En este caso, escribe el código de la forma más humana, de forma declarativa con BDD, usando expect
o should
y sin usar código personalizado. Si Chai o Jest no incluyen las aserciones que deseas y se hace muy repetitivo, siempre puedes extender Jest con Jest matcher o escribir un plugin de Chai
❌ De lo contrario: El equipo escribirá menos test y acabará marcando los test más molestos con .skip()
✏ Código de Ejemplo
:thumbsdown: Ejemplo Anti Patrón: quien lea nuestro test deberá enfrentarse a un código largo e imperativo para conocer la historia del test
test("When asking for an admin, ensure only ordered admins in results", () => {
//presuponiendo que hayamos agregado aquí dos administradores "admin1", "admin2" y "user1"
const allAdmins = getUsers({ adminOnly: true });
let admin1Found,
adming2Found = false;
allAdmins.forEach(aSingleUser => {
if (aSingleUser === "user1") {
assert.notEqual(aSingleUser, "user1", "A user was found and not admin");
}
if (aSingleUser === "admin1") {
admin1Found = true;
}
if (aSingleUser === "admin2") {
admin2Found = true;
}
});
if (!admin1Found || !admin2Found) {
throw new Error("Not all admins were returned");
}
});
👏 Ejemplo de cómo hacerlo correctamente: Comprender el siguiente test declarativo es muy sencillo
it("When asking for an admin, ensure only ordered admins in results", () => {
//presuponiendo que hayamos agregado aquí dos administradores
const allAdmins = getUsers({ adminOnly: true });
expect(allAdmins)
.to.include.ordered.members(["admin1", "admin2"])
.but.not.include.ordered.members(["user1"]);
});
⚪ ️ 1.4 Acercarse al testing caja-negra: Testea solo métodos públicos
✅ Haz: Testear las partes internas suele traer un gasto extra enorme para obtener muy poco beneficio. Si tu código/API comprueba todos los resultado posibles correctamente, ¿deberías perder las próximas 3 horas en comprobar como esta funcionando internamente y después mantener esos test tan frágiles? Cada vez que se comprueba un comportamiento público, la implementación privada es implícitamente testeada y tus test se romperán sólo si hay un problema concreto (por ejemplo una salida incorrecta). Este enfoque también es conocido como behavioral testing
(testing de comportamiento). Por otro lado, si se testean las partes internas (caja-blanca) tu enfoque cambia de planificar la salida del componente a detalles minúsculos, y tus test pueden romperse debido a refactors de código menores sin que se rompan los test de salida, por lo que aumenta tremendamente el mantenimiento de los mismos
❌ De lo contrario: Tus test se comportaran como la fabula de que viene el lobo: gritando falsos positivos (por ejemplo, un test falla porque se cambio el nombre a una variable probada). Como es de esperar, la gente empezara a ignorar estos test hasta que un día ignoren un test de verdad...
✏ Código de Ejemplo
:thumbsdown: Ejemplo Anti Patrón: Un test esta testeando la parte interna sin ninguna razón aparente
class ProductService {
//este método es usado solo internamente
//Cambiarle el nombre causara que el test falle
calculateVATAdd(priceWithoutVAT) {
return { finalPrice: priceWithoutVAT * 1.2 };
//Cambiar el formatos de salida o nombre de la clave a continuación hará que el test falle
}
//public method
getPrice(productId) {
const desiredProduct = DB.getProduct(productId);
finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice;
return finalPrice;
}
}
it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => {
//No hay ningún requisito para permitir a los usuarios calcular el IVA, solo existe el de mostrar el precio final al usuario. Sin embargo, falsamente insistimos en testear las partes privadas de la clase
expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);
});
⚪ ️ ️1.5 Eligiendo los dobles de los test correctamente: Evita mocks en favor de stubs y spies
✅ Haz: Los dobles de los test son un mal necesario porque están acoplados a las tripas de la aplicación, sin embargo, algunos proporcionan un valor inmenso (Aquí tienes un bueno recordatorio de que son los dobles de los test: mocks vs stubs vs spies)
Antes de usar un doble, hazte una simple pregunta: ¿Lo voy a usar para testear una funcionalidad que aparece, o puede aparecer, en el documento de requisitos? Si no, eso huele que estas testeando partes privadas
Por ejemplo, si quieres testear que tu app se comporta razonablemente cuando el servicio de pagos está caído, deberías hacer un stub del servicio de pagos y devolver un 'Sin respuesta' para asegurar que la unidad que está siendo testeada devuelve el valor correcto. Esto verifica que el comportamiento/respuesta/resultado de nuestra app en ciertos escenarios. También podrías usar un spy para asegurar que un email ha sido enviado cuando este servicio está caído — esto es nuevamente una verificación de comportamiento que probablemente aparezca en el documento de requisitos ("Enviar un correo electrónico si no se pudo guardar el pago"). En el lado opuesto, si se mockea el servicio de pagos y se asegura que se haya llamado con el tipado correcto — entonces tu test esta comprobando cosas internas que no tienen nada que ver con la funcionalidad de la app y es muy probable que cambien con frecuencia
❌ De lo contrario: Cualquier refactor de código te exigirá buscar todos los mocks que tengas y tendrás que actualizarlos en consecuencia. Los test pasan de ser un amigo útil a una carga más
✏ Código de Ejemplo
:thumbsdown: Ejemplo Anti Patrón: Mocks centrados en la parte interna
it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => {
//Asumimos que ya hemos añadido un producto
const dataAccessMock = sinon.mock(DAL);
//hmmm MAL: testear las partes internas está siendo nuestro principal objetivo, en vez de ser un efecto secundario
dataAccessMock
.expects("deleteProduct")
.once()
.withArgs(DBConfig, theProductWeJustAdded, true, false);
new ProductService().deletePrice(theProductWeJustAdded);
dataAccessMock.verify();
});
👏 Ejemplo de cómo hacerlo correctamente: los spies se centran en testear los requisitos pero como efecto secundario inevitable se están testeando las partes internas
it("When a valid product is about to be deleted, ensure an email is sent", async () => {
//Asumimos que ya hemos añadido un producto
const spy = sinon.spy(Emailer.prototype, "sendEmail");
new ProductService().deletePrice(theProductWeJustAdded);
//hmmm OK: ¿Testeamos las partes internas? Si, pero como efecto secundario de hacer test de los requisitos (enviar un email)
expect(spy.calledOnce).to.be.true;
});
📗 ¿Quieres aprender todas esto con video en directo?
Visita el curso online Testing Node.js & JavaScript From A To Z
⚪ ️1.6 No uses “foo”, usa datos realistas
✅ Haz: A menudo, los bugs de producción se revelan bajo una entrada muy específica y sorprendente: cuanto más realista sea la entrada de un test, mayores serán las posibilidades de detectar bugs temprano. Utiliza librerías dedicadas como [Faker] (https://www.npmjs.com/package/faker) para generar datos pseudo-reales que se asemejan en variedad y forma a los datos de producción. Por ejemplo, dichas librerías pueden generar números de teléfono realistas, nombres de usuario, tarjetas de crédito, nombres de empresas e incluso texto "lorem ipsum". También puedes crear algunos test (además de los test unitarios, no como un reemplazo) que aleatorizan los datos falsos para forzar la unidad que estamos testeando o incluso importar datos reales de su entorno de producción. ¿Quieres llevarlo al siguiente nivel? Ve la próxima sección (test basados en propiedades)
❌ De lo contrario: Todo tus test de desarrollo estarán en verde falsamente cuando uses datos sintéticos como "Foo", pero luego en producción pueden ponerse en rojo cuando un hacker use cadenas extrañas como “@3e2ddsf . ##’ 1 fdsfds . fds432 AAAA”
✏ Código de Ejemplo
:thumbsdown: Ejemplo Anti Patrón: Un conjunto de test que dan ok debido a datos no realistas
const addProduct = (name, price) => {
const productNameRegexNoSpace = /^\S*$/; //no se admiten espacios
if (!productNameRegexNoSpace.test(name)) return false; //esta rama nunca se testeara debido a inputs sintéticos
//algo de lógica aquí
return true;
};
test("Wrong: When adding new product with valid properties, get successful confirmation", async () => {
//La cadena "Foo" que es usada en todo los test, nunca provocará un resultado false
const addProductResult = addProduct("Foo", 5);
expect(addProductResult).toBe(true);
//Falso positivo: la operación tuvo éxito porque nunca lo intentamos con un
//nombre de producto largo que incluya espacios
});
:clap:Ejemplo de cómo hacerlo correctamente: Generando datos de entrada realistas
it("Better: When adding new valid product, get successful confirmation", async () => {
const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
//Datos de entrada generados aleatoriamente: {'Sleek Cotton Computer', 85481}
expect(addProductResult).to.be.true;
//El test falla, El valor de entrada random ha provocado que se vaya por un camino que nunca planeamos
//!Hemos descubierto un bug muy pronto!
});