Simplifying Serverless Application Testing

The Importance of Testing

When it comes to writing high-quality code, testing is crucial. However, with the rise of serverless architecture, new challenges have emerged. Since we don’t control the environment in which our functions run, simulating that environment can be unreliable.

Understanding Unit, Integration, and End-to-End Testing

There are three primary types of tests: unit, integration, and end-to-end.

  • Unit tests: focus on isolated pieces of logic
  • Integration tests: examine contracts between two or more units
  • End-to-end tests: cover everything from start to finish

Each type of test has its own strengths and weaknesses, and understanding their differences is key to effective testing.

Designing Testable Code

To write good tests, you must first write testable code and functions. This means detecting all the places where your function communicates with the outside world and abstracting them away. We’ll call these abstractions adapters.

By doing so, we can test these occurrences in isolation using cheap unit tests.

Implementing Adapters and Core Logic

Let’s take a simple Lambda function as an example. Our function receives parameters from an SQS queue, fetches an image from an S3 bucket, reduces its size, and uploads it back to the same S3 bucket.

const AWS = require('aws-sdk');

const sqs = new AWS.SQS({ region: 'us-east-1' });
const s3 = new AWS.S3({ region: 'us-east-1' });

exports.handler = async (event) => {
  // fetch image from S3 bucket
  const image = await s3.getObject({ Bucket: 'y-bucket', Key: event.imageKey }).promise();

  // reduce image size
  const reducedImage = await reduceImageSize(image);

  // upload reduced image back to S3 bucket
  await s3.putObject({ Bucket: 'y-bucket', Key: event.imageKey, Body: reducedImage }).promise();
};

We’ll create adapters for EventParser and FileService, as well as a core logic function for image reduction.

Unit Testing Adapters and Core Logic

We’ll start by writing unit tests for our adapters and core logic.

const { EventParser } = require('./EventParser');

describe('EventParser', () => {
  it('should receive an event and sanitize it', () => {
    // test implementation
  });
});

const { FileService } = require('./FileService');

describe('FileService', () => {
  it('should fetch an image from S3 bucket', () => {
    // test implementation
  });

  it('should upload an image to S3 bucket', () => {
    // test implementation
  });
});

const { reduceImageSize } = require('./reduceImageSize');

describe('reduceImageSize', () => {
  it('should reduce image size', () => {
    // test implementation
  });
});

Integration Testing

Integration testing is all about testing contracts and integrations between two or more code components that are already unit tested.

We’ll connect our adapters and core logic according to our business needs and test the resulting integration.

End-to-End Testing

Finally, we’ll test our function in real-life conditions using real AWS infrastructure.

const AWS = require('aws-sdk');

const sqs = new AWS.SQS({ region: 'us-east-1' });
const s3 = new AWS.S3({ region: 'us-east-1' });

describe('End-to-end testing', () => {
  it('should upload an image to S3 bucket after function execution', async () => {
    // insert a message into the SQS queue connected to our function
    await sqs.sendMessage({ QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', MessageBody: JSON.stringify({ imageKey: 'image.jpg' }) }).promise();

    // wait for the function to finish executing
    await wait(5000);

    // check if a new image exists on the given S3 bucket
    const reducedImage = await s3.getObject({ Bucket: 'y-bucket', Key: 'educed-image.jpg' }).promise();
    expect(reducedImage).toBeTruthy();
  });
});

Putting it All Together

Writing tests for Lambda functions requires careful planning and design from the very beginning. By following these principles and using the right tools, we can ensure that our serverless applications are reliable, efficient, and scalable.

Check out the complete code from this article on GitHub to see everything in action.

Leave a Reply