I’ve recently taken an interest in Cloudfront Functions and started using them for some of this sites functionality. To read more about what I’ve been doing check out these posts that capture my experience so far:

This post is about my approach to unit testing the Cloudfront Function code which was a little challenging.

What are Cloudfront Functions?

Cloudfront Functions, whilst not a successor to Lambda@Edge, are an evolution of them. Whereas Lambda@Edge allows fully fledged lambda functions to run within a regional edge cache (AWS region) closest to the request origin, Cloudfront Functions run right at the edge where the content is ideally going to be served from. In terms of Cloudfront architecture you cannot get closer to the user. There are some restrictions to Cloudfront Functions you have to bear in mind like having a limited runtime, execution time, memory allowance, package size and that they are javascript only with no network access.

Why are Cloudfront Functions hard to unit test?

Because there are no exports allowed. There are a bunch of restrictions within the Cloudfront Function runtime as highlighted, but it is the lack of any kind of export support that makes it difficult. This will work:

function handler() {
    // do something here
}

but any attempt at exporting will cause a runtime error when the Cloudfront Function is run. So anything akin to the following wont work:

export function handler() {
    // do something here
}

module.exports = {
    handler
}

Even this wont work, which surprised me:

export function handler() {
    // do something here
}

if (module) {
  module.exports = {
    handler
  }
}

If you cannot export a function (or import one) how can you test it in another file using a library like jest? Ordinarily you don’t want to test functions that are not exported because the public interface of a module (its exported functions) are the testing target - testing anything else becomes brittle and tests implementation as opposed to behaviour. I’ve not wanted to be able to this before but Cloudfront Functions are my special case!

What are the options?

I considered a few approaches to facilitate unit testing my Cloudfront Functions that I realised wouldn’t work or I didn’t like for various reasons:

  1. Exporting a string template of the function body and then creating a Function object with it in both my Cloudfront Function at runtime and in my tests. Whilst this would work great for the testing part the Function class is not available in the Cloudfront Function runtime. Even if it worked there would be no support in VSCode for syntax highlighting etc. making the development experience unpleasant.
  2. The function code was being placed inline in my Cloudformation template as required (covered here). This meant I could use sed or some regex/replacing to remove any export statements. This would have worked but felt too brittle and I also thought it would be confusing to mix supported and unsupported features in the file without much context.
  3. Duplicate the code both in the Cloudformation template and again in a test file. This is possible but ensuring that when one is updated so is the other will inevitably lead to them going out of sync at some point even with the best intentions.
  4. Use babel to transpile the code to something supported, unfortunately no form of exporting at all is allowed.
  5. Don’t unit test it. This would work but if not unit testing something is the easy option (when isn’t it?!) it should probably be unit tested as there is some inherent complexity or lack of observability there. Also, it was a challenge by now that I wanted to solve and would undoubtedly learn something along the way.

An approach to unit-testing Cloudfront Functions

First I came across the rewire module but that only supports up to ES5 syntax and Cloudfront Functions blends syntax/features support from ES5, ES6, ES7 and beyond. Looking further afield I found a babel plugin called babel-plugin-rewire that brings the approach of rewire a bit more up to date.

My babel config looks like this:

module.exports = {
  presets: [["@babel/preset-env", { targets: { node: "current" } }]],
  // Only invoke rewire when tests are running
  plugins: process.env.NODE_ENV === "test" ? ["rewire"] : [],
};

I only want the plugin to run in a test environment, it takes quite invasive actions and I don’t want the possible side effects of that anywhere near production code. If I had more things to test (I don’t) I could also target the plugin only at my Cloudfront Function test files.

And my test setup looks like this:

import myModule from "./myModule";

describe("myModule tests", () => {
    it("should do some stuff", () => {
      // Arrange
      // Get unexported function
      const unexportedFunc = myModule.__get__("functionName");

      // Act
      const res = unexportedFunc();

      // Assert some stuff
    });
});

The benefits

Though the Cloudfront Function runtime still restricts the syntax and features that I use this babel-plugin-rewire gives me a decent amount more flexibility with how I can write the functions.

Using the approach of rewire means that I have what is hopefully a reliable and supported method of testing my functions in this way. When reading around I came across suggestions of reading the string contents of the file and using eval or Node’s vm to run it. This seems brittle, makes test setup pretty complex and are harder to reason about when I come back to the test code.

Some downsides

Babel had to be added to the project which is considerably more complex than adding the average dependency. Ideally that wouldn’t have been required, particularly as it is only being used by jest to run tests. Having said that, babel wasn’t really a necessity as I could have written my functions in entirely ES5 syntax but I didn’t want to. I value the developer experience as well as the speed and ease with which I can make changes to the site as required, this felt like the best way to facilitate both of those things.

Not all functions can be accessed even when using this approach and I don’t know why. The only mention I’ve found of this is this issue but there is no official response or progress. If super simple/light functions are untestable they are hopefully simple enough that the value of unit testing them is minimal. Testing them by testing other code that consumes them shouldn’t add too much complexity.

Summary

It took some reading and thinking around the problem to figure out the best way to unit test my Cloudfront Functions but I’ve implemented a solution I’m happy with. I’ve not worked on any commercial projects that have utilised Cloudfront Functions yet but hopefully save some time with this approach than having to solve the same challenge.

Get in contact

If you have comments, questions or better ways to do anything that I have discussed in this post then please get in contact.