0

There are similar questions but all of them uses some kind of library ( which does not work in my case).How do i make i work for promise return by dynamodb query

here is my code to be tested

  const isUrl = require('is-url');
const AWS = require('aws-sdk');
const { nanoid } = require('nanoid/async');
const express = require('express');

const router = express.Router();
const dynamoDb = new AWS.DynamoDB.DocumentClient();

// URL from users

router.post('/', async (req, res) => {
  // urlId contains converted short url characters generated by nanoid

  const urlId = await nanoid(8);
  const { longUrl } = req.body;

  // Veryfying url Format using isUrl, this return a boolean
  const checkUrl = isUrl(longUrl);
  if (checkUrl === false) {
    return res.status(400).json({ error: 'Invalid URL, please try again!!!' });
  }

  const originalUrl = longUrl;
  const userType = 'anonymous'; // user type for anonymous users
  const tableName = 'table-name'; // table name for storing url's

  const anonymousUrlCheckParams = {
    TableName: tableName,
    Key: {
      userId: userType,
      originalUrl,
    },
  };

  const paramsForTransaction = {
    TransactItems: [
      {
        Put: {
          TableName: tableName,
          Item: {
            userId: userType,
            originalUrl,
            convertedUrl: `https://url/${urlId}`,
          },
        },
      },

      {
        Put: {
          TableName: tableName,
          Item: {
            userId: urlId,
            originalUrl,
          },
          ConditionExpression: 'attribute_not_exists(userId)',
        },
      },
    ],
  };

  try {
    const dynamoDbGetResults = await dynamoDb
      .get(anonymousUrlCheckParams)
      .promise();
    if (
      Object.keys(dynamoDbGetResults).length === 0 &&
      dynamoDbGetResults.constructor === Object
    ) {
      await dynamoDb.transactWrite(paramsForTransaction).promise();
      return res.status(201).json({
        convertedUrl: `https://trimify.awssensei.tk/${urlId}`,
      });
    }
    return res.status(201).json({
      convertedUrl: dynamoDbGetResults.Item.convertedUrl,
    });
  } catch (err) {
    return res
      .status(500)
      .json({ error: 'Unknown Server Error, Please Trimify Again!' });
  }
});

module.exports = router;

my test code

describe('Test for Authenticated Url', () => {
  beforeEach(() => {
    jest.resetModules();
  });
  test('should return converted url for authenticated user', async () => {
    const mockedDocumentClient = {
      get: jest.fn(),
      transactWrite: jest.fn(),
    };

    const mockedDynamoDB = {
      DocumentClient: jest.fn(() => mockedDocumentClient),
    };
    jest.doMock('aws-sdk', () => ({ DynamoDB: mockedDynamoDB }));

    mockedDocumentClient.get.mockImplementation((params, callback) => {
      const data = {
        Item: {
          convertedUrl: 'xyz',
        },
      };
      callback(null, data);
    });
    mockedDocumentClient.transactWrite.mockImplementation(
      (params, callback) => {
        callback(null);
      }
    );

    const app = require('../app');
    const response = await request(app).post('/auth-ops/convert').send({
      longUrl: 'https://google.com',
      userId: 'google-oauth2|110198332436292901252',
    });
    expect(response.body).toMatchObject({ convertedUrl: 'xyz' });
    expect(response.statusCode).toBe(201);
  });
test('should send 201 when it is new Url', async () => {
    const mockedDocumentClient = {
      get: jest.fn(),
      transactWrite: jest.fn(),
    };

    const mockedDynamoDB = {
      DocumentClient: jest.fn(() => mockedDocumentClient),
    };
    jest.doMock('aws-sdk', () => ({ DynamoDB: mockedDynamoDB }));

    mockedDocumentClient.get.mockImplementation((params, callback) => {
      const data = {};
      callback(null, data);
    });
    mockedDocumentClient.transactWrite.mockImplementation(
      (params, callback) => {
        callback(null);
      }
    );

    const app = require('../app');
    const response = await request(app).post('/auth-ops/convert').send({
      longUrl: 'https://google.com',
      userId: 'google-oauth2|110198332436292901252',
    });
    expect(response.body).not.toBe(undefined);
    expect(response.statusCode).toBe(201);
  });
});
Jatin Mehrotra
  • 9,286
  • 4
  • 28
  • 67

2 Answers2

2

i removed the express specific code and create an example using your logic, i added 3 comments, "CASE A", "CASE B" and "CASE C", which co-relates to all branches in your code (e.g. ifs or returns), we will use that in our test case

let AWS = require("aws-sdk");

let dynamoDb = new AWS.DynamoDB.DocumentClient({ apiVersion: "2012-08-10" });

async function main() {
  try {
    const dynamoDbGetResults = await dynamoDb.get("some-loginUrlCheckParams").promise();
    // CASE A
    if (Object.keys(dynamoDbGetResults).length === 0 && dynamoDbGetResults.constructor === Object) {
      let urlId = await dynamoDb.transactWrite("some-paramsForTransaction").promise();
      return {
        convertedUrl: `https://my-url/${urlId}`,
      };
    }
    // CASE B
    return {
      convertedUrl: dynamoDbGetResults.Item.convertedUrl,
    };
  } catch (err) {
    // CASE C
    return { error: "Unknown Server Error, Please Trimify Again!", message: err.message };
  }
}

module.exports = { main };

from here, we would need to have access to .get and .transactWrite per test, so we can mock the values returned in that specific case

to achieve that, you can take advantage of how jest mock and hoist variables starting with mock* works

creating an api to manipulate the returned value per test case:

let AWS = require("aws-sdk");

let { main } = require("./main");

let mockDocumentClient = {
  get: {
    promise: jest.fn()
  },
  transactWrite: {
    promise: jest.fn()
  },
};

jest.mock("aws-sdk", () => {
  return {
    DynamoDB: {
      DocumentClient: jest.fn().mockImplementation(() => {
        return {
          get: () => mockDocumentClient.get,
          transactWrite: () => mockDocumentClient.transactWrite,
        };
      }),
    },
  };
});

describe("Test for Authenticated Url", () => {
  it("returns CASE A", async () => {
    mockDocumentClient.get.promise.mockReturnValueOnce({})
    mockDocumentClient.transactWrite.promise.mockReturnValueOnce("test-urlId")
    let test = await main();
    expect(test).toEqual({ convertedUrl: 'https://my-url/test-urlId' })
  });

  it("returns CASE B", async () => {
    mockDocumentClient.get.promise.mockReturnValueOnce({
      Item: {
        convertedUrl: "test-converted-url"
      }
    })
    let test = await main();
    expect(test).toEqual({ convertedUrl: 'test-converted-url' })
  });

  it("returns CASE C", async () => {
    mockDocumentClient.get.promise.mockImplementationOnce(() => {
      throw new Error("test-error")
    })
    let test = await main();
    expect(test).toEqual({ error: 'Unknown Server Error, Please Trimify Again!', message: "test-error" })
  });
});

as you can see, we lazy load mockDocumentClient into our jest.mock (Jest will only call mockDocumentClient when the function of get: () => is executed), enabling us to pass the same mock variable to our test cases and manipulate the returned value

achieving 100% test coverage in that snippet:

enter image description here

i highly recommend you to review these two articles, they will explain it in more detail from Jest perspective:

https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter

https://blog.bam.tech/developer-news/fix-jest-mock-cannot-access-before-initialization-error

oieduardorabelo
  • 2,687
  • 18
  • 21
  • i am sorry i edited my question, and still new to jest and its mighty mocking can, you explain it again or check my edit – Jatin Mehrotra Sep 07 '21 at 12:36
  • nothing changes from a test perspective @JatinMehrotra, because the aws-sdk library is still the same, you can follow the mock structure i provided in my answer to achieve what you asked for – oieduardorabelo Sep 07 '21 at 12:38
  • i will try your code. would you take some pains to explain this `let mockDocumentClient = { get: { promise: jest.fn() }, transactWrite: { promise: jest.fn() }, }; jest.mock("aws-sdk", () => { return { DynamoDB: { DocumentClient: jest.fn().mockImplementation(() => { return { get: () => mockDocumentClient.get, transactWrite: () => mockDocumentClient.transactWrite, }; }), }, }; });` – Jatin Mehrotra Sep 07 '21 at 13:43
0

you should reproduce dynamodb signature with mock (my example is just general, idk all details of you dynamo import)

jest.mock('dynamodb', () => {
  get: async (data) => jest.fn()
})
tslalamam
  • 198
  • 7
  • can you tell me either with aws-sdlk-mock library or the way i am trying to mock – Jatin Mehrotra Sep 07 '21 at 11:22
  • @JatinMehrotra you can try stubmatic, please check this article https://amitgupta-gwl.medium.com/mock-dynamodb-services-5192cdcc450f – tslalamam Sep 07 '21 at 11:33
  • or as you say aws-sdk-mock ` var AWS = require('aws-sdk-mock'); AWS.mock('DynamoDB', 'putItem', function (params, callback){ callback(null, "successfully put item in database"); }); /** TESTS **/ AWS.restore('DynamoDB'); ` – tslalamam Sep 07 '21 at 11:34