Below is the Lambda’s handler:
This handler gets the parameters to create a book from the Lambda input, instantiate the manager class, create our book, and if nothing breaks, returns a successful message.
The manager class creates all the boto3 clients to prepare for the book creation and also wraps all the environment variables used by this Lambda. It helps both the production and alpha environments to use the same code by changing only these variables.
The way we presented the client and environment variables declarations is not a best practice, because it will run on every Lambda instance. But, it helps for demonstrating purposes and sets this class ready to be used as a singleton, running only on Lambda’s cold start. To keep it simple, let’s continue as it is:
The create_new_book
method will then orchestrate the AWS services calls, like our diagram, to build the S3 key based on the author and title, save the book’s information on the database, upload the book file, and broadcast the message of its creation for all the subscribers.
This is a simple bit of code, but it will work to demonstrate the use of Moto on the tests. The test was developed using the well-known library pytest. To develop this same logic with unittest.TestCase
you can check Moto’s documentation here.
This test will begin running the main_fixture
, responsible for activating Moto’s mock, and configuring the local AWS environment created. Then, it continues building an input, the same as the Lambda would receive, and calling the handler method we want to test, our run
method. To guarantee the book was created correctly, we make three asserts:
- Check if the DynamoDB item was created as it should have been
- Check that the S3 object was uploaded in the right place
- Check if the SNS message was published.
Let’s now analyze what is happening under the hood, inside our main_fixture
:
The pytest decorator will ensure this code will run before the actual test if we add the method name as a parameter on the test - no need to import.
To ensure the credentials were configured, and that the system could not make any real call, dummy values were added before activating the mock. That way we avoid the case of a non-mocked service hitting the real AWS environment.
Generating this local AWS environment consists of redirecting the calls of actual systems (mock_
methods) and creating resources as the real environment would have. To keep it all encapsulated in this fixture we make use of context managers, yielding the already configured MainFixture
instance to use inside the test.
The MainFixture
instantiation begins storing the path to the test resources. This will be especially useful to upload a dummy file to the mocked bucket for testing the S3 logic. The env var will be copied to be applied as environment variables allowing us to simulate Lambda’s run closely. Next, we have the clients of the three AWS services called by our handler, which will help us with the asserts. For the last piece, the sns_backend
contains all the messages sent to any SNS topic.
Insert formatted text here
With the instance now ready, we can use its attributes to set up the empty local AWS environment to be created. The exit_stack
will help us to work with all the context managers we need.
As we have an empty infrastructure, to be able to save book information to a DynamoDB table, the table needs to be created first. The same logic applies to the other services, so we create the S3 bucket and the SNS topic. While creating the topic, we save its ARN, to be used in the message assert.
The environment variables copied will be added to our context - avoiding using the @patch
decorator on every test. Any other patch can be added like that if needed.
The code above demonstrates how those services are created, and the cached_property
decorator will make sure this topic is created just once and only the topic ARN is returned on the following calls.
And finally, let’s analyze the asserts made:
The first two are basically the opposite of what was presented inside the NewBookManager
, making sure the right S3 object is in the bucket, and that the right item was added to the DynamoDB table. The third assert needs to call Moto’s sns_backend
, which is the engine behind Moto’s SNS mock. It persists all messages published to topics, so with the topic’s ARN the notifications sent can be retrieved and compared with the expected message.
Conclusion
Mocking AWS services can be complicated without the right tools, but solutions like Moto make it more trustworthy. With the ability to emulate the behavior of AWS services, and to do it offline, developers can test their code more effectively, efficiently, and with no real calls.
The hands-on example presented in this post provides not only an explanation of how to build tests that behave as AWS real resources but also provides a template that can be reused across other examples. When developing tests, some resources tend to be the same across several tests, which means that with only a few changes the template provided can be adapted to other services and other tests. As you begin implementing this in your own code you will solidify your application, gain increased confidence in deployments, and improve the overall experience for your users.
The full code can be found in this repository, along with another example using SES: mock_aws_Moto