Compartir a través de


Prueba de la integración del SDK de Azure en aplicaciones de JavaScript

Probar el código de integración para el SDK de Azure para JavaScript es esencial para asegurarse de que las aplicaciones interactúan correctamente con los servicios de Azure. En esta guía se muestra cómo probar eficazmente la integración del SDK de Azure en las aplicaciones de JavaScript en un marco de pruebas.

Al decidir si se deben simular llamadas de SDK de servicio en la nube o usar un servicio activo con fines de prueba, es importante tener en cuenta las ventajas entre velocidad, fiabilidad y coste. En este artículo se muestra cómo usar un marco de pruebas para probar la integración del SDK. El código de aplicación inserta un documento en Cosmos DB. El código de prueba simula ese uso de recursos para que no se use el recurso en la nube.

Los marcos usados son:

  • Jest con CommonJs
  • Vitest con ESM
  • Ejecutor de pruebas Node.js con ESM

Requisitos previos

Node.js LTS. El estado de la versión LTS es de "soporte a largo plazo", que suele garantizar la corrección de errores críticos durante un total de 30 meses.

El ejecutor de pruebasNode.js forma parte de la instalación de Node.js.

Precaución

El ejemplo proporcionado para el ejecutor de pruebas de Node.js usa el módulo experimental node:test con mock.fn(). Tenga en cuenta que el ejecutor de pruebas integrado de Node aún no ofrece una API de simulación totalmente compatible. Asegúrese de que la versión del nodo de destino admita las API experimentales o considere la posibilidad de usar una biblioteca ficticia de terceros (o funciones de código auxiliar) en su lugar.

Simulación de servicios en la nube

Ventajas:

  • Acelera el conjunto de pruebas mediante la eliminación de la latencia de red.
  • Proporciona entornos de prueba predecibles y controlados.
  • Es más fácil simular varios escenarios y casos límite.
  • Reduce los costes asociados al uso de servicios en la nube activos, especialmente en pipelines de integración continua.

Inconvenientes:

  • Los simulacros pueden desviarse del SDK real, lo que conduce a discrepancias.
  • Es posible que se omitan determinadas características o comportamientos del servicio en vivo.
  • Entorno menos realista en comparación con el de producción.

Uso de un servicio en vivo

Ventajas:

  • ¿Es un entorno realista que refleja estrechamente la producción?
  • ¿Resulta útil para las pruebas de integración para garantizar que las distintas partes del sistema funcionen juntas?
  • ¿Resulta útil identificar problemas relacionados con la confiabilidad de la red, la disponibilidad del servicio y el control de datos real?

Inconvenientes:

  • Es más lento debido a las llamadas de red.
  • Es más caro debido a posibles costos de uso del servicio.
  • Es complejo y lento configurar y mantener un entorno de servicio activo que coincida con la producción.

La elección entre simular y usar servicios activos depende de la estrategia de pruebas. En el caso de las pruebas unitarias en las que la velocidad y el control son primordiales, la simulación suele ser la mejor opción. En el caso de las pruebas de integración en las que el realismo es fundamental, el uso de un servicio activo puede proporcionar resultados más precisos. El equilibrio de estos enfoques ayuda a lograr una cobertura completa de pruebas a la vez que gestiona los costes y mantiene la eficacia de las pruebas.

Dobles de prueba: simulacros, códigos auxiliares y fakes

Un doble de prueba es cualquier tipo de sustituto utilizado en lugar de algo real con fines de prueba. El tipo de doble que elija se basa en lo que quiere que reemplace. El término simulacro suele ser cualquier doble cuando el término se usa casualmente. En este artículo, el término se utiliza y se ilustra de manera específica en el marco de pruebas de Jest.

Objetos ficticios

Simulacros (también llamados espías): sustituto en una función y poder controlar y espiar el comportamiento de esa función cuando se llama indirectamente por algún otro código.

En los ejemplos siguientes, tiene 2 funciones:

  • someTestFunction: la función que necesita probar. Llama a una dependencia, dependencyFunction, que no ha escrito y no es necesario probar.
  • dependencyFunctionMock: simulación de la dependencia.
import { mock } from 'node:test';
import assert from 'node:assert';

// ARRANGE
const dependencyFunctionMock = mock.fn();

// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// ASSERT
assert.strictEqual(dependencyFunctionMock.mock.callCount(), 1);

El propósito de la prueba es asegurarse de que someTestFunction se comporta correctamente sin invocar realmente el código de dependencia. La prueba valida que se llamó la simulación de la dependencia.

Simulación de dependencias grandes frente a pequeñas

Cuando decida simular una dependencia, puede optar por simular solo lo que necesita, como:

  • Una función o dos de una dependencia mayor. Jest ofrece simulacros parciales para este propósito.
  • Todas las funciones de una dependencia más pequeña, como se muestra en el ejemplo de este artículo.

Códigos auxiliares

El propósito de un código auxiliar es reemplazar los datos devueltos de una función para simular diferentes escenarios. Usas un stub para permitir que tu código llame a la función y reciba varios estados, incluidos los resultados correctos, los errores, las excepciones, y los casos límite. La comprobación de estado garantiza que el código controle estos escenarios correctamente.

import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};

const dependencyFunctionMock = mock.fn();
dependencyFunctionMock.mock.mockImplementation((arg) => {
    return fakeDatabaseData;
});

// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// ASSERT
assert.strictEqual(name, `${fakeDatabaseData.first} ${fakeDatabaseData.last}`);

El propósito de la prueba anterior es asegurarse de que el trabajo realizado por someTestFunction cumple el resultado esperado. En este ejemplo sencillo, la tarea de la función es concatenar los nombres de pila y apellidos. Mediante el uso de datos falsos, conoce el resultado esperado y puede validar que la función funciona correctamente.

Falsificaciones

Los fakes sustituyen una funcionalidad que normalmente no usaría en producción, como el uso de una base de datos en memoria en lugar de una base de datos en la nube.

// fake-in-mem-db.spec.ts
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';

class FakeDatabase {
  private data: Record<string, any>;

  constructor() {
    this.data = {};
  }

  save(key: string, value: any): void {
    this.data[key] = value;
  }

  get(key: string): any {
    return this.data[key];
  }
}

// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
  db.save(key, value);
  return db.get(key);
}

describe('In-Mem DB', () => {
  let fakeDb: FakeDatabase;
  let testKey: string;
  let testValue: any;

  beforeEach(() => {
    fakeDb = new FakeDatabase();
    testKey = 'testKey';
    testValue = {
      first: 'John',
      last: 'Jones',
      lastUpdated: new Date().toISOString(),
    };
  });

  afterEach(() => {
    // Restore all mocks created by node:test’s mock helper.
    mock.restoreAll();
  });

  it('should save and return the correct value', () => {
    // Create a spy on the save method using node:test's mock helper.
    const saveSpy = mock.method(fakeDb, 'save').mock;

    // Call the function under test.
    const result = someTestFunction(fakeDb, testKey, testValue);

    // Verify state.
    assert.deepStrictEqual(result, testValue);
    assert.strictEqual(result.first, 'John');
    assert.strictEqual(result.last, 'Jones');
    assert.strictEqual(result.lastUpdated, testValue.lastUpdated);

    // Verify behavior
    assert.strictEqual(saveSpy.callCount(), 1);
    const calls = saveSpy.calls;
    assert.deepStrictEqual(calls[0].arguments, [testKey, testValue]);
  });
});

El propósito de la prueba anterior es asegurarse de que someTestFunction interactúa correctamente con la base de datos. Mediante el uso de una base de datos falsa en memoria, puede probar la lógica de la función sin depender de una base de datos real, lo que hace que las pruebas sean más rápidas y fiables.

Escenario: Inserción de documentos en Cosmos DB mediante el SDK de Azure

Imagina que tienes una aplicación que necesita escribir un nuevo documento en Cosmos DB si toda la información se envía y verifica. Si se envía un formulario vacío o la información no coincide con el formato esperado, la aplicación no debe escribir los datos.

Cosmos DB se usa como ejemplo, pero los conceptos se aplican a la mayoría de los SDK de Azure para JavaScript. La función siguiente captura esta funcionalidad:

// insertDocument.ts
import { Container } from '../data/connect-to-cosmos.js';
import type {
  DbDocument,
  DbError,
  RawInput,
  VerificationErrors,
} from '../data/model.js';
import Verify from '../data/verify.js';

export async function insertDocument(
  container: Container,
  doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
  const isVerified: boolean = Verify.inputVerified(doc);

  if (!isVerified) {
    return { message: 'Verification failed' } as VerificationErrors;
  }

  try {
    const { resource } = await container.items.create({
      id: doc.id,
      name: `${doc.first} ${doc.last}`,
    });

    return resource as DbDocument;
  } catch (error: any) {
    if (error instanceof Error) {
      if ((error as any).code === 409) {
        return {
          message: 'Insertion failed: Duplicate entry',
          code: 409,
        } as DbError;
      }
      return { message: error.message, code: (error as any).code } as DbError;
    } else {
      return { message: 'An unknown error occurred', code: 500 } as DbError;
    }
  }
}

Nota:

Los tipos de TypeScript ayudan a definir los tipos de datos que usa una función. Aunque no necesita TypeScript para usar Jest u otros marcos de pruebas de JavaScript, es esencial para escribir JavaScript seguro para tipos.

Las funciones de esta aplicación son:

Función Descripción
insertDocument Inserta un documento en la base de datos. Esto es lo que queremos probar.
inputVerified Comprueba los datos de entrada en un esquema. Garantiza que los datos tienen el formato correcto (por ejemplo, direcciones de correo electrónico válidas, direcciones URL con formato correcto).
cosmos.items.create Función de SDK para Azure Cosmos DB que usa el @azure/cosmos. Esto es lo que queremos simular. Ya tiene sus propias pruebas mantenidas por los propietarios del paquete. Es necesario comprobar que la llamada a la función de Cosmos DB se realizó y devolvió datos si los datos entrantes pasaron la comprobación.

Instalación de la dependencia del marco de pruebas

Este marco se proporciona como parte de Node.js LTS.

Configuración del paquete para ejecutar la prueba

Actualice el package.json para la aplicación con un nuevo script para probar nuestros archivos de código fuente. Los archivos de código fuente se definen mediante la coincidencia en el nombre de archivo parcial y la extensión. El ejecutor de pruebas busca archivos que siguen la convención de nomenclatura común para los archivos de prueba: <file-name>.spec.[jt]s. Este patrón significa que los archivos denominados como los ejemplos siguientes se interpretan como archivos de prueba y los ejecuta el ejecutor de pruebas:

  • * .test.js: por ejemplo, math.test.js
  • * .spec.js: por ejemplo, math.spec.js
  • Archivos ubicados en un directorio de pruebas, como tests/math.js

Agregue un script al package.json para admitir ese patrón de archivo de prueba con el ejecutor de pruebas:

"scripts": {
    "test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}

Configuración de pruebas unitarias para Azure SDK

¿Cómo podemos usar mocks, stubs y fakes para probar la función insertDocument?

  • Simulacros: necesitamos un simulacro para asegurarnos de que el comportamiento de la función se prueba como:
    • Si los datos pasan la comprobación, la llamada a la función de Cosmos DB se produjo solo 1 vez.
    • Si los datos no pasan la comprobación, no se produjo la llamada a la función de Cosmos DB.
  • Códigos auxiliares:
    • Los datos pasados coinciden con el nuevo documento retornado por la función.

Al realizar pruebas, piense en la configuración de la prueba, la propia prueba y la comprobación. En términos de vernáculo de prueba, esta funcionalidad usa los siguientes términos:

  • Organizar: configurar las condiciones de prueba
  • Actuar: llamar a la función para probar, también conocida como el sistema sometido a prueba o SUT
  • Aseverar: validar los resultados. Los resultados pueden ser comportamiento o estado.
    • El comportamiento indica la funcionalidad de la función de prueba, que se puede comprobar. Un ejemplo es que se llamó a alguna dependencia.
    • El estado indica los datos devueltos de la función.
import { describe, it, afterEach, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

describe('boilerplate', () => {
  beforeEach(() => {
    // Setup required before each test
  });
  afterEach(() => {
    // Cleanup required after each test
  });

  it('should <do something> if <situation is present>', async () => {
    // Arrange
    // - set up the test data and the expected result
    // Act
    // - call the function to test
    // Assert
    // - check the state: result returned from function
    // - check the behavior: dependency function calls
  });
});

Cuando se usan simulaciones en las pruebas, ese código de plantilla debe utilizar simulaciones para probar la función sin llamar a la dependencia subyacente empleada en la función, como las bibliotecas cliente de Azure.

Creación del archivo de prueba

El archivo de prueba con simulacros, para simular una llamada a una dependencia, tiene una configuración adicional.

Hay varias partes en el archivo de prueba:

  • import: las instrucciones import permiten usar o simular cualquiera de las pruebas.
  • mock: cree el comportamiento ficticio predeterminado que desee. Cada prueba puede modificarse según sea necesario.
  • describe: Familia del grupo de prueba para el archivo insert.ts.
  • it: cada prueba para el archivo insert.ts.

El archivo de prueba cubre tres pruebas para el archivo insert.ts, que se pueden dividir en dos tipos de validación:

Tipo de validación Prueba
Ruta feliz: should insert document successfully El método simulado de la base de datos fue llamado y devolvió los datos alterados.
Ruta de error: should return verification error if input is not verified Los datos no pudieron validarse y devolvieron un error.
Ruta de error: should return error if db insert fails Se llamó al método de base de datos simulado y devolvió un error.

El siguiente archivo de prueba muestra cómo probar la función insertDocument .

// insertDocument.test.ts
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';

import { Container } from '../src/data/connect-to-cosmos.js';
import { createTestInputAndResult } from '../src/data/fake-data.js';
import type { DbDocument, DbError, RawInput } from '../src/data/model.js';
import { isDbError, isVerificationErrors } from '../src/data/model.js';

import Verify from '../src/data/verify.js';
import CosmosConnector from '../src/data/connect-to-cosmos.js';
import { insertDocument } from '../src/lib/insert.js';

describe('SDK', () => {
  beforeEach(() => {
    // Clear all mocks before each test
    mock.restoreAll();
  });

  it('should return verification error if input is not verified', async () => {
    const fakeContainer = {
      items: {
        create: async (_: any) => {
          throw new Error('Create method not implemented');
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => false);

    const mGetUniqueId = mock.method(CosmosConnector, 'getUniqueId').mock;
    mGetUniqueId.mockImplementation(() => 'unique-id');

    const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;

    // Arrange: wrong shape of document on purpose.
    const doc = { name: 'test' } as unknown as RawInput;

    // Act:
    const insertDocumentResult = await insertDocument(fakeContainer, doc);

    // Assert - State verification.
    if (isVerificationErrors(insertDocumentResult)) {
      assert.deepStrictEqual(insertDocumentResult, {
        message: 'Verification failed',
      });
    } else {
      throw new Error('Result is not of type VerificationErrors');
    }

    // Assert - Behavior verification: Verify that create was never called.
    assert.strictEqual(mContainerCreate.callCount(), 0);
  });
  it('should insert document successfully', async () => {
    // Arrange: override inputVerified to return true.
    const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
      createTestInputAndResult();

    const fakeContainer = {
      items: {
        create: async (doc: any) => {
          return { resource: result };
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => true);

    const mContainerCreate = mock.method(
      fakeContainer.items as any,
      'create',
    ).mock;
    mContainerCreate.mockImplementation(async (doc: any) => {
      return { resource: result };
    });

    // Act:
    const receivedResult = await insertDocument(fakeContainer, input);

    // Assert - State verification: Ensure the result is as expected.
    assert.deepStrictEqual(receivedResult, result);

    // Assert - Behavior verification: Ensure create was called once with correct arguments.
    assert.strictEqual(mContainerCreate.callCount(), 1);
    assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
      id: input.id,
      name: result.name,
    });
  });
  it('should return error if db insert fails', async () => {
    // Arrange: override inputVerified to return true.
    const { input, result } = createTestInputAndResult();
    const errorMessage: string = 'An unknown error occurred';

    const fakeContainer = {
      items: {
        create: async (doc: any): Promise<any> => {
          return Promise.resolve(null);
        },
      },
    } as unknown as Container;

    const mVerify = mock.method(Verify, 'inputVerified').mock;
    mVerify.mockImplementation(() => true);

    const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
    mContainerCreate.mockImplementation(async (doc: any) => {
      const mockError: DbError = {
        message: errorMessage,
        code: 500,
      };
      throw mockError;
    });

    // Act:
    const insertDocumentResult = await insertDocument(fakeContainer, input);

    // // Assert - Ensure create method was called once with the correct arguments.
    assert.strictEqual(isDbError(insertDocumentResult), true);
    assert.strictEqual(mContainerCreate.callCount(), 1);
    assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
      id: input.id,
      name: result.name,
    });
  });
});

Solución de problemas

La mayoría del código de este artículo procede del repositorio de GitHub MicrosoftDocs/node-essentials . Si desea insertar en un recurso de Cosmos DB Cloud, cree el recurso con este script.

Información adicional