imagen

Testing : Implementación mock de la clase Date

El objetivo es testear esta función con diferentes inputs para verificar que el retorno es el esperado.

// Subject under test - SUT (lo que queremos probar)
"use strict";

exports.calculateExpirationTime = ({ monthsToExpire = null }) => {
  if (!monthsToExpire) throw new Error("monthsToExpire not found");
  const now = new Date();
  const expirationDateTimeUTC = now.setMonth(now.getMonth() + monthsToExpire);
  /** fecha UNIXTIME que el registro sera eliminado de la DB */
  const expirationTime = Math.floor(
    new Date(expirationDateTimeUTC).getTime() / 1000
  );
  return expirationTime;
};

Dado que en la línea 5 estamos creando una nueva instancia de la fecha y hora actual (momento en el que se ejecuta la función) el valor retornado por nuestra función dependerá directamente del valor de nuestra variable now de la línea 5.

Aquí es donde entran nuestros mock/stub/spy [ref1] [ref2], el nombre puede variar según la librería/framework de testing que estemos usando, pero a grandes rasgos son objetos que nos permiten simular un comportamiento/respuesta de un método o función.

Dado que nuestro objetivo es probar el comportamiento de la función calculateExpirationTime pero esta depende internamente de la respuesta new Date() para poder verificar el resultado, necesitamos conocer este valor (new Date()) y para poder lograr eso, debemos "forzar" que cuando hagamos el test de nuestra función calculateExpirationTime el llamado a new Date() retorne un valor que vamos a definir previamente y así poder conocer el valor esperado (expected) de nuestra función que estamos testetando (SUT) y poder validar que el valor retornado sea igual al valor esperado.

Dado que estamos en el universo de JS, hoy nos ayuda nuestro amigo JEST que nos proveera de algunas herramientas básicas para hacer nuestro test.

Primero definiremos el valor que queremos "forzar" cuando se llame a new Date()

const mockDateObject = new Date("2021-01-01T03:00:00.000Z");

Luego usando el método spyOn [ref] y mockImplementation "forzaremos" a nuestro SUT que cuando se llame al método "Date" del objeto "Global" obtendremos como respuesta nuestro mockDateObject

const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);

Con lo anterior, ya podemos armar nuestro primer test

test("Expira en 6 meses", async () => {
  // Objeto mock (respuesta que dara el new date dentro de la función
  const mockDateObject = new Date("2021-01-01T03:00:00.000Z");
  
  // Usamos spyOn para crear nuestro Mock
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  
  // Llamamos a nuestro SUT
  const expirationTime = calculateExpirationTime({ monthsToExpire: 6 });
  
  // Verificamos que la respuesta es el valor esperado
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2021-07-01T04:00:00.000Z"
  );
  
  // En caso de generar mas test, sera importante hacer un restore
  // del spy en cada caso de prueba para no afectar los demas test
  // Ya que estamos haciendo un mock de un método de global
  spy.mockRestore();
});

Finalmente nuestro set de pruebas "podría" verse algo como esto

"use strict";

process.env.TZ = "America/Santiago";

const { calculateExpirationTime } = require("../index");

test("Verifica Error con parametro incorrecoto", async () => {
  const mockDateObject = new Date("2021-01-01T03:00:00.000Z");
  jest.spyOn(global, "Date").mockImplementation(() => mockDateObject);
  const wrapperFunction = () => calculateExpirationTime({ monthsToExpire: 0 });
  expect(wrapperFunction).toThrow(new Error("monthsToExpire not found"));
});

test("Expira en 6 meses", async () => {
  const mockDateObject = new Date("2021-01-01T03:00:00.000Z");
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  const expirationTime = calculateExpirationTime({ monthsToExpire: 6 });
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2021-07-01T04:00:00.000Z"
  );
  spy.mockRestore();
});

test("Expira en 12 meses", async () => {
  const mockDateObject = new Date("2021-01-01T03:00:00.000Z");
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  const expirationTime = calculateExpirationTime({ monthsToExpire: 12 });
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2022-01-01T03:00:00.000Z"
  );
  spy.mockRestore();
});

test("Expira en 18 meses", async () => {
  const mockDateObject = new Date("2021-01-01T03:00:00.000Z");
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  const expirationTime = calculateExpirationTime({ monthsToExpire: 18 });
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2022-07-01T04:00:00.000Z"
  );
  spy.mockRestore();
});

test("Expira en 24 meses", async () => {
  const mockDateObject = new Date("2021-01-01T03:00:00.000Z");
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  const expirationTime = calculateExpirationTime({ monthsToExpire: 24 });
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2023-01-01T03:00:00.000Z"
  );
  spy.mockRestore();
});

test("Fecha actual de verano y expira en invierno", async () => {
  const mockDateObject = new Date("2021-02-15T03:00:00.000Z");
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  const expirationTime = calculateExpirationTime({ monthsToExpire: 6 });
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2021-08-15T04:00:00.000Z"
  );
  spy.mockRestore();
});

test("Fecha actual de invierno y expira en verano", async () => {
  const mockDateObject = new Date("2021-07-18T04:00:00.000Z");
  const spy = jest
    .spyOn(global, "Date")
    .mockImplementation(() => mockDateObject);
  const expirationTime = calculateExpirationTime({ monthsToExpire: 6 });
  expect(new Date(expirationTime).toISOString()).toEqual(
    "2022-01-18T03:00:00.000Z"
  );
  spy.mockRestore();
});

En caso que necesitar hacer un mock de un método de la clase Date, por ejemplo Date.now() lo podemos hacer así:

const spy = jest
        .spyOn(Date, "now")
        .mockImplementation(() => "valor a retornar");