I've recently been working on a new project to automatically convert blog posts to audio that has a couple different serverless microservices.
Each microservice is initialized using the Serverless Framework and typically consists of one or more Lambda functions and either an SNS topic, SQS queue, DynamoDB table, or API Gateway endpoint.
I decided to implement each serverless microservice using TypeScript rather than my default of traditional Javascript. I chose this path because I wanted to have static type checking for a more resilient code base.
Typescript is a very cool language and has grown in popularity in recent years. It provides a lot of different benefits outside of just static type checking:
- Optional typings.
- Type inference.
- Access to new ES features.
- Option to compile to different JS versions.
- Clear library API definitions via
.d.ts
. - Class and module support.
- A lot of great tooling to support the environment.
In building out these services I ran into a problem that stopped me dead in my tracks for a few hours. I was writing unit tests for a new adapter inside of one of my services that leverages DynamoDB for storage.
To effectively test the adapter I needed to mock the responses from the DynamoDB client inside of the aws-sdk
. This proved to be a bit more challenging than I originally thought.
Therefore I thought it would be worthwhile to put together a quick blog post on the two methods I found for mocking the aws-sdk.
Mocking in TypeScript with TypeMoq
There are a lot of fantastic tools for mocking TypeScript. The one I chose for my work was TypeMoq. It is very similar to the Moq library in the .NET framework.
It is fairly simple to get started. Let's say we have a piece of code like the one shown below.
export class MessageProcessor {
conversionAdapter: ConversionAdapter
constructor(conversionAdapter: ConversionAdapter) {
this.conversionAdapter = conversionAdapter;
}
processJobs(records: IConversionModel[]): Promise<boolean> {
return new Promise((resolve, reject) => {
this.conversionAdapter.convert(record).then((result) => {
resolve(true);
}).catch((err) => {
reject(err);
});
});
}
}
This code is calling the convert
method on a class named ConversionAdapter
. From the code, we can see that the method returns a Promise
. We use the result of that promise to resolve our outer promise or reject it if there is an error.
We can set up a test for the processJobs
method by using TypeMoq to mock the result of the convert
method on ConversionAdapter
.
describe("MessageProcessor", function () {
before(function () => {
this.model = new ConversionModel();
this.model.url = "url";
this.model.content = "content";
this.model.title = "title";
this.model.userId = "1234-userid";
});
it("processJobs success", function (done) => {
var mockAdapterFactory = TypeMoq.Mock.ofType<IConversionAdapter>(ConversionAdapter);
mockAdapterFactory.setup(m => m.convert(this.model)).returns(() => Promise.resolve(1));
var processor = new MessageProcessor(mockAdapterFactory.object);
var pass = [ this.model ];
processor.processJobs(pass).then((result) => {
expect(result).to.eql(true);
done();
});
});
});
The key here is our setup of mockAdapterFactory
. We are using TypeMoq.Mock.ofType<IConversionAdapter>(ConversionAdapter)
to create a mock of our adapter. We then set up the convert
method to return Promise.resolve(1)
.
From there it's just a matter of us passing the actual mock, mockAdapterFactory.object
into our MessageProcessor
class.
Now we can wait for our promise to resolve and verify that the result we get back is true.
OK, now we got the basics of mocking in Typescript. Let's move onto how we can mock AWS services like SNS, SQS, or DynamoDB.
Two ways to mock AWS services in TypeScript
To be clear, there is more than these two ways to mock the aws-sdk
in TypeScript. These are just two ways I got working and wanted to share my experiences to hopefully save someone time in the future.
Let's give ourselves some context first. Here we have a class, StorageAdapter
, that is using DynamoDB internally.
export class StorageAdapter implements IStorageAdapter {
dynamoClient: DynamoDB;
tableName: string;
constructor(dynamoClient: DynamoDB, tableName: string) {
this.tableName = tableName;
this.dynamoClient = dynamoClient;
}
readItem(id: string): Promise<DynamoDB.GetItemOutput> {
return new Promise((resolve, reject) => {
let itemInput: DynamoDB.Types.GetItemInput = {
TableName: this.tableName,
Key: this.setKey(id)
};
this.dynamoClient.getItem(itemInput).promise().then((data: PromiseResult<DynamoDB.GetItemOutput, AWSError>) => {
if (data.$response && data.$response.error != null) {
reject(data.$response.error);
} else {
resolve(data.Item);
}
});
});
}
private setKey(id: string): Key {
let keyStruct: Key = {
"ConversionId": {
S: id
}
}
return keyStruct;
}
}
Here in readItem
, we are calling getItem
on the DynamoDB client from the aws-sdk
. Additionally, we tell the SDK that we want a promise returned to us by appending .promise()
at the end of our call.
We then take the result of that call and reject or resolve our outer promise depending on whether there is an error.
Now, let's dive into two ways we can mock getItem
on the DynamoDB client in order to properly test our code.
Method 1: Using TypeMoq
Our first option is to use TypeMoq.
describe("StorageAdapter", function () => {
before(function () => {
});
it("readItem correctly", function (done) => {
var expectedPr: PromiseResult<DynamoDB.GetItemOutput, AWSError> = {
Item: {
"ConversionId": {
S: "fooid"
},
"Email": {
S: "kyle@test.com"
},
"Status": {
S: "Converting"
},
"Characters": {
N: `${123}`
},
"Url": {
S: "https://cloudfront.com"
}
},
$response: null
};
var mockAdapterFactory = TypeMoq.Mock.ofType<DynamoDB>(DynamoDB);
var mockRequestFactory = TypeMoq.Mock.ofType<Request<DynamoDB.GetItemOutput, AWSError>>(Request);
mockRequestFactory.setup(m => m.promise()).returns(() => Promise.resolve(expectedPr));
mockAdapterFactory.setup(m => m.getItem(TypeMoq.It.isAny())).returns(() => mockRequestFactory.object);
var storage = new StorageAdapter(mockAdapterFactory.object, "testTable");
storage.readItem("fooid").then((result) => {
console.log(result);
}).catch((err) => {
console.error(err);
});
});
});
We first have to set up our DynamoDB mock which is what we see with mockAdapterFactory
. Then we have to create a mocked Request
object that getItem
returns and we can see that with our mockRequestFactory
variable.
Now we can configure our mocks to return what we need to test our code.
First, we configure promise()
on mockRequestFactory
to return a resolved promise with our expected DynamoDB.GetItemOutput
declared in expectedPR
.
Next, we configure getItem()
on mockAdapterFactory
to return the mocked request we just finished setting up.
From there we just need to pass the actual mock of our DynamoDB client by passing mockAdapterFactory.object
into our StorageAdapter
.
If your keeping track we need four lines of code using TypeMoq in order to mock our getItem
call on DynamoDB. This is very verbose for writing tests, but it does provide a high level of visibility into what types are being leveraged for this getItem
call to complete successfully.
Let's explore how we can accomplish the same goal using a different mocking library.
Method 2: Using aws-sdk-mock
There is another mocking library, aws-sdk-mock, that is dedicated to mocking the aws-sdk
.
We can actually reduce the four lines of code we have with TypeMoq to just one line of code.
describe("StorageAdapter", () => {
before(() => {
});
it("readItem correctly", () => {
var expectedPr: PromiseResult<DynamoDB.GetItemOutput, AWSError> = {
Item: {
"ConversionId": {
S: "fooid"
},
"Email": {
S: "kyle@test.com"
},
"Status": {
S: "Converting"
},
"Characters": {
N: `${123}`
},
"Url": {
S: "https://cloudfront.com"
}
},
$response: null
};
AWSMock.mock('DynamoDB', 'getItem', Promise.resolve(expectedPr));
var storage = new StorageAdapter(new DynamoDB(), "testTable");
storage.readItem("fooid").then((result) => {
console.log(result);
}).catch((err) => {
var x = 3;
});
});
});
Instead of mocking our DynamoDB client, the request, and the promise associated with getItem
, we can just mock getItem
and return a resolved promise. There is a lot less set up in order to create a valuable mock and accurately test our StorageAdapter
class.
This has the benefit of being very concise and clear to follow. However, currently aws-sdk-mock
does not have any TypeScript typings which impacts our IntelliSense during development.
Recapping mocking AWS in TypeScript
There have been quite a few articles that cover how to mock AWS services, but they often don't incorporate TypeScript. This isn't a problem as anything written for normal JavaScript can be leveraged in our TS world, just without any types. This is what we see with our use of aws-sdk-mock
.
Testing is key to any production-ready service, even in your low-level adapters that interface with AWS services. However, it can be deceptively tricky to mock AWS services in order to accurately test our code. This is often because the types are more complex, for example, .getItem().promise()
actually returns a generic type of PromiseResult<DynamoDB.GetItemOutput, AWSError>
.
What we have demonstrated here, is two approaches to mocking the aws-sdk
. Our first approach using TypeMoq demonstrated a TypeScript style, using types and incrementally building up our mock. We build up the entire chain of types from the request all the way up to the resolved promise.
The second approach using aws-sdk-mock
showed how we can mock a single service call with one line of code.
In method one, we have types and verbosity, but sacrifice conciseness by having to build up our mock. Method two, we have conciseness but lose our types and must then know the specific method calls and client declarations. There is no right answer here, both work great depending on what your preference is.
On another note, aws-sdk-mock
is dedicated to mocking just the aws-sdk
. Whereas TypeMoq can be used universally throughout our TypeScript tests. In fact, I ended up using both throughout my services. TypeMoq to mock my classes as well as my interfaces and the SDK mock for mocking out the AWS services.
Like most things in developing code, there isn't a single right answer. My hope is that my experience in mocking the aws-sdk
in TypeScript has given you an example of how you can do the same in your own code.
As always if you have questions or comments related to AWS or this post, please leave a comment below.
Are you hungry to learn even more about Amazon Web Services?
If you are looking to begin your AWS journey but feel lost on where to start, consider checking out my course. We focus on hosting, securing, and deploying static websites on AWS. Allowing us to learn over 6 different AWS services as we are using them. After you have mastered the basics there we can then dive into two bonus chapters to cover more advanced topics like Infrastructure as Code and Continous Deployment.