Introducción al testing con ejemplos prácticos de frontend: ¿De qué me sirven y qué es lo que debería probar?
Introducción
El testing en el desarrollo de software ha sido muy subestimado y mucha gente lo considera una carga extra o un gasto innecesario de tiempo.
Sin embargo, en aplicaciones empresariales como las que trabajamos, los tests juegan un rol esencial. No solo nos ayudan a detectar errores antes de que lleguen al usuario final, sino que también mejoran nuestra confianza al hacer cambios en el código, simplificando el proceso de mantenimiento/migraciones y también sirven para mantener una documentación activa de las funcionalidades que provee cada componente.
Muchos desarrolladores empiezan sin ver el valor del testing, yo mismo solía ser escéptico al respecto. A veces, puede parecer algo complicado o innecesario, pero con este artículo intentaré hacerte ver sus beneficios e introducirte al tema para que no tengas dudas a la hora de saber qué es lo que deberías testear en tu proyecto, todo con ejemplos prácticos de frontend.
El testing se enseña mal
A menudo, el testing se introduce con ejemplos muy simplificados, como el clásico ejemplo de probar una función de suma, multiplicación o potencia.
Aunque es un ejemplo fácil de entender, no refleja los desafíos de la vida real a los que nos enfrentamos al trabajar en aplicaciones empresariales.
Las funciones como una simple suma no tienen la complejidad de manejar estados, entradas múltiples, condiciones o cambios arbitrarios en las especificaciones con el tiempo, por lo que pueden hacernos pensar que el testing es más simple o menos útil de lo que realmente es en un caso real.
Yo mismo pasé por eso, cuando me lo enseñaron no le vi mucha utilidad y lo veía como una forma innecesaria de sobrecomplicar el desarrollo para lo que iba a aportar.
Un ejemplo más realista
Después de darle muchas vueltas, he encontrado un ejemplo que puede demostrar bastante bien la utilidad de los tests, con una casuística más real donde se pueda entender qué nos aporta de forma sencilla.
Imaginemos que tenemos una web de pedidos, y que existe una función muy importante que es la de aplicar descuento applyDiscount
, esta función a lo largo de los años se ha ido complicando mucho porque hay muchos casos diferentes interconectados que casi nadie conoce de forma completa.
Cada vez que se tienen que hacer cambios sobre esa función es un proceso lento y complicado en el que resulta muy difícil afirmar con certeza que el cambio funciona bien ya que hay demasiadas posibilidades para probarlas manualmente.
Algo como esto:
function applyDiscount(clientData, purchaseData) {
let discountPercentage = 0
if (clientData.wasRegisteredBefore(2015)) {
discountPercentage = 5
} else {
if (clientData.isUnderAge()){
if (clientData.isFromAustralia()) {
if (purchaseData.wasOrderedDuringFullMoon()) {
discountPercentage = 37
} else {
if (purchaseData.isDuringSummer()) {
discountPercentage = 11
} else {
discountPercentage = 1
}
}
} else {
discountPercentage = discountPercentage * 1.5
}
}
if (clientData.isBanned()) {
throw new Error('User cannot buy because is banned')
}
if (purchaseData.containsSpecialProduct('Cthulhu')) {
discountPercentage = discountPercentage / 2
if (clientData.isElderAge()) {
throw new Error('User cannot buy evil products')
}
}
discountPercentage += 5
}
// ...
// otras condiciones que complican la función y aumentan
// exponencialmente el tiempo necesario para probar todas las combinaciones a mano
// ...
discountPercentage = discountPercentage > 100 ? 100 : discountPercentage
return {
discountPercentage,
otherRandomData
}
}
⚠️ Todo esto es código legacy inventado, escrito en Javascript, aunque se podría limpiar y modularizar para hacerlo más legible y mantenible, los casos de uso siguen siendo la misma cantidad a la hora de probarlos
Si tuvieras que hacer un desarrollo sobre una función como esa, para por ejemplo, añadir una condición para que los pedidos que se hagan en luna nueva, se les aplique un 20% de aumento en el precio. La parte de programar la condición sería sencilla, pero como hemos comentado antes, te puedes imaginar que probar todos los casos relacionados a mano tardaría cada vez más conforme añadamos condiciones.
Cuando esa misma aplicación si ya la hemos desarrollado creando sus tests oportunos, cuando nos toque modificar o añadirle algo a esa función, podremos ahorrarnos el probar a mano todas las combinaciones posibles para comprobar que todo sigue funcionando como antes y nuestros cambios funcionan correctamente integrados con el desarrollo previo.
Cualquier duda sobre el funcionamiento esperado de la función estaría documentada con su propio test y así no tendríamos que pasar por todas las dificultades mencionadas anteriormente y ganaremos fluidez y fiabilidad en nuestro desarrollo.
¿De qué me sirven?
Con ese ejemplo creo que ya vamos entendiendo un poco mejor los problemas que intentamos resolver introduciendo el testing en nuestro desarrollo. Ahora voy a explicar algunas de las ventajas más en detalle:
- Encontrar errores en la etapa de desarrollo:
Escribir tests para nuestra aplicación nos obliga a pensar en todos los casos que se puedan dar y en el proceso podemos encontrar errores que se nos habrían pasado solamente probando la aplicación manualmente. Además con el coverage podemos ver que partes del código de nuestra aplicación se ha ejecutado durante los tests, lo que nos facilita encontrar casos de uso al pensar en cuando se deberían ejecutar.
- Facilitar el mantenimiento y evolución del código:
Cuando tenemos buenos tests en nuestra aplicación, el hacer grandes cambios o tocar partes antiguas no nos debería intimidar. Ya que al haber automatizado las pruebas que certifican que la aplicación funciona correctamente, nosotros solamente tenemos que desarrollar los cambios y en lugar de probar a mano todo lo que se nos ocurra para certificar que funciona como antes, simplemente lanzamos un comando. 😎
- Documentar lo que se espera que haga de la aplicación:
Como nuestros tests deberían probar las funcionalidades de nuestra aplicación, cuando un nuevo desarrollador llega al proyecto, puede simplemente leer el enunciado y los pasos de los tests para saber cual es el comportamiento esperado de la aplicación. Y así poder empezar a hacer cambios con confianza de que no está rompiendo nada.
Además como los tests son código que se ejecuta durante las pipelines por cada commit, sirven como una documentación actualizada y confiable del estado del proyecto.
"No sirven para nada, nunca fallan"
Cuando he escuchado argumentos en contra de los tests esto es una de las cosas que se repiten bastante:
La mayoría de veces que fallan es porque he cambiado cosas y no porque hayan encontrado ningún bug.
Entiendo que al principio pueda parecer que es algo que no vale la pena porque consume muchos más recursos que los beneficios que nos pueda parecer que proporciona, aún con lo mencionado antes.
Pero un buen ejemplo para entender su importancia son las revisiones médicas. Seguramente la mayoría de tus revisiones médicas hayan pasado todos los tests sin ningún resultado negativo, pero en ningún momento piensas que no son útiles, su utilidad no se encuentra en su uso individual, se encuentra en el uso prolongado para encontrar problemas antes de que ocurran.
Tipos de tests
Si ya sabías algo sobre testing habrás escuchado que existen diferentes tipos y que debemos tener una cantidad diferente de cada uno de los tipos.
Probablemente habrás visto alguna de estas pirámides:
Sin entrar mucho en detalle, ya que esto es algo de lo que se ha escrito mucho, esto son los tipos de tests más comunes:
-
Unit tests: Estos son los tests que prueban que las piezas individuales funcionan por separado. Aquí tienes algunos ejemplos con casos de la vida real:
- 🔗 En el caso de una cadena: podríamos comprobar que cada eslabón aguanta un máximo de peso.
- 🪑 En el caso de una silla: podríamos probar cual es el peso máximo que aguanta las patas por separado.
-
Integration tests: Estos son los tests que prueban que las piezas individuales ahora funcionan en conjunto. Siguiendo los ejemplos de antes:
- 🔗 En el caso de una cadena: sería comprobar que cuando juntamos varios eslabones para formar una cadena, aguantan el peso de un objeto.
- 🪑 En el caso de una silla: sería comprobar que ahora las patas de la silla pegadas al asiento es lo que aguanta un peso máximo.
-
Tests E2E (End to End): Estos son los tests que prueban el comportamiento más cercano al resultado final. Los equivalentes serían:
- 🔗 En el caso de una cadena: probar que funciona con las ruedas del coche en la nieve.
- 🪑 En el caso de una silla: probar que no se rompe después de un día en un restaurante.
Aunque en las primeras etapas del testing creo no tiene mucho sentido enfocarse demasiado en este punto. En su lugar, creo que es más valioso empezar por entender dónde se ejecutan nuestros tests.
Entornos de tests
Cuando estamos haciendo testing estamos automatizando nuestras pruebas manuales, pero en muchos casos para ello no utilizamos el mismo entorno que el que utilizaríamos para nuestras pruebas manuales (principalmente nuestro navegador), sino que utilizaremos o un navegador especial o un entorno que simula un navegador (JSDom, Happy-dom, etc).
Es importante conocer la diferencia entre estos porque eso define el como escribimos nuestros tests, lo que será posible probar con ellos y lo mucho que van a tardar en ejecutarse.
- Pruebas sin navegador:
Estas son las más comunes y lo que se refiere la mayoría cuando se habla de testing, normalmente la mayoría de los tests que escribamos serán sin navegador. Esto es porque para ganar en velocidad en lugar de un navegador entero, utilizamos un sustituto que provee las funcionalidades básicas que necesitamos para comprobar que nuestro código funciona.
El problema de estos es que en favor de la velocidad, nos alejamos de un entorno real y perdemos la capacidad de probar algunas cosas, sobretodo las relacionadas con el objeto window o las partes visuales.
Para escribir estos tests utilizamos librerías como Vitest, Jest, etc
- Pruebas con navegador:
También existe la posibilidad de que nuestros tests se ejecuten sobre un navegador lo más parecido a uno real posible. Estos se usan generalmente para resolver problemas que nos encontramos con los anteriores. Ya que nos permiten probar la parte visual con pruebas como comparar capturas de como se renderiza nuestro componente antes y después de nuestros cambios. O incluso grabar videos de toda la interacción con nuestra aplicación realizada durante los tests.
La contrapartida de estos tests es que consumen bastantes más recursos de computación y tardan más en ser ejecutados, además se multiplica su coste si los ejecutamos en diferentes navegadores y tamaños de pantalla. Es por eso que se suelen utilizar para casos más puntuales que no conseguimos cubrir de otra manera.
Para escribir estos tests utilizamos librerías Playwright, Cypress, Puppeteer, etc
Al final decidir donde utilizar unos y otros depende de las necesidades que tengamos ya que ambos tipos se complementan.
¿Qué debería probar?
Te voy a contestar con otra pregunta:
¿Cuando estas desarrollando sin testing, como sabes que has acabado tu desarrollo y lo puedes entregar?
Tu respuesta será diferente según el tipo de proyecto en el que estés trabajando, pero es algo que solemos tener claro cuando desarrollamos sin testing.
Realmente las preguntas que debemos hacernos son algo más parecidas a estas:
¿Qué es lo que debería hacer mi desarrollo, y como lo compruebo utilizando solamente código?
Porque realmente lo que tienes que probar es lo mismo que ya ibas a probar manualmente si no usases testing, lo único que estamos haciendo diferente es escribir código para automatizar esas pruebas y que en el futuro no tengamos que repetirlas a mano.
Lo más importante de esto es que tenemos que escribir tests que sean lo suficientemente específicos como para que comprueben que la aplicación funciona correctamente. Pero sin llegar a tanto detalle como para que cualquier cambio de implementación como el cambiar una librería, nos obligue a reescribir nuestros tests al completo.
Eso en el entorno frontend para comprobarlo lo que solemos usar es:
- Verificar que los datos se muestran
- Confirmar que un elemento tiene una clase
- Comprobar alguna otra propiedad de un elemento
- Asegurarse de que tras ciertas acciones la aplicación reacciona (clicks, teclas, cambios en la ventana, etc)
- Verificar que se han lanzado ciertos eventos
- Validar que se ha navegado a otra página u otro cambio en la ruta
- Confirmar que se ha hecho algún fetch específico
- Asegurarse que se ha hecho alguna llamada a algo externo (librería o API)
Ejemplo tabla paginación
Hablemos de un ejemplo en concreto. Digamos que nos han pedido que creemos una pantalla nueva para nuestra aplicación, que consistirá en poner una tabla con paginación para mostrar unos datos.
Para este ejemplo ya disponemos del componente tabla y de un componente para hacer la paginación.
Veamos el código de como deberíamos testear este ejemplo:
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import TablaPaginacion from '@/components/TablaPaginacion.vue'; // Componente de la tabla con paginación
import api from '@/api'; // Módulo de llamadas a la API
// Mock de la API
vi.mock('@/api');
describe('TablaPaginacion.vue', () => {
let wrapper: any;
beforeEach(() => {
// Restablecer mocks antes de cada test
vi.resetAllMocks();
});
it('debe mostrar la vista con los datos cargados', async () => {
// Datos de ejemplo para la tabla
const datosMock = [
{ id: 1, nombre: 'Elemento 1' },
{ id: 2, nombre: 'Elemento 2' },
// ...más datos
];
// Configurar el mock de la llamada a la API para que resuelva con datos
api.getDatos.mockResolvedValue({ data: datosMock });
// Renderizar el componente
wrapper = mount(TablaPaginacion);
// Comprobar que la llamada a API se hace
expect(api.getDatos).toHaveBeenCalled();
// Esperar a que se actualice el DOM después de la llamada a la API
await wrapper.vm.$nextTick();
// Comprobar que los datos se muestran correctamente en la tabla
const filas = wrapper.findAll('table tbody tr');
expect(filas).toHaveLength(datosMock.length);
datosMock.forEach((dato, index) => {
expect(filas[index].text()).toContain(dato.nombre);
});
});
it('no debe mostrar datos cuando la llamada a la API falla', async () => {
// Adaptar el entorno para que la llamada a API de error
api.getDatos.mockRejectedValue(new Error('Error de API'));
// Renderizar el componente
wrapper = mount(TablaPaginacion);
// Comprobar que llamada que da error se haga
expect(api.getDatos).toHaveBeenCalled();
// Esperar a que se actualice el DOM después de la llamada fallida
await wrapper.vm.$nextTick();
// Comprobar que se muestra un error
const mensajeError = wrapper.find('.error-message');
expect(mensajeError.exists()).toBe(true);
expect(mensajeError.text()).toBe('Error al cargar los datos.');
// Comprobar que la tabla no muestra datos
const filas = wrapper.findAll('table tbody tr');
expect(filas).toHaveLength(0);
});
describe('Funcionalidad de paginación', () => {
const datosPagina1 = [
{ id: 1, nombre: 'Elemento 1' },
{ id: 2, nombre: 'Elemento 2' },
];
const datosPagina2 = [
{ id: 3, nombre: 'Elemento 3' },
{ id: 4, nombre: 'Elemento 4' },
];
beforeEach(() => {
// Configurar el mock de la API para la primera página
api.getDatos.mockResolvedValueOnce({ data: datosPagina1, total: 4 });
});
it('navega entre la primera y última página mostrando los datos en paginación y loading', async () => {
// Renderizar el componente
wrapper = mount(TablaPaginacion);
// Esperar a que se actualice el DOM después de la llamada a la API
await wrapper.vm.$nextTick();
// Comprobar que los primeros datos se muestran correctamente (datos en la tabla, número de páginas y página actual)
let filas = wrapper.findAll('table tbody tr');
expect(filas).toHaveLength(datosPagina1.length);
expect(wrapper.find('.pagina-actual').text()).toBe('1');
expect(wrapper.find('.numero-paginas').text()).toBe('2');
// Comprobar que el botón de ir hacia adelante esté activado
const botonSiguiente = wrapper.find('.boton-siguiente');
expect(botonSiguiente.attributes('disabled')).toBeUndefined(); // Debe estar habilitado inicialmente
// Pinchamos en el botón hacia adelante
await botonSiguiente.trigger('click');
// Comprobar que los estados de loading se muestran correctamente (skeleton-loader, botones desactivados, etc)
expect(wrapper.find('.skeleton-loader').exists()).toBe(true);
expect(botonSiguiente.attributes('disabled')).toBe('disabled');
const botonAnterior = wrapper.find('.boton-anterior');
expect(botonAnterior.attributes('disabled')).toBe('disabled');
// Configurar el mock de la API para la segunda página
api.getDatos.mockResolvedValueOnce({ data: datosPagina2, total: 4 });
// Esperar a que se actualice el DOM después de la segunda llamada a la API
await wrapper.vm.$nextTick();
// Comprobar que la llamada a la API correspondiente se ha hecho
expect(api.getDatos).toHaveBeenCalledWith(2); // Suponiendo que se pasa el número de página
// Comprobar que los datos se han actualizado (de la tabla y de los números de paginación)
filas = wrapper.findAll('table tbody tr');
expect(filas).toHaveLength(datosPagina2.length);
expect(wrapper.find('.pagina-actual').text()).toBe('2');
// Comprobar que el botón de hacia adelante y el de hacia atrás están activados
expect(botonSiguiente.attributes('disabled')).toBe('disabled'); // Última página, deshabilitado
expect(botonAnterior.attributes('disabled')).toBeUndefined(); // Debe estar habilitado
// Pinchamos en el botón de ir a la primera página
const botonPrimera = wrapper.find('.boton-primera');
await botonPrimera.trigger('click');
// Configurar el mock de la API para la primera página nuevamente
api.getDatos.mockResolvedValueOnce({ data: datosPagina1, total: 4 });
// Esperar a que se actualice el DOM después de la llamada a la API
await wrapper.vm.$nextTick();
// Comprobar los casos/datos correspondientes
filas = wrapper.findAll('table tbody tr');
expect(filas).toHaveLength(datosPagina1.length);
expect(wrapper.find('.pagina-actual').text()).toBe('1');
// Comprobar que el botón de hacia adelante y el de hacia atrás están desactivados/enabled correctamente
expect(botonSiguiente.attributes('disabled')).toBeUndefined(); // Habilitado
expect(botonAnterior.attributes('disabled')).toBe('disabled'); // Primera página, deshabilitado
});
});
it('el botón de volver navega a la /pagina-anterior', async () => {
// Configurar el mock de la API para la primera página
const datosPagina1 = [
{ id: 1, nombre: 'Elemento 1' },
{ id: 2, nombre: 'Elemento 2' },
];
api.getDatos.mockResolvedValueOnce({ data: datosPagina1, total: 4 });
// Renderizar el componente
wrapper = mount(TablaPaginacion);
// Esperar a que se actualice el DOM después de la llamada a la API
await wrapper.vm.$nextTick();
// Comprobar que los datos se muestran correctamente (datos en la tabla)
const filas = wrapper.findAll('table tbody tr');
expect(filas).toHaveLength(datosPagina1.length); // Aún en la primera página
// Pinchamos en el botón de volver a la página anterior
const botonVolver = wrapper.find('.boton-volver-nav');
await botonVolver.trigger('click');
// Comprobar que la ruta ha cambiado como corresponde
expect(wrapper.vm.$route.path).toBe('/pagina-anterior'); // Suponiendo la ruta esperada
// Alternativamente, si usas $router.push, puedes comprobarlo así:
// const router = wrapper.vm.$router;
// expect(router.push).toHaveBeenCalledWith('/pagina-anterior');
});
});
⚠️ Esto no es código completamente funcional, simplemente está pensado para que sea un ejemplo realista sin llegar a ser ideal. Aunque el ejemplo está escrito usando Vue y Vitest, no sería muy diferente si el componente usase React y Jest.
Como puedes ver no es muy diferente lo que haremos por código de lo que haríamos manualmente. La dificultad suele estar en encontrar la manera de comprobar ciertas cosas o estados utilizando la librería de testing correspondiente.
Bugs
Otro caso muy claro para el que deberíamos hacer tests, son los bugs. Es bastante recomendable, que cada vez que arregles un bug, crees un tests para comprobarlo. Esto lo que consigue es que no tropecemos con la misma piedra dos veces, y si en el futuro después de haber estado fuera del proyecto 3 años vuelves para hacer un cambio, no vuelvas a introducir el mismo bug.
El tema es un poco más complicado
El objetivo de este artículo solo es introducir el testing, pero el tema tiene mucha más profundidad, a continuación voy a nombrar de pasada algunos conceptos un poco más avanzados.
Mocks y Stubs ¿Sí o no?
Los mocks y stubs son formas de falsear partes de nuestra aplicación. El objetivo de esto es tener control sobre comportamientos en el entorno de testing para poder probar escenarios y casos especiales como por ejemplo cuando falla una llamada a API. La cuestión suele ser cuando usarlos.
Digamos que en nuestra aplicación utilizamos un servicio que nos devuelve un producto aleatorio para mostrar una ruleta. Para hacer un test que cubra la pantalla que muestra la ruleta, deberemos reemplazar el servicio real, por uno falseado por nosotros que devuelva el producto que necesitemos en cada caso.
De esta manera, nuestros tests que se tiene que encargar de comprobar que la pantalla para mostrar la ruleta funciona utilizarán el servicio falso que hemos creado para cada caso. Pues puede haber casos distintos según el producto o para cubrir los casos de error.
Luego la API que devuelve el producto aleatorio tendrá que tener sus propios tests, pero para probar nuestra pantalla no deberíamos depender de servicios externos ya que podrían ser privados o no estar disponibles siempre.
Los stubs generalmente son lo mismo que los mocks pero en lugar de reemplazar métodos, remplazan componentes enteros. Esto es útil por ejemplo cuando utilizamos librerías de componentes externos y en nuestros tests realmente solo queremos probar nuestra funcionalidad y no que esos componentes funcionen bien internamente.
Falsos positivos
Uno de los errores más comunes cuando empezamos con el testing son los falsos positivos. Escribimos nuestro test completo, lo ejecutamos y funciona. ¿Victoria, verdad? Pues puede que no.
Si solo escribimos el comportamiento que esperamos y ejecutamos el test, no podremos saber si ha funcionado correctamente o simplemente las cosas que debería comprobar no han sido probadas.
Por ejemplo si queremos comprobar que un modal se muestra al hacer click en un botón y en el código estamos comprobando que el elemento del modal existe en la página, puede que nos de positivo porque el modal siempre se encuentra en la página y lo que cambia es su visibilidad. Con lo cual este test aunque parece que está probando algo, siempre da positivo aunque el modal no se muestre.
Por lo tanto debemos tener la regla de que un test que no hemos podido hacer que falle durante el desarrollo, no es un test fiable.
Coverage
El coverage te muestra que partes del código se han ejecutado durante tus tests. Esto es muy útil para saber que partes de la aplicación no tienen tests o para encontrar casos que no estamos cubriendo con nuestros tests.
Pero nos podemos dejar llevar por esto y definir un porcentaje mínimo de coverage para nuestro proyecto y asumir que eso garantiza la confiabilidad de nuestra aplicación. Eso solo nos dará una falsa confianza, ya que en un solo test podemos ejecutar el código de nuestra aplicación al completo, lo importante es que se hagan las comprobaciones correctas.
Cuando NO debería usar tests
Aunque el testing es recomendado en la mayoría de los casos, hay algunas ocasiones donde hacer testing no aporta prácticamente nada.
Por ejemplo si estamos trabajando en alguna prueba de concepto o en algún prototipo que no sepamos a donde va a llegar. El común de estos casos es que no deberías hacer testing sobre cosas que no tienen los requisitos claros o que van a cambiar demasiado rápido.
Ya que para estos casos, perderíamos mucho tiempo haciendo su testing y nuestro objetivo solo es avanzar, no que lo que desarrollemos sea fiable en esta fase.
Otra idea que pueda parecer atractiva en un principio es añadir tests a ese método legacy que lleva funcionando perfectamente durante años, ya que nos lo conocemos perfectamente. Pero cuidado piénsalo bien, ¿Realmente que errores vas a evitar si eso lleva funcionando bien durante todo ese tiempo?
Conclusión
Con este artículo espero haberte convencido aunque sea un poco de lo mucho que nos aportan los tests en el desarrollo. O de haberte aclarado las dudas sobre que tests debería tener tu aplicación.
Como este es un artículo para introducirse en el testing no he explicado algunos conceptos más avanzados y he simplificado algunas cosas para que se entiendan más fácil, ahora que ya entiendes lo básico puedes buscar más información o practicar por ti mismo con este repositorio:
Y recuerda:
Los tests no rompen tu código, rompen la ilusión de que funciona bien 🙂
Créditos
Foto de la portada por Vedrana Filipović en Unsplash
Foto pirámide tests de Medium
Foto pirámide nutricional de Nutrición Comunitaria
Meme: código vs unit tests de Programmer Humor
Meme: pájaro enfadado con galleta de Programmer Humor
Meme: mi experiencia con los tests de Reddit
Meme: tipos de pirámides generado en ImgFlip
Meme: unit testing vs integration testing de Programmer Humor
Meme: señal altura puente de Programmer Humor
Meme: cita final de Programmer Humor