skip to Main Content

I am new to Jest and currently practicing writing tests for my old project. I am stuck on how to write a test for Amazon’s S3Client.

const s3 = new S3Client({
  credentials: {
    accessKeyId: bucketAccess,
    secretAccessKey: bucketSecret,
  },
  region: bucketRegion,
});
export const uploadProductHandler = async (req, res) => {
  const files = req.files;
  const images = [];
//generateRandom fnc generate random value that can be use as Id for a data
  const productId = generateRandom(10);
  // uploading files to aws ...
  try {
    files.forEach(async (file) => {
      const imageName = generateRandom(32);
      images.push(imageName);
      const params = {
        Bucket: bucketName,
        Key: imageName,
        Body: file.buffer,
        ContentType: file.mimetype,
      };
      const commad = new PutObjectCommand(params);
      await s3.send(commad);
    });
  } catch {
    return res.sendStatus(502);
  }
  const data = req.body;
  const date = Date.now();
  const urls = await setUrls(images);
  try {
    const product = new Product({ ...data, date: date, images: images, productId: productId, imagesUrl: { urls: urls, generated: date } });
    await product.save();
    res.sendStatus(201);
  } catch (err) {
    res.status(400).send(err.message);
  }
};
jest.mock("@aws-sdk/client-s3", () => ({
  PutObjectCommand: jest.fn(),
  S3Client: jest.fn(() => ({
    send: jest.fn(() => "send"),
  })),
}));

describe("upload product", () => {
  it("should send status of 502 on failling on saving img", async () => {
    await uploadProductHandler(mockReq, mockRes);
    const setfail = jest.spyOn(S3Client, "send").mockImplementationOnce(() => Promise.reject("not saved"));
    expect(generateRandom).toHaveBeenCalledTimes(3);
    expect(generateRandom).toHaveBeenLastCalledWith(32);
    expect(PutObjectCommand).toHaveBeenCalledTimes(2);
  });
});

I did tried using "S3Client.prototype" insted of S3Client in jest.spyOn it gave same error

My code is giving error of "Property send does not exist in the provided object"

2

Answers


  1. The problem is that the send method that’s accessible on S3Client instances is not on the constructor and it’s not on its prototype either, and so jest.spyOn throws the error you see. If you need to use jest.spyOn you must pass it an instance of S3Client (its prototype doesn’t count).

    Your code sample is still not quite complete to run it and test it as is but I had a go at simplifying it for the sake of testing.

    Also, I don’t understand why you have two different mock implementations of send: one returned in the S3Client mock above that returns a "send" string, and a different mock via spyOn that returns a rejected promise. Could you not use jest.mock alone, like in the following excerpt which runs fine for me?

    Simplified source, call it src.js:

    const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3");
    
    const s3 = new S3Client({});
    module.exports = uploadProductHandler = async (req, res) => {
      try {
        const commad = new PutObjectCommand({});
        await s3.send(commad);
      } catch {
        // ...
      }
    };
    

    Test script:

    const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3");
    const uploadProductHandler = require("./src.js"); // obviously update import loc
    
    jest.mock("@aws-sdk/client-s3", () => ({
      PutObjectCommand: jest.fn(),
      S3Client: jest.fn(() => ({
        send: jest.fn(() => Promise.reject("not saved")),
      })),
    }));
    
    describe("upload product", () => {
      it("should send status of 502 on failling on saving img", async () => {
        await uploadProductHandler(null, null);
        expect(PutObjectCommand).toHaveBeenCalledTimes(1); // why expect twice by the way?
      });
    });
    

    __

    If you need two different mock versions of send (e.g. one that succeeds), then one solution is to split those tests in different files and define the mock once per file as above.

    Login or Signup to reply.
  2. Building up on @aayla-secura‘s response, if you want to test both send call results (the one that resolves and the one that rejects), what you could do is declare the send mock as a separate const

    The idea of using an arrow function comes from this other contribution: https://stackoverflow.com/a/68073694/12194386

    const mockSend = jest.fn();
    
    jest.mock("@aws-sdk/client-s3", () => ({
      PutObjectCommand: jest.fn(),
      S3Client: jest.fn(() => ({
        send: () => mockSend(),
      })),
    }));
    

    Another solution found in the Jest documentation (see https://jestjs.io/docs/es6-class-mocks) could be to use mockImplementation :

    const mockSend = jest.fn();
    
    jest.mock("@aws-sdk/client-s3", () => ({
      PutObjectCommand: jest.fn(),
      S3Client: jest.fn().mockImplementation(() => ({
        send: mockSend,
      }));
    }));
    

    That way, you have the possibility to mock different behaviors

    describe("upload product", () => {
      it("should send status 502 if saving image fails", async () => {
        mockSend.mockRejectedValueOnce(new Error('Some error'));
        ...
    
        await uploadProductHandler(mockReq, mockRes);
    
        ...
        expect(mockRes.sendStatus).toHaveBeenCalledWith(502);
      });
    
      it("should send status 201 when everything works correctly", async () => {
        mockSend.mockResolvedValueOnce(null);
        ...
    
        await uploadProductHandler(mockReq, mockRes);
    
        ...
        expect(mockRes.sendStatus).toHaveBeenCalledWith(201);
      });
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search