When needing to mock a portion of the aws-sdk we must mock ES6 Classes which are not the most straightforward task in Jest. Let’s take a look at mocking the S3 Class and a few of its methods.
We will be using jest.mock() instead of manual mocks in __mocks__/.
Why Is This Tricky?
When we call jest.mock() on a module, Jest mocks all of the modules’ values, and will recursively traverse the module to mock the most deeply nested value. Functions become a jest.fn(), arrays become empty arrays, and so on; but, what we care about is that ES6 Classes, like functions, become a jest.fn() (because Classes are really just functions).
So, that means when we import aws-sdk and call jest.mock(‘aws-sdk’), then all of the Classes on the sdk (ie. S3) will turn into an empty jest.fn().
import aws from "aws-sdk"
jest.mock("aws-sdk")
// we usually instantiate aws.S3(), but let's log out what that returns instead
console.log(aws.S3)
// function(){
// return fn.apply(this, arguments)
// }
aws.S3 is no longer an ES6 Class with properties and methods, it’s an empty mocked function. To show you it’s no different than just jest.fn() consider the following…
console.log(jest.fn())
// function(){
// return fn.apply(this, arguments)
// }
This is where the trickiness of mocking aws-sdk comes into play because we need to run a method from the S3 class, like s3.getSignedUrl(), but we can’t call any methods since we just turned the entire sdk’s Classes into empty jest.fn()s. That means for each AWS service we’re trying to access via the sdk we need to mock an ES6 Class. Good thing for us that mocking Classes is a commonly complained about portion of the Jest documentation.
Let’s get on with it!
Module Factory to the Rescue!
So, how do we provide the mocked implementation of the S3 class? By providing a module factory.
A module factory is the second argument passed into jest.mock(), and it must be a function. It is where we will spell out the return value from calling new aws.S3().
jest.mock("aws-sdk", <module factory goes here>)
We are going to consider two approaches to this
- import the entire
aws-sdkmodule and mock it to return an S3 class - import only the S3 client from the module and mock the Class directly
There is a performance difference you should look into when importing these two different ways, but we won’t discuss that since it’s not the point of this post.
First Approach: import aws from “aws-sdk”
Our jest.mock() module factory must be a function. So let’s get that out of the way…
jest.mock("aws-sdk", () =>{
// implementation goes here
}
Our implementation is going to have two parts to it. We first need to mock how the sdk initially returns an Object with keys. Those keys are the different AWS services it supports. In our case we only care about the S3.
jest.mock("aws-sdk", () =>{
return {
S3: <more implementation>,
<another AWS service if you needed>: <more implementation>
}
}
The second thing we need to do is add the implementation for the S3 Class. To mock a Class we either write a Class or a function (since Classes are just functions). Here’s an example of both:
jest.mock("aws-sdk", () =>{
return {
S3: class {
<whatever properties or methods you need from S3>
}
}
}
Or, as a function…
jest.mock("aws-sdk", () =>{
return {
S3: function(){
return {
<whatever properties or methods you need from S3>
}
}
}
}
I prefer to use the Class syntax only since that’s what is really used in the sdk. It’s also less typing. It doesn’t really matter.
Now we’ll add the specific S3 method we care about.
We’ll start with the .getSignedUrl() method which is pretty straight forward, and then we’ll look at .getObject() and use AWS’ asynchronous .promise() method.
Example #1: S3.getSignedUrl
jest.mock("aws-sdk", () => {
return {
S3: class {
getSignedUrl() {
return "url";
}
}
}
Here I added the .getSignedUrl method to the S3 Class, and it simply returns "url". Now, in my opinion there’s no reason to write tests for the aws-sdk. The only reason we’re mocking it is for the functionality to prevent our code from breaking. AWS tests their own code. Our tests should focus on our code, like whatever we end up doing with "url".
But! If you really want to test the method, here’s an alternative approach…
const mockGetUrl = jest.fn((a, b) => {
return "url";
});
jest.mock("aws-sdk", () => {
return {
S3: class {
getSignedUrl() {
return mockGetUrl();
}
}
}
By separating out the returning of "url" and wrapping it in a jest.fn(), we can now spy on whether this method was called, which will give us confirmation that the sdk is performing as it should.
Example #1: S3.listObjects()
This example is only different in that we’re looking at how to incorporate the .promise() method that AWS exposes for us to run these methods asynchronously. In our code the use of this would look like this…
const params = {
Bucket: <String>,
Prefix: <String>
};
const objects = await s3.listObjects(params).promise();
Accounting for the .promise() method requires adding it to the return value of our S3 .listObjects() method, like so…
jest.mock("aws-sdk", () => {
return {
S3: class {
listObjects() {
return {
promise: function() {
return {
Contents: [ <put your contents here> ]
};
}
};
}
}
};
});
To me this starts to look eerily similar to callback hell from days gone by. I prefer to break it apart similar to the example below for those of you who want to spy on the sdk (…psh!).
const mockPromise = jest.fn(() => {
return {
Contents: [ <put your contents here> ]
};
});
jest.mock("aws-sdk", () => {
return {
S3: class {
listObjects() {
return { promise: mockPromise };
}
}
};
});
Another way to reduce the nested depth of this mock is by not importing the entire aws-sdk when we only need one of its clients.
Second Approach: import S3 from ‘aws-sdk/clients/s3’
This isn’t all that different, as you’ll see, but it does save us from mocking a level into the sdk.
First, here’s how we import and start our mock…
import S3 from "aws-sdk/clients/s3"
const s3 = new S3() // don't need this here, but this is how you'll use // it in your code
jest.mock("aws-sdk/clients/s3", () => {
// implementation goes here
}
Now we still provide our module factory, but we are already at the S3 level. Here it is…
jest.mock("aws-sdk/clients/s3", () => {
return class S3 {
listObjects() {
return {
promise: function() {
return {
Contents: [<put your contents here>]
};
}
};
}
};
});
Okay…so it’s still callback hell-ish, but we could always break out the interior, and maybe spy on it if we felt like it.
const mockPromise = jest.fn(() => {
return {
Contents: [<put your contents here>]
};
});
jest.mock("aws-sdk/clients/s3", () => {
return class S3 {
listObjects() {
return { promise: mockPromise };
}
};
});
Conclusion!
And there you have it!
This is the model you can use when testing the aws-sdk and its supported Classes synchronously or asynchronously.
I hope you enjoyed, and happy coding!
