-1

I have decided to go with mocking dynamodb in my express API using jest.

Checkedout out various examples aws-sdk-mock https://stackoverflow.com/a/64568024/13126651

but what i am failing to understand is that -> "dynamodb operations code is inside express route and test file is seperate, so how do i write a mock which ensures that does testing express route code but uses mock which i am implementing. "

my test.js

    const request = require('supertest');
    const app = require('../app');

//should i write my mock implementation over here?
    
    test('Should convert url from anonymous user ', async () => {
      await request(app)
        .post('/anon-ops/convert')
        .send({
          longUrl: 'https://google.com',
        })
        .expect(201);
    });

my express router ( dynamodb is under post)

//how to mock dynamodb over here

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, next) => {
  // 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) {
    res.status(400).json({ error: 'Invalid URL, please try again!!!' });
  }

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

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

  dynamoDb.get(anonymousUrlCheckParams, (err, data) => {
    const paramsForTransaction = {
      TransactItems: [
        {
          Put: {
            TableName: tableName,
            Item: {
              userId: userType,
              originalUrl,
              convertedUrl: `https://xxxxxxxxxxxxxxxx/${urlId}`,
            },
          },
        },

        {
          Put: {
            TableName: tableName,
            Item: {
              userId: urlId,
              originalUrl,
            },
            ConditionExpression: 'attribute_not_exists(userId)',
          },
        },
      ],
    };
    if (err) {
      console.log(err);
      res
        .status(500)
        .json({ error: 'Unknown Server Error, Please Trimify Again!' });
    } else if (Object.keys(data).length === 0 && data.constructor === Object) {
      dynamoDb.transactWrite(paramsForTransaction, async (error) => {
        if (error) {
          // err means converted value as userId is repeated twice.

          console.log(error);
          res
            .status(500)
            .json({ error: 'Unknown Server Error, Please trimify again. ' });
        } else {
          res.status(201).json({
            convertedUrl: `https://xxxxxxxxxxxx/${urlId}`,
          });
        }
      });
    } else {
      res.status(201).json({
        convertedUrl: data.Item.convertedUrl,
      });
    }
  });
});

module.exports = router;
Jatin Mehrotra
  • 9,286
  • 4
  • 28
  • 67

1 Answers1

2

You can use jest.doMock(moduleName, factory, options) to mock aws-sdk module.

Since the DocumentClient class is initialized in the module scope, the module will be cached if the same module is required for multiple times, which means that mock objects in the module scope will also be cached. In order to clear the cache, you need to use the jest.resetModules() method to ensure that the test cases are isolated, and the mock object of each test case will not affect other test cases.

I made some changes to the code to keep it simple.

E.g.

server.js:

const AWS = require('aws-sdk');
const express = require('express');
const bodyParser = require('body-parser');

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

app.use(bodyParser.json());
app.post('/anon-ops/convert', async (req, res) => {
  const urlId = 123;
  const { longUrl } = req.body;
  const originalUrl = longUrl;
  const userType = 'anonymous';
  const tableName = 'xxxxxxxxxxxxx';

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

  dynamoDb.get(anonymousUrlCheckParams, (err, data) => {
    if (err) {
      res.status(500).json({ error: 'Unknown Server Error, Please Trimify Again!' });
    } else if (Object.keys(data).length === 0 && data.constructor === Object) {
      const paramsForTransaction = {
        TransactItems: [
          {
            Put: {
              TableName: tableName,
              Item: { userId: userType, originalUrl, convertedUrl: `https://xxxxxxxxxxxxxxxx/${urlId}` },
            },
          },
          {
            Put: {
              TableName: tableName,
              Item: { userId: urlId, originalUrl },
              ConditionExpression: 'attribute_not_exists(userId)',
            },
          },
        ],
      };
      dynamoDb.transactWrite(paramsForTransaction, (error) => {
        if (error) {
          res.status(500).json({ error: 'Unknown Server Error, Please trimify again. ' });
        } else {
          res.status(201).json({ convertedUrl: `https://xxxxxxxxxxxx/${urlId}` });
        }
      });
    } else {
      res.status(201).json({ convertedUrl: data.Item.convertedUrl });
    }
  });
});

module.exports = app;

server.test.js:

const request = require('supertest');

describe('68901199', () => {
  beforeEach(() => {
    jest.resetModules();
  });
  test('should send converted url', (done) => {
    const mockedDocumentClient = {
      get: jest.fn(),
      transactWrite: jest.fn(),
    };

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

    mockedDocumentClient.get.mockImplementation((params, callback) => {
      callback(null, {});
    });
    mockedDocumentClient.transactWrite.mockImplementation((params, callback) => {
      callback(null);
    });
    const app = require('./server');
    request(app)
      .post('/anon-ops/convert')
      .send({ longUrl: 'https://google.com' })
      .expect('Content-Type', /json/)
      .expect(201)
      .end((err, res) => {
        if (err) return done(err);
        expect(res.body).toEqual({ convertedUrl: `https://xxxxxxxxxxxx/123` });
        done();
      });
  });
});

test result:

 PASS  examples/68901199/server.test.js (10.195 s)
  68901199
    ✓ should send converted url (893 ms)

-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |      88 |     62.5 |     100 |    87.5 |                   
 server.js |      88 |     62.5 |     100 |    87.5 | 23,44,50          
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.893 s

package version:

"supertest": "^6.0.1",
"jest": "^26.6.3",
"aws-sdk": "^2.875.0",
Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • Thank you for the amazing answer, can you explain the server.test so that i can reproduce in my other projects too – Jatin Mehrotra Aug 24 '21 at 07:48
  • Also whyare you sing `const app = require('./server');` when you are exporting app file – Jatin Mehrotra Aug 24 '21 at 08:05
  • @JatinMehrotra I didn't use `express.Router()`, I use `express()`. Just make it simple to demonstrate how to test. – Lin Du Aug 24 '21 at 08:18
  • Understood also i tried your code just added express.Router(), on running your code it gives 500 internal server error – Jatin Mehrotra Aug 24 '21 at 08:20
  • https://gist.github.com/jatinmehrotra/9b85517c2f63bf03d4f599f76b45458e here is the gist – Jatin Mehrotra Aug 24 '21 at 08:22
  • @JatinMehrotra Mount the `router` to `/anon-ops/convert` path and export the `app`. supertest should use `app` instead of `router` created by `express.Router()` – Lin Du Aug 24 '21 at 08:29
  • I am doing exactly like this, it gives 500 internal server error. howver in app,js i am using app.use('/anon-ops/convert,convertAnonUrlRouter) – Jatin Mehrotra Aug 24 '21 at 08:45
  • I miised a part of your code by calling my app.js on top of the file which wa sloaded before mocking actually happends. it worked like a chamr – Jatin Mehrotra Aug 25 '21 at 06:50
  • @slideshownp2 https://stackoverflow.com/questions/68920036/how-to-unit-test-lambda-custom-authorizer-with-jest would you mind to take a look on this – Jatin Mehrotra Aug 25 '21 at 09:19
  • does this also work for promise based query https://stackoverflow.com/questions/42443655/mocking-using-aws-sdk-mocks-promise-support-with-documentclient – Jatin Mehrotra Sep 07 '21 at 11:27