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.

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.

Requisitos previos

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 activo.
  • Entorno menos realista en comparación con el de producción.

Uso de un servicio en vivo

Ventajas:

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

Inconvenientes:

  • Más lento debido a las llamadas de red.
  • Más caro debido a posibles costes de uso del servicio.
  • Configurar y mantener un entorno de servicio activo que coincida con el de producción es complejo y lento.

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 usa específicamente e ilustra específicamente 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: esta es la función que necesita probar. Llama a una dependencia, dependencyFunction, que no ha escrito y no es necesario probar.
  • dependencyFunctionMock: se trata de una simulación de la dependencia.
// setup
const dependencyFunctionMock = jest.fn();

// perform test
// Jest replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()

// verify behavior
expect(dependencyFunctionMock).toHaveBeenCalled();

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 los códigos auxiliares es reemplazar los datos devueltos de una función para simular diferentes escenarios. Esto permite que el 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.

// ARRANGE
const dependencyFunctionMock = jest.fn();
const fakeDatabaseData = {first: 'John', last: 'Jones'};
dependencyFunctionMock.mockReturnValue(fakeDatabaseData);

// ACT
// date is returned by mock then transformed in SomeTestFunction()
const { name } = someTestFunction()

// ASSERT
expect(name).toBe(`${first} ${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.

Fakes

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
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);
}

// Jest test suite
describe('someTestFunction', () => {
  let fakeDb: FakeDatabase;
  let testKey: string;
  let testValue: any;

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

    // Spy on the save method
    jest.spyOn(fakeDb, 'save');
  });

  afterEach(() => {
    // Clear all mocks
    jest.clearAllMocks();
  });

  test('should save and return the correct value', () => {
    // Perform test
    const result = someTestFunction(fakeDb, testKey, testValue);

    // Verify state
    expect(result).toEqual(testValue);
    expect(result.first).toBe('John');
    expect(result.last).toBe('Jones');
    expect(result.lastUpdated).toBe(testValue.lastUpdated);

    // Verify behavior
    expect(fakeDb.save).toHaveBeenCalledWith(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 un documento en Cosmos DB mediante el SDK de Azure

Imagine que tiene una aplicación que necesita escribir un nuevo documento en Cosmos DB si toda la información se envía y comprueba. 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';
import {
  DbDocument,
  DbError,
  RawInput,
  VerificationErrors,
} from '../data/model';
import { inputVerified } from '../data/verify';

export async function insertDocument(
  container: Container,
  doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
  const isVerified: boolean = 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 la aplicación anterior 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

En este artículo se usa Jest como marco de pruebas. Hay otros marcos de pruebas comparables que también puede usar.

En la raíz del directorio de aplicaciones, instale Jest con el siguiente comando:

npm install jest

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. Jest busca archivos siguiendo 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 interpretarán como archivos de prueba y se ejecutarán mediante Jest:

  • *.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 a package.json para admitir ese patrón de archivo de prueba con Jest:

"scripts": {
    "test": "jest dist",
}

El código fuente de TypeScript se genera en la subcarpeta dist. Jest ejecuta los archivos .spec.js encontrados en la subcarpeta dist.

Configuración de pruebas unitarias para Azure SDK

¿Cómo podemos usar objetos ficticios, códigos auxiliares 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 devuelto 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, esto se conoce como:

  • 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.

Jest, similar a otros marcos de pruebas, tiene un archivo de prueba reutilizable para definir el archivo de prueba.

// boilerplate.spec.ts

describe('nameOfGroupOfTests', () => {
  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
  });
});

Al usar simulacros, ese lugar reutilizable debe usar el simulacro para probar la función sin llamar a la dependencia subyacente usada 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 extra adicional al código reutilizable de prueba común. A continuación hay varias partes en el archivo de prueba:

  • import: las instrucciones de importación permiten usar o simular cualquiera de las pruebas.
  • jest.mock: cree el comportamiento ficticio predeterminado que desee. Cada prueba puede modificarse según sea necesario.
  • describe: pruebe la familia de grupos para el archivo insert.ts.
  • test: 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 Se llamó al método de base de datos simulado y devolvió los datos modificados.
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 Jest muestra cómo probar la función insertDocument.

// insertDocument.test.ts
import { Container } from '../data/connect-to-cosmos';
import { createTestInputAndResult } from '../data/fake-data';
import {
  DbDocument,
  DbError,
  isDbError,
  isVerificationErrors,
  RawInput,
} from '../data/model';
import { inputVerified } from '../data/verify';
import { insertDocument } from './insert';

// Mock app dependencies for Cosmos DB setup
jest.mock('../data/connect-to-cosmos', () => ({
  connectToContainer: jest.fn(),
  getUniqueId: jest.fn().mockReturnValue('unique-id'),
}));

// Mock app dependencies for input verification
jest.mock('../data/verify', () => ({
  inputVerified: jest.fn(),
}));

describe('insertDocument', () => {
  // Mock the Cosmo DB Container object
  let mockContainer: jest.Mocked<Container>;

  beforeEach(() => {
    // Clear all mocks before each test
    jest.clearAllMocks();

    // Mock the Cosmos DB Container create method
    mockContainer = {
      items: {
        create: jest.fn(),
      },
    } as unknown as jest.Mocked<Container>;
  });

  it('should return verification error if input is not verified', async () => {
    // Arrange - Mock the input verification function to return false
    jest.mocked(inputVerified).mockReturnValue(false);

    // Arrange - wrong shape of doc on purpose
    const doc = { name: 'test' };

    // Act - Call the function to test
    const insertDocumentResult = await insertDocument(
      mockContainer,
      doc as unknown as RawInput,
    );

    // Assert - State verification: Check the result when verification fails
    if (isVerificationErrors(insertDocumentResult)) {
      expect(insertDocumentResult).toEqual({
        message: 'Verification failed',
      });
    } else {
      throw new Error('Result is not of type VerificationErrors');
    }

    // Assert - Behavior verification: Ensure create method was not called
    expect(mockContainer.items.create).not.toHaveBeenCalled();
  });

  it('should insert document successfully', async () => {
    // Arrange - create input and expected result data
    const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
      createTestInputAndResult();

    // Arrange - mock the input verification function to return true
    (inputVerified as jest.Mock).mockReturnValue(true);
    (mockContainer.items.create as jest.Mock).mockResolvedValue({
      resource: result,
    });

    // Act - Call the function to test
    const insertDocumentResult = await insertDocument(mockContainer, input);

    // Assert - State verification: Check the result when insertion is successful
    expect(insertDocumentResult).toEqual(result);

    // Assert - Behavior verification: Ensure create method was called with correct arguments
    expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
    expect(mockContainer.items.create).toHaveBeenCalledWith({
      id: input.id,
      name: result.name,
    });
  });

  it('should return error if db insert fails', async () => {
    // Arrange - create input and expected result data
    const { input, result } = createTestInputAndResult();

    // Arrange - mock the input verification function to return true
    jest.mocked(inputVerified).mockReturnValue(true);

    // Arrange - mock the Cosmos DB create method to throw an error
    const mockError: DbError = {
      message: 'An unknown error occurred',
      code: 500,
    };
    jest.mocked(mockContainer.items.create).mockRejectedValue(mockError);

    // Act - Call the function to test
    const insertDocumentResult = await insertDocument(mockContainer, input);

    // Assert - verify type as DbError
    if (isDbError(insertDocumentResult)) {
      expect(insertDocumentResult.message).toBe(mockError.message);
    } else {
      throw new Error('Result is not of type DbError');
    }

    // Assert - Behavior verification: Ensure create method was called with correct arguments
    expect(mockContainer.items.create).toHaveBeenCalledTimes(1);
    expect(mockContainer.items.create).toHaveBeenCalledWith({
      id: input.id,
      name: result.name,
    });
  });
});

Información adicional