Mastering Serverless IV: Unit Testing DynamoDB Dependency Injection With Jest

The SaaS Enthusiast
7 min readJan 20, 2024

--

In the fourth installment of our ‘Mastering Serverless’ series, we unlock the secrets to effortlessly testing our DynamoDB Dependency Injection wrapper with Jest, transforming a complex task into a streamlined and efficient process.

Welcome back to our journey through the world of serverless architecture! Over the past articles, we’ve evolved from using a basic DynamoDB helper function to adopting a more sophisticated Dependency Injection (DI) wrapper, significantly enhancing our interactions with DynamoDB. Now, we’re ready to tackle a crucial aspect of software development — testing.

Testing, often viewed as a daunting task, is vital for ensuring code reliability and functionality. In this article, we delve into the reasons why testing our DI-based DynamoDB wrapper is not just easier, but also more effective. We’ll explore the isolation of unit tests, the ease of mocking dependencies, and the flexibility in simulating diverse test scenarios.

Furthermore, we introduce Jest — a powerful testing framework that simplifies and accelerates the testing process. We’ll guide you through setting up Jest in your serverless environment, creating mock objects, and writing comprehensive test cases that cover various aspects of your application’s functionality.

By the end of this article, you’ll have a clear understanding of how to leverage Jest for testing your serverless applications, ensuring that your code is not only functional but also robust and resilient. So, let’s dive into the world of testing with Jest, and see how it can make a significant difference in your serverless projects.

DynamoDB DI Helper Testability Advantage Explained

1. Isolation of Unit Tests:

  • Without DI: In a traditional setup without DI, your methods might directly instantiate and use dependencies (like a database client). Testing these methods becomes challenging because you’re not just testing the method’s logic but also its interaction with the actual dependency. This setup can lead to tests that are less of a unit test and more of an integration test.
  • With DI: DI allows you to inject dependencies into your classes or methods. In testing, you can replace these dependencies with mocks or stubs. This separation means you’re testing only the logic of the method itself, not the underlying dependency. Such isolation is the cornerstone of effective unit testing.

2. Ease of Mocking:

  • Mock Creation: When using DI, creating mocks for dependencies is straightforward. Most modern testing frameworks provide easy ways to create and manage these mocks.
  • Controlled Environment: With mocks, you simulate the behavior of real dependencies under controlled conditions. For instance, you can mock a database client to return specific data or throw an error without making actual database calls.

3. Flexibility in Test Scenarios:

  • Simulating Different Conditions: DI makes it easier to test how your method behaves under various scenarios. For example, you can inject a mock that simulates a successful database query, a network error, or a timeout.
  • Edge Case Testing: Handling edge cases becomes more manageable. You can fine-tune your mocks to test how your method handles unexpected or rare situations.

4. Reduced Overhead:

  • No Real Side Effects: Since you’re not interacting with real dependencies (like an actual database), there’s no risk of altering real data. This aspect is crucial for testing methods that perform write operations in a database.
  • Faster Execution: Tests run faster as they don’t need to wait for real network calls or database operations. This speed is beneficial, especially in large test suites or continuous integration environments.

5. Increased Test Coverage and Reliability:

  • Comprehensive Coverage: You can write tests for almost every aspect of your method’s logic, including error handling and corner cases, leading to higher test coverage.
  • Reliable Tests: Tests are more reliable and less flaky, as they are not affected by external factors like database server availability or network latency.

Example in Context

Consider a method in a class that interacts with a DynamoDB table. Without DI, you’d have to either interact with the actual DynamoDB or mock the entire AWS SDK. With DI, you inject a DynamoDB client into the class. In your tests, you replace this client with a mock that you can program to behave exactly as needed for each test, providing a clean, controlled, and efficient testing environment.

Choosing the Right Tool

Analysis of Mocking Tools for Serverless

Several mocking tools are available for serverless applications, each with its strengths:

  1. aws-sdk-mock: Specific to AWS SDK. It’s useful if you’re heavily reliant on AWS services. However, it can be cumbersome for complex scenarios or when you need to mock services outside the AWS SDK.
  2. Jest: A comprehensive testing framework with built-in mocking capabilities. It’s not limited to AWS or serverless and provides a more holistic testing solution.
  3. Sinon.js: Great for spies, stubs, and mocks, but it doesn’t replace modules like Jest. Works well in combination with other testing frameworks.
  4. Nock: Ideal for HTTP request mocking, useful if your serverless application interacts with external HTTP APIs.

Why Jest is Popular

  • Versatility: Jest can handle testing needs for a wide range of JavaScript applications, not just serverless.
  • Integrated Mocking: Built-in mocking capabilities make it easy to mock dependencies, timers, and more.
  • Watch Mode: Jest’s watch mode is helpful during development for running tests related to changed files.
  • Coverage Reports: Jest generates coverage reports out-of-the-box.
  • Community and Support: Being widely used, Jest has extensive community support and frequent updates.

Testing with DI vs. Regular Helper

A. Testing a Regular Helper File:

Consider a helper file that directly creates and uses a DynamoDB client:

// RegularHelper.js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({});

export const getItem = async (TableName, Key) => {
// Code to get item from DynamoDB
};

Testing this directly involves more complexity:

  • You need to mock the DynamoDB client and its methods globally or alter the implementation for testing.
  • Testing different scenarios (like error handling) might require altering the mocks for each test.
  • It’s harder to isolate the method under test from its dependencies.

B. Testing with DI:

Now, consider the DynamoDbHelper class using DI:

// DynamoDbHelper.js (using DI)
class DynamoDbHelper {
constructor(docClient) {
this.docClient = docClient;
}

async get(TableName, Key) {
// Code to get item from DynamoDB using this.docClient
}
}

Testing this is more straightforward:

  • You can inject a mock docClient when testing, allowing you to control and simulate various scenarios easily.
  • The method under test is isolated from its dependencies, leading to more reliable and maintainable tests.

Example: Testing Difference

Regular Helper Test:

To test getItem from RegularHelper.js, you need to mock the entire AWS SDK:

// Using jest
import * as AWS from "@aws-sdk/client-dynamodb";
jest.mock("@aws-sdk/client-dynamodb");

test('getItem returns expected result', async () => {
// Mock DynamoDBClient method
AWS.DynamoDBClient.prototype.send = jest.fn().mockResolvedValue(/* Mocked Response */);

const result = await getItem('TableName', { id: '1' });
expect(result).toEqual(/* Expected Result */);
});

DI-Based Test:

Testing get in DynamoDbHelper is cleaner as you only mock what you inject:

// Using jest
import DynamoDbHelper from './DynamoDbHelper';

test('get returns expected result', async () => {
// Mock DocumentClient
const mockDocClient = {
send: jest.fn().mockResolvedValue(/* Mocked Response */)
};
const dbHelper = new DynamoDbHelper(mockDocClient);

const result = await dbHelper.get('TableName', { id: '1' });
expect(result).toEqual(/* Expected Result */);
});

Testing an Example Method

Let’s consider an example test case for the get method in your DynamoDbHelper class.

// Using Jest and aws-sdk-mock

import DynamoDbHelper from '../path/to/DynamoDbHelper';
import AWSMock from 'aws-sdk-mock';
import AWS from 'aws-sdk';

describe('DynamoDbHelper Tests', () => {
let docClient;

beforeAll(() => {
AWSMock.setSDKInstance(AWS);
docClient = new AWS.DynamoDB.DocumentClient();
});

afterEach(() => {
AWSMock.restore('DynamoDB.DocumentClient');
});

test('get method throws error for non-existent table', async () => {
// Arrange
AWSMock.mock('DynamoDB.DocumentClient', 'get', (params, callback) => {
callback(new Error('Table not found'), null);
});
const dbHelper = new DynamoDbHelper(docClient);

// Assert
await expect(dbHelper.get('NonExistentTable', { id: '1' })).rejects.toThrow('Table not found');
});

test('get method calls send with correct parameters', async () => {
// Arrange
const getSpy = jest.fn((params, callback) => {
callback(null, { Item: { id: '1', name: 'Test Item' } });
});
AWSMock.mock('DynamoDB.DocumentClient', 'get', getSpy);
const dbHelper = new DynamoDbHelper(docClient);

// Act
await dbHelper.get('TableName', { id: '1' });

// Assert
expect(getSpy).toHaveBeenCalledWith(
{ TableName: 'TableName', Key: { id: '1' } },
expect.any(Function)
);
});
});

Conclusion

As we wrap up our exploratory journey into the world of serverless testing with Jest, it’s evident that we’ve ventured much further than just understanding code. We’ve stepped into an era where efficiency, reliability, and scalability are not just aspirations but achievable realities. In the vast landscape of serverless architecture, where each element plays a pivotal role, Jest stands out as a key enabler, guiding us through the complexities of automated testing with ease and precision.

Our integration of Jest with the DynamoDB Dependency Injection wrapper has transformed testing from a daunting task into an empowering element of the development process. This combination of Jest’s robust testing framework with the agility of serverless architecture paves the way for building truly resilient applications.

As this chapter of ‘Mastering Serverless’ comes to a close, let’s take these insights and move forward with a renewed perspective. In the ever-changing world of technology, adapting and thriving with tools like Jest is not just an advantage; it’s a necessity. Let’s continue to develop, test, and deploy applications that don’t just meet today’s requirements but are poised to embrace the challenges and opportunities of the future.

Other Articles In The Series

--

--