Testing automatizado — Javascript

Testing automatizado — Javascript

Testing unitario sobre frameworks de JS

Para comenzar con el testing automatizados sobre aplicaciones desarrolladas con Javascript, utilizaremos JEST.

JEST es un framework de testing que funciona junto a muchos otros frameworks, como Angular, React, Express, Nest, entre otros.

Si quieres saber con más detalle de los distintos tipos de pruebas automatizadas y como conviven con un proyecto de software, te invito a ver el siguiente enlace:

Testing automatizado — Iniciando

Antes de comenzar, vamos a ver un poco de términos claves que debes conocer.

Términos claves

Mock: Son simulaciones que nos permiten sustituir funcionalidades con datos falsos con el fin de realizar pruebas sin afectar el funcionamiento real de la aplicación (Ejemplo: persistencia)

**Definición de contextos:
**- Ficheros: Los archivos de pruebas podemos ubicarlos de las formas .test.js o .spec.ts (usando typescript).
- Contextos: Podemos agrupar pruebas con la palabra reservada “describe”.

Gestión de estados
-
before: Acciones a realizar antes de algúna ejecución de código. (beforeEach, beforeAll, etc)
- after: Acciones a realizar despues de alguna ejecución de código (afterEach, afterAll, etc).

**Matchers
**Funciones utilizadas para comprobar si el valor esperado coincide con el obtenido.

**Expect
**Es la función que permite comprobar y comparar algún valor

Pruebas
Para la definición de pruebas se encuentran las sentencias “it” o “test”

Cobertura
Permite visualizar a nivel de %, el alcance de las pruebas que estamos realizando.

Un ejemplo de cada uno de estos terminos claves los podemos ver en el siguiente código:

COMPARACIONES

Ahora que ya sabemos o recordamos los terminos generales del testing automatizando usando JEST, comenzaremos a crear nuestras primeras pruebas, pero probablemente tengamos preguntas como ¿ahora como pruebo? o ¿qué debo probar?, para esto veremos como comparar valores.

Igualdad

Nos permite comparar un valor obtenido con el esperado. En el siguiente ejemplo vemos como el valor obtenido de la operación “2+2” esperemos que sea “4”.

expect(2 + 2).toBe(4);

Adicionalmente, podemos comparar el opuesto de la forma:

expect( 2 + 2).not.toBe(5);

Objetos

Una comparación no solo puede ser a nivel de operaciones o variables con un valor determinado, sino que tambien objetos o instancias de distintas clases.

const example = {test: 'Test 1', testing: 'Test 2'};expect(example).toEqual({test: 'Test 1', testing: 'Test 2'});

Listas

La comparación de listas puede validarse en base a una comparación completa (todos sus elementos) con funciones como toEqual() o bien que contenga elementos de la forma toContain():

const example = ['Test 1', 'Test 2', 'Test 3'];expect(example).toContain('Test 2');

Expresiones regulares

Dado que en los testing lo más importante no son los datos obtenidos sino que como trabajan esto dentro del sistema, se pueden usar expresiones regulares para validar contenidos generales.

expect('Testing').toMatch(/sti/);

Excepciones

Obviamente como buenos desarrolladores, tendremos controlados cada uno de los posibles errores que arroje nuestra aplicación. Con esto podremos realizar pruebas a casos de errores (controlados).

const errorValue = () => {
   throw new Error('Error testing');
};expect(errorValue).toThrow();
expect(errorValue).toThrow(Error);

CASOS DE PRUEBAS

Para poder hacer el testing, sobre todo en front end, tenemos que saber que experiencia buscamos para el usuario final. Al crear un componente nuevo es importante considerar como esperamos que el usuario interactue con el, que posibilidades tendrá, si hace un acción sobre este, que desencadenará y con qué estará relacionado. Teniendo esto en mente, será mucho más facil probar nuestro componente.

Pongamonos en un caso: Una aplicación que se encarga de tener un listado de tareas “TODOs” las cuales puedo ir creando, finalizando, volviendo atrás o eliminando, cada una de estas se encontrará en un “div” diferente según el estado en el que se encuentre y claramente, contaremos con un formulario que permite “crear” tarjetas nuevas.

Ahora, veamos un caso de angular como valido que al presionar el botón de crear, este agregue una tarea nueva en el listado de tareas pendientes. A continuación vemos un ejemplo realizado en angular:

it(`testing add todo`, async function () {        const testingTodo = {      
        task: 'to do testing',      
        id: 'testingid',      
        complete: false,    
    };     
    const [todoForm] = findComponent(fixture, 'app-todo-form');
    const [addButton] = findComponent(fixture, '#add-button');     /**
     * Triggering the event toDoChange in toDoForm which is an observable
     * provoques that app.component add that change in partialTodo state variable
  */
    todoForm.triggerEventHandler('toDoChange', testingTodo);
     fixture.detectChanges();     expect(component.partialTodo).toBe(testingTodo);

     /**
     * Clicking the addButton provoques that app.component move the partialTodo
     * into the incompleteTodo list with completed field as false.
     * That change reflects in the DOM as an unchecked input checkbox
  */
     addButton.triggerEventHandler('click', null); 
     fixture.detectChanges();

    expect(component.incompleteToDos.length).toBe(1);
});

Lo mismo se puede realizar trabajando en React:

test("testing add todo", async () => {
  /**
   * Render de app with http service mocked with a successfull response
   */
  render(<App http={httpMock} />, container);
   const input = screen.getByTestId("input").querySelector('input[type="text"]');  const addToDo = screen.getByTestId("add-todo");
   /**
   * Clicking addTodo button without write a todo before
   * dont add any todo to the incompletedList
   */   act(() => {
    fireEvent.click(addToDo);
  });   const [emptyIncompleteList] = screen.getAllByRole("list");
   expect(emptyIncompleteList.childElementCount).toEqual(0);
   /**
   * Creating an event of change in the input
   * and clicking the addButton provoques
   * that an unchecked todo is added to incompleteTodo list
   */  act(() => {
    fireEvent.change(input, { target: { value: "to do testing" } });
  });   act(() => {
    fireEvent.click(addToDo);
  });
   const [incompleteList, completeList] = screen.getAllByRole("list");   expect(incompleteList.childElementCount).toEqual(1);
  expect(completeList.childElementCount).toEqual(0);});

Si quieres ver estos ejemplos completos dentro de la aplicación, puedes bajar su código de github accediendo a los enlaces al final de esta publicación.

Ahora que sucede si queremos hacer un mock de un servicio para simular la respuesta que este lanzará. Usaremos el ejemplo que entrega Nest con un controlador de “Cats”.

import ...describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
      providers: [
        CatsService,
        {
          provide: 'CAT_MODEL',
          useValue: Cat,
        },
      ],
    }).compile();catsService = moduleRef.get<CatsService>(CatsService);
    catsController = moduleRef.get<CatsController>(CatsController);
  });describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result: Cat[] = [
        { name: 'simba', age: 1, breed: 'ginger' },
      ] as Cat[];
      jest.spyOn(catsService, 'findAll').mockResolvedValue(result);expect(await catsController.findAll()).toBe(result);
    });it('should throw an exception if not found the cat ', async () => {
      const foundedId = 'not-exist-id';jest.spyOn(catsService, 'findOne').mockResolvedValue(null);
      try {
        await catsController.findOne(foundedId);
      } catch (err) {
        expect(err).toBeInstanceOf(HttpException);
      }
    });it('should call create function with the same valor of the request', async () => {
      const request = { name: 'testing-cat', breed: 'test', age: 1 };const spyOnCreate = jest
        .spyOn(catsService, 'create')
        .mockResolvedValue(null);
      await catsController.create(request);
      expect(spyOnCreate).toBeCalledWith(request);
    });
  });
});

En este ejemplo contamos con una nueva función de JEST, que es “jest.spyOn(…)”, esta función permite hacer un mock de cualquier servicio o función, indicando como queremos que se comporte esta. Del ejemplo anterior vemos el siguiente caso:

it('should return an array of cats', async () => {
    const result: Cat[] = [
        { name: 'simba', age: 1, breed: 'ginger' },
      ] as Cat[];
      jest.spyOn(catsService, 'findAll').mockResolvedValue(result);    expect(await catsController.findAll()).toBe(result);
});

Acá podemos ver que al ajecutar la api “findAll()” del controlador catsController, esperamos que su resultado sea el array creado. Sin embargo, como es una API genérica, no sabemos lo que resolverá, ahi es donde nuestro spyOn soluciona el juego. Esté indica que cuando se ejecute el “catsService” en su método “findAll” (esto se llama dentro de catsController.findAll()”), queremos que resuelva “result”, osea que devuelva el array creado más arriba.

Con este spyOn el servicio no se ejecutará, inmediatamente resolverá lo que nosotros indicamos con nuestro spyOn. Así es como podemos “engañar” a nuestra base de datos, repositorios externos, sistemas integrados, etc., sin peligrar de afectar la persistencia de la información.

TIPS ADICIONALES

Te dejo algunas recomendaciones que puedes considerar en tu proyecto.

Repositorio

Puedes configurar tu proyecto y personalizar tus commits & push de código con hooks que permiten correr comandos y validaciones antes de lanzarlos. Un ejemplo de esto es Husky, el que te permite lanzar tus test antes de cada commit, asi te aseguras de que el código que estas subiendo, no afecta al resto del sistema.

Watch mode

Existe el comando “npm run test:watch” el cual va ejecutando las pruebas afectadas a medidas que vas actualizando el código, de esta forma no se lanza el testing completo cada vez.

Extensiones Code

Si trabajas con el IDE “Code” existen muchas extensiones que permiten llevar de forma más gráfica y funcional tus testing. Las extensiones “Jest” y “Jest runner” te simplificaran la vida.

EJEMPLOS

Te dejo algunos ejemplos para que puedas practicar o ver diferentes tipos de pruebas, proyectos que fueron im

Frontend

Angular: https://github.com/jpsaldivar/example-angular-todo
React: https://github.com/jpsaldivar/example-react-todo

Backend

Nest: https://github.com/jpsaldivar/example-nest-testing