imagen

Mocks con Jest

Nuestro SUT es una clase que implementa el sdk @aws-sdk/client-dynamodb en un método llamado create que implementa el sdk.

// index.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

export class DynamoDBService {
  private tableName: string;
  private dynamoDBDocumentClient: DynamoDBDocumentClient;
  constructor({ region, tableName }: { region: string; tableName: string }) {
    const translateConfig = { marshallOptions };
    const client: DynamoDBClient = new DynamoDBClient({ region });

    this.dynamoDBDocumentClient = DynamoDBDocumentClient.from(
      client,
      translateConfig
    );
    this.tableName = tableName;
  }

  async create({
    newItem,
    conditionExpression,
  }: {
    newItem: object;
    conditionExpression?: string;
  }) {
    try {
      const params: PutCommandInput = {
        TableName: this.tableName,
        Item: newItem,
      };

      if (conditionExpression) params.ConditionExpression = conditionExpression;

      const response: PutCommandOutput = await this.dynamoDBDocumentClient.send(
        new PutCommand(params)
      );

      return { status: "success", response };
    } catch (error: any) {
      console.error(new Error(`Fail execution in: ${this.constructor.name}`));
      // eslint-disable-next-line prefer-rest-params
      console.error("Arguments received", arguments);
      console.error(error);
      return { status: "fail", error, errorName: error?.name };
    }
  }
}

En el test que vamos a escribir para método create vamos va validar que efectivamente se llame al método send de dynamoDBDocumentClient sin llamarlo realmente, para esto nos vamos a ayuda de dos recurso provistos por jest

  • mockImplementation: Que nos permite simular la implementación de una método o recurso.
  • toHaveBeenCalled: Validar que un método, normalmente un mock ha sido llamado en durante la ejecución del test.
// test.ts
import { DynamoDBService } from "../src";
const mockResponseFromSendCommand = {
  $metadata: {
    httpStatusCode: 200,
    requestId: "9ABQ1FE6VQ7VKP5J4D6GDN4BNFVV4KQNSO5AEMVJF66Q9ASUAAJG",
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0,
  },
  Attributes: undefined,
  ConsumedCapacity: undefined,
  ItemCollectionMetrics: undefined,
};

const mockSend = jest.fn(() => mockResponseFromSendCommand);

jest.mock("@aws-sdk/lib-dynamodb", () => ({
  DynamoDBDocumentClient: {
    from: () => ({
      send: mockSend,
    }),
  },
  PutCommand: jest.fn().mockImplementation(() => ({})),
}));

describe("Create Item", () => {
  test("Method dynamoDBDocumentClient.send is called", async () => {
    const dynamodb = new DynamoDBService({
      region: "us-east-1",
      tableName: "table-axample-qa",
    });

    const newItem = {
      id: 88,
      firstName: "Carry",
      lastName: "Morgan",
    };

    const response = await dynamodb.create({ newItem });
    const expectedResponse = {
      status: "success",
      response: {
        $metadata: {
          httpStatusCode: 200,
          requestId: expect.any(String),
          extendedRequestId: undefined,
          cfId: undefined,
          attempts: 1,
          totalRetryDelay: 0,
        },
        Attributes: undefined,
        ConsumedCapacity: undefined,
        ItemCollectionMetrics: undefined,
      },
    };
    expect(response).toEqual(expectedResponse);
    expect(mockSend).toHaveBeenCalled();
  });
});

Si queremos evaluar mas casos de uso, podemos hacer el mockSend.mockImplementation dentro de cada test.

import { DynamoDBService } from "../src";

const mockSend = jest.fn(() => {});

jest.mock("@aws-sdk/lib-dynamodb", () => ({
  DynamoDBDocumentClient: {
    from: () => ({
      send: mockSend,
    }),
  },
  PutCommand: jest.fn().mockImplementation(() => ({})),
}));

describe("Create Item", () => {
  test("Method dynamoDBDocumentClient.send is called", async () => {
    mockSend.mockImplementation(() => ({
      $metadata: {
        httpStatusCode: 200,
        requestId: "9ABQ1FE6VQ7VKP5J4D6GDN4BNFVV4KQNSO5AEMVJF66Q9ASUAAJG",
        extendedRequestId: undefined,
        cfId: undefined,
        attempts: 1,
        totalRetryDelay: 0,
      },
      Attributes: undefined,
      ConsumedCapacity: undefined,
      ItemCollectionMetrics: undefined,
    }));
    const dynamodb = new DynamoDBService({
      region: "us-east-1",
      tableName: "table-axample-qa",
    });

    const newItem = {
      id: 88,
      firstName: "Carry",
      lastName: "Morgan",
    };

    const response = await dynamodb.create({ newItem });
    const expectedResponse = {
      status: "success",
      response: {
        $metadata: {
          httpStatusCode: 200,
          requestId: expect.any(String),
          extendedRequestId: undefined,
          cfId: undefined,
          attempts: 1,
          totalRetryDelay: 0,
        },
        Attributes: undefined,
        ConsumedCapacity: undefined,
        ItemCollectionMetrics: undefined,
      },
    };

    expect(response).toEqual(expectedResponse);
    expect(mockSend).toHaveBeenCalled();
  });

  test("fail to create", async () => {
    // MockError
    mockSend.mockImplementation(() => {
      const error = new Error(
        "One or more parameter values were invalid: Missing the key contentType in the item"
      );
      error.name = "ValidationException";
      throw error;
    });

    const dynamodb = new DynamoDBService({
      region: "us-east-1",
      tableName: "table-axample-qa",
    });

    const newItem = {
      id: 88,
      firstName: "Carry",
      lastName: "Morgan",
    };

    const response = await dynamodb.create({ newItem });
    const expectedResponse = {
      status: "fail",
      error: expect.any(Object),
      errorName: "ValidationException",
    };

    expect(response).toEqual(expectedResponse);
    expect(mockSend).toHaveBeenCalled();
  });
});