DynamoDB nodejs Testing Tool

Why?

DynamoDB is a fantastic database, but so far it misses a nice abstraction that would make you more confident that the code you write is correct. All those string-based queries and parameters probably make you uncomfortable, especially if you are coming from the easy-land of MongoDB.
Writing node.js scripts to set tables up and run methods on them quickly gets tiring, without the fantastic workflow that tools like Jest and WallabyJS provide you.
This tool will allow you to iterate quickly while working with DynamoDB.
Take a look at source code with some examples here, or keep reading

How?

We will start the DynamoDB in the background for you (assuming you have Java SDK, if not, you will have to install it). That should also work on CIs!
Locally, we suggest running DynamoDB in the background on port 4567 (run your tests with DYNAMO_TEST_PORT env variables if you want to use a different one) to make things faster.

We dynamically create and delete tables for your tests behind the scenes, to make sure there are no "collisions" or "race conditions".

There is not much API to the library.
You call createTable with an input defined by DynamoDB.CreateTableInput.
That returns an object that has inside two things - the generated name, documentClient which you can use to perform operations.

Example:

import { DynamoDB } from "aws-sdk";
import {
  CreatedTable,
  createTable,
  generateRandomName,
} from "dynamodb-testing-tool";

let tableObject: CreatedTable;

describe("Multiple tests do not collide with each other", () => {
  beforeEach(async () => {
    const dynamoSchema: DynamoDB.CreateTableInput = {
      TableName: generateRandomName(),
      AttributeDefinitions: [{ AttributeType: "N", AttributeName: "id" }],
      KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
      BillingMode: "PAY_PER_REQUEST",
    };
    tableObject = await createTable(dynamoSchema);
  });
  test("Adding an item and getting all should return one item", async () => {
    await tableObject.documentClient
      .put({
        TableName: tableObject.tableName,
        Item: { id: 1, somethingElse: true },
      })
      .promise();

    const results = await tableObject.documentClient
      .scan({ TableName: tableObject.tableName })
      .promise();

    expect(results.Items).toHaveLength(1);
    // @ts-ignore
    expect(results.Items[0]).toMatchObject({ somethingElse: true });
  });

  test("Adding another item and getting all should still return one item", async () => {
    await tableObject.documentClient
      .put({
        TableName: tableObject.tableName,
        Item: { id: 2, somethingElse: false },
      })
      .promise();

    const results = await tableObject.documentClient
      .scan({ TableName: tableObject.tableName })
      .promise();

    expect(results.Items).toHaveLength(1);
    // @ts-ignore
    expect(results.Items[0]).toMatchObject({ somethingElse: false });
  });
});

Options:

readOnly

We remove tables automatically, even though the names do not collide with each other, DynamoDB starts to slow down with hundreds of tables, especially if they use the same indexes (and they will if you recreate tables with the same shapes over and over).
This method makes the tooling remove the table after all tests in a given file or block are finished.
The use-case is similar to keepTable, but the table will get dynamically recreated from scratch on every test run.
This might be useful in the initial phase when you want to frequently change the table definition and its items.
If you work with a small number of items and you are only concerned about testing reads from a pre-populated table, it's probably best to use this option. It's plenty fast and convenient.

keepTable

If you want to avoid removing tables altogether, pass {keepTable: true} as an option to createTable function.
That option does two things:

  • it prevents the tooling from deleting the table
  • it allows writing operations on the table the first time around (when the table does not exist or is empty)
  • it disables the write operations on the table when the table exists and it has at least one item inside.

This is especially useful if you want to test multiple different ways of querying your dataset, but do not want to wait hundreds of milliseconds or even seconds for the DB to get populated over and over. Imagine inserting a million records to DynamoDB and being able to test different read patterns with wallaby.js. Dream come true!

Example:

import { DynamoDB } from "aws-sdk";
import { createTable } from "dynamodb-testing-tool";

const dynamoSchema: DynamoDB.CreateTableInput = {
  TableName: "fixedName",
  AttributeDefinitions: [{ AttributeType: "N", AttributeName: "id" }],
  KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
  BillingMode: "PAY_PER_REQUEST",
};

test("keepTable option disallows changing the table if it finds the table with items already", async () => {
  const { documentClient, tableName } = await createTable(dynamoSchema, {
    keepTable: true,
  });
  await documentClient
    .put({
      TableName: tableName,
      Item: { id: 1, somethingElse: true },
    })
    .promise();

  let results = await documentClient.scan({ TableName: tableName }).promise();

  expect(results.Items).toHaveLength(1);

  await createTable(dynamoSchema, { keepTable: true });
  await documentClient
    .put({
      TableName: tableName,
      Item: { id: 1, somethingElse: true },
    })
    .promise();

  results = await documentClient.scan({ TableName: tableName }).promise();

  expect(results.Items).toHaveLength(1);
});

Common gotchas

Timeouts

If your CI fails and you see this scary error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout. or/and this one: TypeError: Cannot read property 'documentClient' of undefined

That means your CI was too slow to startup the DynamoDB. You might need to increase the jest timeout, take a look here: https://jestjs.io/docs/en/jest-object#jestsettimeouttimeout

PreExisting tables

If you see this error: ResourceInUseException: Cannot create preexisting table that might mean that you are generating the randomName not often enough. For example:

const dynamoSchema: DynamoDB.CreateTableInput = {
  TableName: generateRandomName(),
  AttributeDefinitions: [{ AttributeType: "N", AttributeName: "id" }],
  KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
  BillingMode: "PAY_PER_REQUEST",
};
beforeEach(async () => {
  tableObject = await createTable(dynamoSchema);
});

Will not work, because the dynamoSchema object will get defined once, and all calls to createTables will reuse the same name. change it to this:

beforeEach(async () => {
  const dynamoSchema: DynamoDB.CreateTableInput = {
    TableName: generateRandomName(),
    AttributeDefinitions: [{ AttributeType: "N", AttributeName: "id" }],
    KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
    BillingMode: "PAY_PER_REQUEST",
  };
  tableObject = await createTable(dynamoSchema);
});

Let me know if you have any questions or thoughts in the comments below.


Let us help you on your journey to Quality Faster

We at Xolvio specialize in helping our clients get more for less. We can get you to the holy grail of continuous deployment where every commit can go to production — and yes, even for large enterprises.

Feel free to schedule a call or send us a message below to see how we can help.

User icon
Envelope icon

or

Book a call
+
Loading Calendly widget...
  • Add types to your AWS lambda handler

    Lambdas handlers can be invoked with many different, but always complex, event arguments. Add to that the context, callback, matching return type and you basically start listing all the different ways that your function can fail in production.

  • How to expose a local service to the internet

    From time to time you might need to expose your locally running service to the external world - for example you might want to test a webhook that calls your service. To speed up the test/development feedback loop it would be great to be able to point that webhook to your local machine.

  • For loops in JavaScript (vs _.times)

    From time to time I still see a for loop in JavaScript codebases. Linters are frequently angry about them. Let's see how we can replace them.