AWS Boto Unit Testing

Introduction

A little while back I was tasked with writing some tooling for an AWS ECS cluster. The core requirements of the tooling were to allow for automated ECS Container Instance AMI updates, and the deployment of new ECS service task defintions with new docker image builds in them.

Unit Tests for ops related code used in tooling etc? It’s an #ops-problem right?! So who cares about testing platform related code? Well, we all should, because unit testing in the loop of software development is indispensable. Testing cloud platform API calls is historically cumbersome and potentially expensive. It’s frightfully easy to accidently make an api call on an environment you didn’t intend and sometimes passing arguments that were incorrect.

The Tools

Fortunately we have some great tools to help us out:-

Moto - Moto is a library that allows your python tests to easily mock out the boto library.

Pytest - The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries.

pytesting, pytesting….123

Let’s keep it simple to start with and look at how we might test a function that is checking for success conditions only.

Let’s look at this rather rudimentary test function:-

def _test__success_condition(self, svcobj, tarn, otasks):
	svc = svcobj['services'][0]
	if svc['taskDefinition'] != tarn:
            return False
        if svc['desiredCount'] != svc['runningCount']:
            return False
        if svc['pendingCount'] != 0:
            return False
        if len(otasks) != 0:
            return False
        return True

We can easily use pytest with the mark.parametrize decorator to run unit tests against each conditional statement:-

@pytest.mark.parametrize('svcobj, tarn, otasks, expected', [
        ({'services': [{'taskDefinition': 'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
                        'desiredCount': 2,
                        'pendingCount': 0,
                        'runningCount': 2}]},
         'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:515',
         ['task'],
         False),
        ({'services': [{'taskDefinition': 'arn:aws:ecs:eu-west-1:xxxxxxxxxxx:task-definition/ddm_task_dev:518',
                        'desiredCount': 2,
                        'pendingCount': 1,
                        'runningCount': 1}]},
         'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
         ['task'],
         False),
        ({'services': [{'taskDefinition': 'arn:aws:ecs:eu-west-1::task-definition/ddm_task_dev:518',
                        'desiredCount': 2,
                        'pendingCount': 1,
                        'runningCount': 2}]},
         'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
         ['task'],
         False),
        ({'services': [{'taskDefinition': 'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
                        'desiredCount': 2,
                        'pendingCount': 0,
                        'runningCount': 2}]},
         'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
         ['task'],
         False),
        ({'services': [{'taskDefinition': 'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
                        'desiredCount': 2,
                        'pendingCount': 0,
                        'runningCount': 2}]},
         'arn:aws:ecs:eu-west-1:xxxxxxxxxx:task-definition/ddm_task_dev:518',
         [],
         True),
    ])
    def test__check_success_condition(self, svcobj, tarn, otasks, expected):
        assert self._test__success_condition(svcobj, tarn, otasks) == expected

Great, isnt it? If you have several test data fixtures you would like to pass to your function, you can put them into seperate files and import them into your test files as a list of tuples. Pytest is an incredibly powerful tool with many excellent features and it would be impossible for me to cover them all here. I just wanted you to get a feel of using a unit test library that is non-standard but very powerful, and widely used.

Moto Mock

Let us look at a very basic set of unit tests that are very useful because a) We are confirming the correctness of our code and b) We don’t make a single API call to do this.

def check_ami_id_format(self, amiid):
        ami = str(amiid)
        """Check if AMI is in correct format or raise exception."""
        id_check = re.match(r'ami-\w{8,17}', ami, re.M | re.I)
        if id_check is None:
	    raise ValueError('AMI id
        else:
            return ami