Sullux / aws-sdk

A wrapper for the woefully inadequate aws-sdk library from Amazon.
25 stars 2 forks source link

How to mock aws-sdk ? #5

Open gagaXD opened 5 years ago

gagaXD commented 5 years ago

Hello !

First of all, let me say thank you, your wrapper really solved many of my issues with aws-sdk :rocket:

I'm trying to add some tests to my lambdas.

In order to do that, I'm using aws-sdk-mock.

Official SDK

It's working great if I use the official aws-sdk :

File.js

const dynamoDC = new AWS.DynamoDB.DocumentClient();
const data= await dynamoDC
      .get({ TableName, Key: key }, (err) => {
        if (err) {
          console.error(err);
          return {};
        }
      })
      .promise();
console.log('data : ', data);

and in my test (using jest)

function mockWorkflowContainer() {
  AWSMock.setSDKInstance(AWS);
  AWSMock.mock('DynamoDB.DocumentClient', 'get', (params, callback) => {
    if (params.Key.workflowName === 'workflow-name') {
      callback(null, {
        Item: {
          test: 'test'
        }
      });
    }
  });
}

Sullux SDK

But, if instead of using the official aws-sdk, i'm using @sullux/aws-sdk, the mock is not working, and I get errors from aws (which should not be called if the mock was working)

( example with S3) bucket.js

const aws = require('@sullux/aws-sdk')();
const {s3} = aws

const file = await s3.getObject(params);
      if (file.Body !== undefined) {
        return file.Body.toString();
      }
      return null;

in my test

awsMock.mock("S3", "getObject", Buffer.from(require("fs").readFileSync("myFile.json")));

I got an error from AWS Error: Error calling AWS.S3.getObject: Access Denied

I was wondering if you had any idea how I could get this to work. I've tried to use the setSDKInstance of aws-sdk-mock, but no luck

Thanks again for your work !

Sullux commented 5 years ago

Hi @gagaXD, thanks so much for starting this discussion! For all the years I've used AWS, I've never used the aws-sdk-mock. My coding style is such that I never use module mocking; only mocking of individual functions. That said, it looks like it would be easy for me to create a @sullux/aws-sdk-mock project, or alternately to add a /mock extension to this existing project. I will look into it and will advise you when it's ready -- hopefully it should be ready by tomorrow if there's not some hidden complexity.

As an aside, I'll share the way I do my testing without module mocking since I imagine some readers of this thread will be asking that question. I am a very functional programmer, so I tend to keep my functions pure by passing IO dependencies (such the AWS SDK functions) to my testable functions; thus, my unit tests just need to pass mocked functions. Here's an example project based on your above dynamo example. I also chose to use Date.now() to illustrate how I keep my testable functions pure. (I kept this code imperative to make it easier to read; normally I write more functionally.)

File Structure

- /workflow
--- index.js
--- workflow.js
--- workflow.spec.js

index.js

NOT TESTED. This module simply injects runtime dependencies into the pure functions and surfaces them for the rest of the app. This file will only be tested by integration tests; not unit tests.

const { dynamoDB: { documentClient } } = require('@sullux/aws-sdk')()
const { workflow, NOT_FOUND_ERROR } = require('./workflow')

const { get: getItem } = documentClient

// pull together the non-deterministic dependencies
const dependencies = {
  getItem,
  now: Date.now,
  tableName: process.env.WORKFLOW_TABLE_NAME,
}

// inject the dependencies into the factory to get the full implementation
const { getWorkflow } = workflow(dependencies)

// export the runtime implementation functions
module.exports = {
  getWorkflow,
  NOT_FOUND_ERROR,
}

workflow.js

This is the implementation file. It uses only pure functions and is fully unit-testable. The rest of the app does not use this file directly; these exports are used from the index.js file.

const NOT_FOUND_ERROR = 'ERR_NOT_FOUND'

const workflow = ({
  getItem,
  now,
  tableName,
}) => ({
  getWorkflow: async (workflowName) => {
    const params = { 
      TableName: tableName,
      Key: { workflowName },
    }
    const { Item: result } = await getItem(params)
    if (!result) {
      const error = new Error(`Workflow "${workflowName}" was not found.`)
      error.code = NOT_FOUND_ERROR
      throw error
    }
    return { result, timestamp: now() }
  },
})

module.exports = {
  workflow,
  NOT_FOUND_ERROR,
}

workflow.spec.js

Using Jest:

const { workflow, NOT_FOUND_ERROR } = require('./workflow')

describe('workflow', () => {
  const mockDependencies = () => ({
    {
      getItem: jest.fn(() => Promise.resolve({ Item: expectedResult.result }))
      now: () => 11111
      tableName: 'workflow-table'
    }
  })
  test('should query the workflow table', async () => {
    const dependencies = mockDependencies()
    const { getWorkflow } = workflow(dependencies)
    await getWorkflow('workflow1')
    expect(dependencies.getItem.mock.calls)
      .toEqual([[{ TableName: 'workflow-table', Key: { workflowName: 'workflow1' } }]])
  })
  test('should return the workflow and timestamp', () => {
    const dependencies = mockDependencies()
    const { getWorkflow } = workflow(dependencies)
    const result = await getWorkflow('workflow1')
    const expectedResult = { result: { foo: 42 }, timestamp: 11111 }
    expect(result).toEqual(expectedResult)
  })
  test('should reject when not found', () => {
    const dependencies = {
      ...mockDependencies(),
      getItem: jest.fn(() => Promise.resolve({}))
    }
    const { getWorkflow } = workflow(dependencies)
    const error = await getWorkflow('workflow1').catch(error => error)
    expect(error).toBeDefined()
    expect(error.code).toEqual(NOT_FOUND_ERROR)
  })
})

I haven't run any of the above code -- just jotted it down quickly as an example of how I work. To consume this in the rest of the app, simply require the folder as in require('./workflow'). That way you will be loading the runtime implementations from the index file.

Je voix que t'abites a Paris -- je n'ecrit pas bient du tout mais je suis fiere de la faite que je parles francais assez courement. 🙂

I will let you know as soon as I have a mock SDK available.

Sullux commented 5 years ago

Update: I looked into it, and it seems that aws-sdk-mock is a thin utility to allow sinon mocking of legacy aws-sdk. It will take some extra effort for me to mimic that functionality directly. I just pushed an update that memoizes sdk instances in an attempt to allow mocking, but apparently sinon doesn't natively support overwriting read-only properties (it isn't hard -- they just haven't chosen to do it).

As a consequence, I will need to add my own feature to allow mocking. I will try to make it follow the pattern of:

const mockS3 = aws().s3.mock()

My recent memoization change will allow this to work consistently between the test file and the implementation file. I was hoping the mocking would be an easy modification, but sinon wasn't cooperative so I will need another day or two to get this ready. I will keep updating this thread.