Cloud Connections

Cloud Connections

cloud engineering, automation, devops, systems architecture and more…

19 Jul 2020

Using Moto to mock test AWS services: A DynamoDB Example

Why mock tests?

When running tests for functions that use AWS services, we would be calling actual services/resources on AWS if we didn’t mock them. This is not a good idea because:

  • Tests might end up changing actual data or resources on a production environment.
  • Tests might run slow due to latency issues.

moto is a really cool Python library that helped me a lot recently. It allows you to mock AWS services so that you can run unit tests locally, with expected responses from AWS, without actually calling any services/resources on AWS.

In this post, I’ll be explaining an example of using this library to test DynamoDB operations using Python unittest.

The goal is to share a general idea of how to approach to writing unit tests for your app code and show how to use moto library.

Requirements

  • Basic Python knowledge
  • boto3 and moto packages installed

Example: Movies Project

The Python and DynamoDB examples used in the AWS documentation is a good reference point, so we can start writing some tests for a few functions.

We’ll use 3 of the DynamoDB functions shown in the example.

  • Create movies table
  • Put Movie
  • Get Movie

Before we start, we need to think of how to structure them.

The general idea would be to:

  • Create the mock database/table.
  • Run the function test.
  • Delete the mock database/table.

Let’s get to it then, step by step.

Testing the create movies table function

This function creates the DynamoDB table ‘Movies’ with the primary-key year (partition-key) and title (sort-key).

  • Note that this function takes an argument dynamodb. (where the default argument value is set to None if no database resource is provided.)
  • As the example shows, if no resource is provided, it attempts to make a dynamodb resource by connecting to the local address, hence the endpoint_url is set to http://localhost:8000. Bascially, the function expects to have a local instance of DynamoDB running, if no resource is passed.
  • Now, just keep that argument in mind going in to the next steps.

Create a new file for our test.

  • Import a few modules that we will be using in our tests.
# TestMovies.py
from pprint import pprint
import unittest
import boto3 # AWS SDK for Python
from botocore.exceptions import ClientError
from moto import mock_dynamodb2 # since we're going to mock DynamoDB service

Define a test class for our database operations.

  • This will contain all the test methods. The example below is a skeleton for the test structure that we plan to use.
# TestMovies.py
from pprint import pprint
import unittest
import boto3 # AWS SDK for Python
from botocore.exceptions import ClientError
from moto import mock_dynamodb2 # since we're going to mock DynamoDB service

class TestDatabaseFunctions(unittest.TestCase):

    def setUp(self):
        """
        Create database resource and mock table
        """
        pass

    def tearDown(self):
        """
        Delete database resource and mock table
        """
        pass
    
    def test_table_exists(self):
        """
        Test if our mock table is ready
        """
        pass

if __name__ == '__main__':
    unittest.main()

Couple of important methods here is the use of setUp and tearDown. These methods are run before and after the actual test is run. So, all of our mock setup has to be done within these methods so that our test can use them.

Write out the code for the first test.

# TestMovies.py
from pprint import pprint
import unittest
import boto3 # AWS SDK for Python
from botocore.exceptions import ClientError
from moto import mock_dynamodb2 # since we're going to mock DynamoDB service

@mock_dynamodb2
class TestDatabaseFunctions(unittest.TestCase):

    def setUp(self):
        """
        Create database resource and mock table
        """
        self.dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
        
        from MoviesCreateTable import create_movie_table
        self.table = create_movie_table(self.dynamodb) 

    def tearDown(self):
        """
        Delete database resource and mock table
        """
        self.table.delete()
        self.dynamodb=None
    
    def test_table_exists(self):
        """
        Test if our mock table is ready
        """
        def test_table_exists(self):
        self.assertIn('Movies', self.table.name) # check if the table name is 'Movies'

if __name__ == '__main__':
    unittest.main()

Hm, that’s a lot of things we’ve written there. Let’s break it down section by section.

Mocking AWS DynamoDB using moto.

--
@mock_dynamodb2
class TestDatabaseFunctions(unittest.TestCase):
--
  • @mock_dynamodb2 is used as a decorator that wraps the whole test class. This ensures that any calls to DynamoDB within the test methods are mocked.

  • It is important to have moto wrap a whole test class, as it maintains the state of the database and tables.

Setting up the mock database and table.

--
    def setUp(self):
        """
        Create database resource and mock table
        """
        self.dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
        
        from MoviesCreateTable import create_movie_table
        self.table = create_movie_table(self.dynamodb) 
--
  • The self.dynamodb is the mock DynamoDB resource that will be used for the test. We can rest assured that moto will take care of mocking the calls to create this resource.

  • The line from MoviesCreateTable import create_movie_table is we’re going to use the create_movie_table function to create our mock table. After all we want to make sure we’re using the table that is actually used in production and this helps us not rewrite a whole block of code to create a table.

  • A question you might ask is why don’t we import all the functions at the top of the file? Most examples seems to use that. Well, in this case, if we do an import of a function that instantiates a boto3 resource, it might actually make calls to AWS. To avoid this, we make sure all the mocks are established BEFORE the resources are setup. Hence, the import within the test method ensures that mock is already established.

  • self.table is the mock table that will be used by the rest of the test methods. Remember that part about dynamodb argument, this is where we use it to pass the mock database resource to the function. This ensures that any operation is run on the mock resource and not the local database instance.

--
    def tearDown(self):
        """
        Delete database resource and mock table
        """
        self.table.delete()
        self.dynamodb=None
--
  • Here, we’re going to delete all the mock resources created once the test is finished. We don’t want them to be hanging around when the next test method is run.

Testing the function.

--
    def test_table_exists(self):
        """
        Test if our mock table is ready
        """
        self.assertIn('Movies', self.table.name) # check if the table name is 'Movies'
--
  • self.assertIn('Movies', self.table.name) checks if we got a matching value. self.assertIn is equivalent to saying check if a is in b. self.table.name should return us the value ‘Movie’.

  • If you’re unsure of the output value, you can always print the result to confirm. For example, pprint(self.table).

  • The use self is based on Object-Oriented concepts in Python. You can read more about it here.

Phew! That’s a load of stuff. Don’t worry, it gets easier from here. We’ve got all the stuff ready to run our tests. You know what, let’s just run it to make sure we haven’t broken anything. We should be able to get a pass on our single test.

(env) muhannad@LA04:/e/PROJECTS/moto-ddb-example$ python TestMovies.py -v
test_table_exists (__main__.TestDatabaseFunctions) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.164s

OK
(env) muhannad@LA04:/e/PROJECTS/moto-ddb-example$ 

Woohoo! 🎉 It works! On to the real stuff now.

Testing the put movie function

This function creats an item in the Movies table. It takes in a few arguments like title, year, plot etc. and saves the item to the table.

  • As mentioned earlier, this function also takes in the dynamodb argument in case you want to provide a different resource.

Write a new test method in our testing class.

# TestMovies.py
--
    def test_put_movie(self):
        from MoviesPutItem import put_movie

        result = put_movie("The Big New Movie", 2015,
                           "Nothing happens at all.", 0, self.dynamodb)
        
        self.assertEqual(200, result['ResponseMetadata']['HTTPStatusCode'])
--
  • As explained earlier, we’re importing put_movie function within the test method to ensure that all operations are carried out on the mock resources.

  • result stores the output returned from the put_movie method. Note that we’re also passing the mock self.dynamodb resource along with the test data.

  • self.assertEqual(200, result['ResponseMetadata']['HTTPStatusCode']) checks whether the value 200 is in the HTTPStatusCode response. This is the output returned on a successful put item.

Let’s run it and see if we get a pass.

(env) muhannad@LA04:/e/PROJECTS/moto-ddb-example$ python TestMovies.py -v
test_put_movie (__main__.TestDatabaseFunctions) ... ok
test_table_exists (__main__.TestDatabaseFunctions) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.305s

OK
(env) muhannad@LA04:/e/PROJECTS/moto-ddb-example$

What did I tell you? It does get easier. 😁

Testing the get movie function

This function gets the movie details from the Movies table. It requires the arguments title and year to retrieve the item.

Write a new test method in our testing class.

# TestMovies.py
--
    def test_get_movie(self):
        from MoviesPutItem import put_movie
        from MoviesGetItem import get_movie

        put_movie("The Big New Movie", 2015,
                    "Nothing happens at all.", 0, self.dynamodb)
        result = get_movie("The Big New Movie", 2015, self.dynamodb)

        self.assertEqual(2015, result['year'])
        self.assertEqual("The Big New Movie", result['title'])
        self.assertEqual("Nothing happens at all.", result['info']['plot'])
        self.assertEqual(0, result['info']['rating'])
--
  • We’re importing 2 functions put_movie and get_movie from their respective files within the test method like before. This is so that we can reuse the code in our main app to add test data, retrieve it and check the results.

  • put_movie method saves the item to the mock table.

  • result stores the output which we’re using for our asserts in the lines that follow.

Running our tests should give us a pass if everything is ok.

(env) muhannad@LA04:/e/PROJECTS/moto-ddb-example$ python TestMovies.py -v
test_get_movie (__main__.TestDatabaseFunctions) ... ok
test_put_movie (__main__.TestDatabaseFunctions) ... ok
test_table_exists (__main__.TestDatabaseFunctions) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.299s

OK
(env) muhannad@LA04:/e/PROJECTS/moto-ddb-example$

Awesome! We can now confirm that our DynamoDB operations are written correctly.

Here’s the final test file that we have written.

So, how fast does mock tests run?

Using a mock library has definitely made it easier and faster to run tests. I made a basic comparison of how long it took to run before and after using mocking in Github Actions CI.

Before

  • Earlier, I launched a docker instance of DynamoDB before running the tests. The code would handle a local endpoint resource passed into the test methods.
    mock_test_gha_before

After

  • Major improvement after switching to mock tests! About 40% faster.
    mock_test_gha_before

Note: This is a general observation based on runs on public Github Actions runners.

Conclusion

It might feel like a hassle to add unit tests to your application, but personally I’ve found that writing unit tests has helped me understand how the code works and tweaking it to make it more efficient and error free.

Mocking certain services/resources can help run tests faster locally and ensure that they would work when you actually deploy the app to AWS. Moto is definitely a library that shoud definitely be in your toolkit for your AWS/Python projects.

On a side note, the talk TATFT by Bryan Liles might be a source of motivation and inspiration (and laughs) to help get started with Behaviour/Test Driven Development.

References

Categories