Better Than Mocking Boto3: DynamoDB Unit Tests with Moto

How and why to write better Python tests for AWS Services using moto. Including a tutorial tests with DynamoDB read and write methods.

ashley carver
ResponseTap Engineering

--

Recently, I’ve mostly been coding AWS Lambdas in Python, which is a joy to implement. Whilst Java is special to me for being my first professional language, I’m growing to like Python more and more. The biggest reason for this is that my main passion is writing testable code. With Python, especially when in small projects, 100% Unit Test coverage becomes really achievable. This is because of how Python is able to mock any method or object to substitute its behaviour to test the layer above it.

Let’s take an ordinary Python example method. Throughout this post, I’ve used the style of cats and kittens, an example often chosen to obfuscate code snippets.

Example Python methods

And you’re probably used to seeing that cats and kittens method tested in the following way using mocking.

Example test for example Python methods

This practice of mocking is fine for a Unit Test with everything in the same module, as they can also be tested both in isolation and together with an Integration Test.

When using AWS Services or libraries like boto3 though, you’re unlikely to have a local version of the service running to make any tests with to check behaviour. So the natural choice is to mock the service or library calls. Obviously, this is better than no test at all, but there are some good reasons why we’d want something a little bit better than this.

Let’s examine first principles and think about best practices for Testing, refreshing our memory of first principles, and keeping a fresh motivation towards good coding practices.

Ideally, we care less for what a method does than for the effect it has on the system. That means that my test should try to focus on checking that the state after the method was run has changed in the way that it was supposed to. So, if the method I’m testing is supposed to save some record to the database, I would really want to check that record is now in the database.

Another best practice for testing is that the test breaks when the intention of the test is no longer met, and not when the parameters of resulting calls change. Using the same example as before, the test should break if it no longer saves the record, not if the implementation changes to alter a less-than-consequential parameter. Both of these are not really met by just mocking the boto3 client/resource and expecting the right values in various parameters — there won’t be a record saved to check (because there isn’t even a table), and the test will break for want of an update as soon as any parameter changes.

There’s also the aspiration of Test Driven Development (TDD), where the developer writes code only to satisfy tests that describe the behaviour of a given service. To do this, the first thing to do is to write or edit the Unit and Integration Tests for the new behaviour to be coded.

If you’re reliant on mocking the boto3 client/resource then you can only make that test in advance if you’re absolutely sure of all the parameter values necessary for the method, otherwise, they’ll need updating once you’ve figured out the right settings. We’ve all had to do that sometimes, but it’s quite awkward, and just grates against the good intentions of following TDD.

One of the major benefits of TDD is that normally you’d achieve a lot of confidence in refactoring of the system. If the Integration Tests still pass, then the refactor’s a good one, and this is a confidence that can last as long as the project does — if the tests are in the right shape to support it. Having the Integration Tests mocking boto3 often works against this, because they are much more likely to need updating.

Lastly, there are some reasons that working with AWS Lambdas raise, too; because of the environment that they run in, and also their deployment processes.

  • Developers are far less likely to test a Lambda locally, and more likely to rely on the development environment for checking behaviour
  • Integration Tests across multiple Lambdas become necessary, but these tests cannot run until after the deployment of the new code
  • Many methods of deploying code to Lambdas take a noticeable amount of time, in which damage can be done by any bugs
  • Bugs in any AWS environment can cost real money from long running Lambdas or expensive service calls for garbage data; this is made worse if the Lambda errors and retries

I think I’ve sold you it to you by now, haven’t I?

We need something better than mocking a boto3 client/resource.

A colleague introduced me to a library called moto, and I’ve loved it ever since. And it really is what all these reasons are absolutely begging for.

It’s a Python library that provides a deep and stateful mocking for the boto3 library. Like the rest of this blog implies, it mocks the action of various boto3 methods, holding that state locally so that I can do better than checking parameters of boto3 — I can check the effect of the invocation in my code.

It’s also very simple to configure, just add “moto” to your Python environment (I use a requirements file to do this). Then, in each test, you use a decorator annotation to indicate which service(s) require moto’s intervention and ask boto3 to setup a default session, just like the snippet below.

Example setup of Moto in a Testcase

So let me show you moto in action, and compare it to how the tests would have looked with just mocking boto3 calls instead. Here begins our tutorial. In this first example, we’ll look at writing a record to a DynamoDB table. The method below will be our example method to write a record.

Example method to write to DynamoDB

Then the test below uses a standard practice of mocking the client/resources from boto3.

Example mocked boto3 test for DynamoDB write method

But the following example is what we get from using Moto instead. In this example test, I’ve checked the response for the majority of things that remain the same between runs.

Example Moto test for DynamoDB write method

The parts to make note of are the table creation with moto, the ability to fetch back the item from the DynamoDB, and the difference with the response. I find those latter two differences very important.

Firstly, the difference in the response shows just how anyone who isn’t familiar with the precise technical return parameters from DynamoDB could get that wrong. Those who are trying DynamoDB for the first time might even assume that the method would return the created record.

Secondly, being able to fetch a created record makes the Unit Test really prove it actually saves a record and in the required shape etc. This is what we’ve been striving for throughout this blog post — a test that checks the effect of the tested method, that breaks when the intention of the tested method is no longer met, that we can write before we’ve completed any of the code, that gives us refactor confidence, and assures us that the code does the required action with the AWS service used.

Let’s look at a second example, just to show a little bit more about how you can use moto, let’s read from the DynamoDB Table.

Example read from DynamoDB method

I’ve chosen to use a Scan operation here because it’s a much more complex operation, and while we know we only want one cat record, the DynamoDB method has no way of making any shortcut with that information. In the real world, this wouldn’t be hard-coded to a particular cat name, and you’d probably at least consider if an index on this attribute is worthwhile. Also, there’s another import at the top of the document for this, to give us access to boto3’s Attr class, which saves a more complicated FilterExpression.

So, the following is our standard mocking test for this operation.

Example mocked boto3 test for DynamoDB read method

This looks fairly standard and covers the basic return, but something much more intricate would be necessary to check the required loop of scanning through a DynamoDB Table. Just as we saw the assumption with the return value when writing, there’s a massive assumption you could make here thinking that the method doesn’t need to loop.

But in the below example using moto instead, you can see a much more intention-focussed way of testing a datasource.

Example Moto test for DynamoDB read method

Our moto test can be written without the worry or knowledge of that loop, as the right responses will appear from the mocked DynamoDB to test through the loop as required. Although, I must be honest, at the scale of this data, the moto test might well return the required “cat” without a loop iteration, so while it’s catching a whole bunch more that a mocked test, not quite 100% of potential issues.

So I can use moto for any of my AWS Service tests?

Pretty much, it’s as straightforward as the previous tests. You’re probably used to thinking of a test in three stages — setting up the test scenario, calling the method, then checking the resulting state/return values as appropriate.

When using moto, the biggest change is in that first stage of preparing the situation, because before you start to set the scene within the AWS Service (in our latter example, writing a record to the DynamoDB Table), moto needs the instructions for the service itself. Specifically, as seen in both examples, setting up a default session, a boto3 client, and creating the DynamoDB Table. Just follow as appropriate for the AWS call you want, whether that’s creating tables, Lambdas, S3 buckets etc.

What you can test with moto is only limited by which endpoints are implemented in the version you’re using. You’ll know when you’ve hit one because an error is raised.

AttributeError: ‘DynamoHandler’ object has no attribute ‘transact_write_items’
Example unimplemented error from Moto

Endpoint implementations get added by moto’s community, so a missing endpoint is merely not there yet. You can always use a mocked boto3 client/resource for the time being — there’s nothing about either style that prevents you from mixing and matching the two approaches where you want to. I’ve even written tests where I’ve had some services mocked and some services operated by Moto.

I hope I’ve persuaded you that Moto will help you achieve efficient, robust Python code calling AWS Services that’s developed quicker and with much more confidence. Until next time, happy coding!

Moto’s home page is found below — where you can see examples, lists of covered services and endpoints, and even join the contributing community.

My example code is also hosted on GitHub so you can run the code for yourself, and view the DynamoDB Transact test to see the unimplemented message.

--

--

ashley carver
ResponseTap Engineering

Software Engineer at ResponseTap. Amateur costume designer, knitter and crocheter.