Joan Antoni Morey
8 min
05/09/2022
Escoger con qué empezar es importante, ya que puedes estar eligiendo un camino eficiente y progresivo o, por otra parte, tener que hacer un gran esfuerzo en aprender mucho más de lo que vas a necesitar en la práctica.
Es importante estar familiarizado con el concepto "Test" y por qué deberías empezar a hacer tests (si no los haces ya). Si para tí son conceptos nuevos te recomiendo leer primero mi anterior artículo ¿Por qué debes empezar a hacer tests?
Como en muchos aspectos de la vida y la programación, es mejor ir de menos a más. Vale más la pena empezar con tests pequeños que se encarguen de funciones pequeñas y/o simples.
Dicho esto, vamos a ver los tests de más bajo nivel y que se encargan de fragmentos muy pequeños de código, los llamados tests unitarios.
Son tests que evalúan funciones concretas y aisladas, aunque no hay un máximo de complejidad que tenga que tener una función para poder ser testeada con este tipo de tests.
Es de buena práctica fragmentar en pequeños bloques el código y dedicar tests a cada fragmento. Así resulta mucho más fácil y rápido detectar si hay un fallo y en qué parte del código se origina. De lo contrario, si tienes funciones demasiado grandes y/o complejas, puede que al fallar solo puedas saber que hay algo que hace que toda la función falle. Como se suele decir, divide y vencerás.
Este es un ejemplo de un test unitario en JavaScript:
getNameById
function getNameById(users, id) {
const user = users.find((user) => user.id === id);
return user.name;
}
describe("given an array of users", () => {
describe("given the 'id' of the desired user", () => {
it("should return the name of that user", () => {
const users = [
{ id: 1, name: "Emily" },
{ id: 2, name: "John" },
];
expect(getNameById(users, 2)).toEqual("John");
});
});
});
Como podemos observar, la función getNameById
es bastante sencilla y directa, con lo que podemos implementar, muy rápidamente, un test para comprobar su funcionamiento. De esta forma podemos comprender cada pequeña parte de nuestro proyecto y generar más y más confianza.
Aunque a simple vista puedan parecer tests triviales, simples y obvios, pueden llegar a alcanzar mucha complejidad al elaborar y contemplar muchos casos de uso. Un ejemplo de ello, con getNameById
, sería el caso en el que solo le pasas un argumento en lugar de los dos que espera, o si le pasas una lista vacía de usuarios, o un listado de usuarios sin la propiedad id
...
Es entonces cuando empiezan a ganar importancia estos tests.
Por ejemplo, si ejecutamos este otro test salta el siguiente error:
describe("given an empty array and the 'id' of the desired user", () => {
it("should return the name of that user", () => {
expect(getNameById([], 2)).toEqual("John");
});
});
TypeError: Cannot read properties of undefined (reading 'name')
Con lo que tenemos que modificar la función para que no lance un error cuando recibe una colección de elementos vacía.
Nos quedaría así:
function getNameById(users, id) {
const user = users?.find((user) => user.id === id);
return user?.name;
}
describe("given an empty array and the 'id' of the desired user", () => {
it("should return the name of that user", () => {
expect(getNameById([], 2)).toEqual(undefined);
});
});
Al contrario que los tests unitarios, los tests E2E se encargan de conjuntos de funcionalidades. Más concretamente comprueban si las interacciones del usuario con la app son y serán como se espera. Necesitan renderizar pantallas completas de la app, tal y como las vería un usuario, e interactuar con los elementos.
Un ejemplo de un test E2E que no puede faltar es el que comprueba que un usuario pueda iniciar sesión. Para ello el test renderiza la pantalla del formulario, rellena los campos con la información adecuada y pulsa el botón "Iniciar sesión". Si todo es correcto, el test será satisfactorio. En caso contrario, pueden haber fallado una o varias cosas. Puede ser que el botón de "Iniciar sesión" quede fuera de la pantalla, con lo que un usuario no podría apretarlo y el flujo se rompería.
De ser así, es infinitamente mejor detectarlo en un test E2E que dejar de tener usuarios en la app por el simple hecho de que un botón no se vea para poder pulsarlo.
Antiguamente era tedioso poder implementar tests E2E, pero hoy en día hay librerías muy avanzadas que mejoran mucho la experiencia de desarrollo. En JavaScript, por ejemplo, contamos con la librería Cypress.js.
Así se vería el código de un test de interacción con formularios en Cypress.js:
describe("Form Interactions", function () {
beforeEach(function () {
cy.viewport(400, 300);
cy.visit("/index.html");
});
it("updates range value when moving slider", function () {
cy.get("input[type=range]").as("range").invoke("val", 25).trigger("change");
cy.get("@range").siblings("p").should("have.text", "25");
});
});
Al ejecutar la suite de tests, esta abre una ventana del navegador en la que renderizará y ejecutará cada test. Resulta muy intuitivo, visual e interactivo.
Ejemplo sacado de la documentación de Cypress.js
El principal motivo es el de mantener el correcto funcionamiento de los flujos de la app, a lo largo del tiempo de vida del proyecto y no solo en una fase específica. No solo la experiencia de usuario permanece intacta (o casi) aunque la app atraviese diferentes etapas y evoluciones, sino que la experiencia de desarrollo también se ve beneficiada.
Igual que ocurre con los tests unitarios, cuantos más tests haya más te cubres las espaldas cuando necesitas modificar parte del código o implementar algo nuevo. A parte, en los tests E2E, hay otro factor importante que inclina la balanza a un SÍ con mayúsculas; plasmar los requerimientos de usuario en el mismo código del test.
Puede parecer algo que, claramente es un punto a favor, pero que tampoco es para tanto. Pues resulta ser muy práctico a la hora de saber por qué el código es tan complejo, pudiéndose hacer más sencillo, y es una forma de tener en cuenta todos los principales casos de uso que tiene que satisfacer cada pantalla de la app. Asimismo ayuda a formular preguntas sobre posibles requerimientos que alomejor no se han tenido en cuenta hasta el momento o simplemente nadie ha recopilado los requerimientos de usuario, cosa que puede llegar a ser crítica llegado el día de la presentación de la app.
Este tipo de tests requiere más tiempo de ejecucón ya que, como comentábamos, cada uno de ellos tiene que renderizar pantallas. Esto es, sin duda alguna, mucho más lento que los tests unitarios. Aún así, son completamente complementarios a los tests unitarios y, en mi opinión, imprescindibles para una app sin flujos rotos y, por tanto, consistente en el tiempo.
Empieza poco a poco con los tipos de tests, ya que con tests unitarios y E2E puedes alcanzar un coverage muy elevado y garantizar, con seguridad, el correcto funcionamiento e integración de tu código.