Thursday, May 2, 2024

TDD AWS – the Moto Library

Moto is a library that allows mocking AWS services when using the Python Boto library. It can be used with other languages as stated in the documentation – standalone server mode. I don’t have direct experience with that feature, though, as Python is my language of choice for coding PoCs or data projects in general.

The TDD way in the Cloud

I don’t need to preach about the benefits of using TDD to design and test your components. In any case, if you are still not convinced, check out books like:

Developing for the cloud comes with its own set of challenges. For instance, testing. Not easy when you need to use services that are not locally available.

The appearance of libraries like Moto has made testing much more manageable. Like any library, it has its peculiarities, but the learning curve or resistance is not exceptionally high, especially if you have previous experience with Pytest or other testing frameworks.

Prerequisites

I assume you have previous knowledge of AWS, Boto, Python, TDD and Pytest. In this case, I’m providing some complete listings to learn by example, not the entire exercise, though, so you can fill any gaps and enhance your learning experience.

Installing Moto

It’s straightforward –

$ pip install moto[all] - if you want to install all the mocks
$ pip install moto[dynamodb]

To mock AWS services and test them properly, you will need a few more dependencies –

$ pip install boto3 pytest

Mocking DynamoDB – Setting up

Let’s go beyond the canonical examples and find out how to mock DynamoDB, the excellent AWS NoSQL database.

As shown in the following code listing, we’ll create the books table with the PK id. The function mocked_table returns a table with some data. Later on, this table will be mocked by Moto.

test_helper.py

import boto3
import json

def mocked_table(data):
    dynamodb = boto3.client("dynamodb")
    table = dynamodb.create_table(
        TableName='notes',
        KeySchema=[
            {
                'AttributeName': 'id',
                'KeyType': 'HASH'
            }
        ],
        AttributeDefinitions=[
            {
                'AttributeName': 'id',
                'AttributeType': 'HASH'
            }
        ],
        ProvisionedThroughput={
            'ReadCapacityUnits': 1,
            'WriteCapacityUnits': 1
        }
    )
    
    table = boto3.resource('dynamodb').Table("books")
    with table.batch_writer() as user_data:
        for item in data:
            table.put_item(Item=item)

    return table

Moto recommends using test credentials to avoid any leaking to other environments. You can provide them in the configuration file conftest.py, using Python fixtures

conftest.py

import os

import boto3
import pytest

from moto import mock_dynamodb2

os.environ['AWS_DEFAULT_REGION'] = 'eu-west-1'

@pytest.fixture(scope='function')
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ['AWS_ACCESS_KEY_ID'] = 'testing'
    os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing'
    os.environ['AWS_SECURITY_TOKEN'] = 'testing'
    os.environ['AWS_SESSION_TOKEN'] = 'testing'

Now, we are ready to start designing and developing.

Mocking DynamoDB with Moto and TDD

We will code the function get_book that retrieves a particular Item from the table. Following the TDD cycle, we have to code the test first, make it fail etc … not showing here the entire cycle, but just a few steps to get the idea.

The test unit would look like this –

get_books_test.py

import pytest
import json
import boto3

from moto import mock_dynamodb2

book_OK = {
    "pathParameters":{
        "id": "B9B3022F98Fjvjs83AB8a80C185D",
    }
}
book_ERROR = {
    "pathParameters":{
        "id": "B9B3022F98Fjvjs83AB8a80C18",
    }
}

@mock_dynamodb2
def test_get_book():
    from get_book import get_book # dont't change the order
    from test_helper import mocked_table # must be imported first
    
    dynamodb = boto3.client("dynamodb") # before getting the client 
    
    data = [{'id' : 'B9B3022F98Fjvjs83AB8a80C185D','user' : 'User1'}]   
    mocked_table(data)

    result = get_book(book_OK)

    item = result['Item']
    assert item['id'] == 'B9B3022F98Fjvjs83AB8a80C185D'
    assert item['user'] == 'User1'

As you can see, nothing very different from a Pytest test unit, except for the Moto annotations and AWS’S specific code.

Let’s increase the test coverage, ensuring that the functionality “Item not found” is working as expected.

@mock_dynamodb2
def test_get_book_not_found():    
from get_book import get_book # dont't change the order
    from test_helper import mocked_table # must be imported first
    
    dynamodb = boto3.client("dynamodb") # before getting the client 
    
    data = [{'id' : 'B9B3022F98Fjvjs83AB8a80C185D','user' : 'User1'}]   
    mocked_table(data)
    
    result = get_book(book_ERROR)
    
    assert 'Item' not in result # book not found

OK, now that we have the test unit by design, we need to write the function get_book. We can start with something basic that satisfies the import and the method signature. By the way, I showed the test unit fully coded, but you can do this gradually and code the essential function initially.

get_book.py

import json
import boto3

dynamodb = boto3.resource('dynamodb')
tableName = 'books'

def get_book(id):
    return {'id' : 'B9B3022F98Fjvjs83AB8a80C185D','user' : 'User1'}

To execute the tests

$ pytest

The test will fail. The book that we are returning is not in the expected format. So let’s add the DynamoDb calls – that will be mocked by Moto.

get_book.py

import json
import boto3

dynamodb = boto3.resource('dynamodb')
tableName = 'books'

def get_book(id):
    table = dynamodb.Table(tableName)
    result = table.get_item(
         Key={
              'id': id,
         }
    )
    return result

Now the test unit will pass 🙂

Some important things to point out:

  • The test case is built using the Pytest framework, then Moto for mocking the calls.
  • @mock_dynamodb – marks this method to indicate to the Moto framework that dynamodb will be decorated.
  • The methods we want to mock must be imported before getting the client instance so that the Moto framework can decorate them properly. This is very important; if you don’t do that, the test won’t work.
  • The assert methods come from the Pytest framework – check the docs for more examples.

Conclusion

Testing is not easy and can be tedious; for some people, even a nuisance. Using TDD – or BDD – changes your mindset entirely because you are designing your system, not only testing. But this is something that shouldn’t be news to you. TDD and BDD have been around for a while.

Not for the cloud, though.

Testing in the cloud is not accessible; it’s all about integration and cost. Libraries like Moto helps to alleviate that, and I have to say that it does pretty well.

Adolfo Estevez
A Estevezhttps://mnube.org
Cloud & Digital Evangelist | AWS x 13 Certified | GCP x 6 | Serverless | Machine Learning | Analytics |

Related Articles

Leave a Reply

Latest Articles

error:

Discover more from MNUBE

Subscribe now to keep reading and get access to the full archive.

Continue reading