geogdog
geogdog

Reputation: 31

Testing boto3/botocore with pagination

I've been trying to add unit tests to my AWS scripts. I've been using botocore.stub to stub the API calls.

I needed to add pagination to various calls, and I can't seem to find a way to write the tests to include pagination.

Here's an example of the non-paginated test, I'm wondering how I can refactor this test and function to use pagination:

 # -*- coding: utf-8 -*-
 import unittest
 import boto3
 from botocore.stub import Stubber
 from datetime import datetime


 def describe_images(client, repository):
     return client.describe_images(repositoryName=repository)


 class TestCase(unittest.TestCase):
     def setUp(self):
         self.client = boto3.client('ecr')

     def test_describe_images(self):
         describe_images_response = {
             'imageDetails': [
                 {
                     'registryId': 'string',
                     'repositoryName': 'string',
                     'imageDigest': 'string',
                     'imageTags': [
                         'string',
                     ],
                     'imageSizeInBytes': 123,
                     'imagePushedAt': datetime(2015, 1, 1)
                 },
             ],
             'nextToken': 'string'
         }
         stubber = Stubber(self.client)
         expected_params = {'repositoryName': 'repo_name'}
         stubber.add_response(
             'describe_images',
             describe_images_response,
             expected_params
         )
         with stubber:
             response = describe_images(self.client, 'repo_name')

         self.assertEqual(describe_images_response, response)


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

If I update the function to include pagination like this:

 def describe_images(client, repository):
     paginator = client.get_paginator('describe_images')

     response_iterator = paginator.paginate(
         repositoryName=repository
     )
     return response_iterator

we seem to be getting somewhere. The test fails as it should as equality has changed:

F
======================================================================
FAIL: test_describe_images (__main__.TestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "desc_imgs_paginated.py", line 47, in test_describe_images
    self.assertEqual(describe_images_response, response)
AssertionError: {'imageDetails': [{'registryId': 'string'[178 chars]ing'} != <botocore.paginate.PageIterator object at 0x1058649b0>

----------------------------------------------------------------------
Ran 1 test in 0.075s

FAILED (failures=1)

When I try to iterate over the generator::

def describe_images(client, repository):
     paginator = client.get_paginator('describe_images')

     response_iterator = paginator.paginate(
         repositoryName=repository
     )
     return [r for r in response_iterator]

I get the following error:

 E
 ======================================================================
 ERROR: test_describe_images (__main__.TestCase)
 ----------------------------------------------------------------------
 Traceback (most recent call last):
   File "desc_imgs_paginated.py", line 45, in test_describe_images
     response = describe_images(self.client, repo_name)
   File "desc_imgs_paginated.py", line 14, in describe_images
     return '.join([r for r in response_iterator])
   File "desc_imgs_paginated.py", line 14, in <listcomp>
     return '.join([r for r in response_iterator])
   File "lib/python3.6/site-packages/botocore/paginate.py", line 255, in __iter__
     response = self._make_request(current_kwargs)
   File "lib/python3.6/site-packages/botocore/paginate.py", line 332, in _make_request
     return self._method(**current_kwargs)
   File "lib/python3.6/site-packages/botocore/client.py", line 312, in _api_call
     return self._make_api_call(operation_name, kwargs)
   File "lib/python3.6/site-packages/botocore/client.py", line 579, in _make_api_call
     api_params, operation_model, context=request_context)
   File "lib/python3.6/site-packages/botocore/client.py", line 631, in _convert_to_request_dict
     params=api_params, model=operation_model, context=context)
   File "lib/python3.6/site-packages/botocore/hooks.py", line 227, in emit
     return self._emit(event_name, kwargs)
   File "lib/python3.6/site-packages/botocore/hooks.py", line 210, in _emit
     response = handler(**kwargs)
   File "lib/python3.6/site-packages/botocore/stub.py", line 337, in _assert_expected_params
     self._assert_expected_call_order(model, params)
   File "lib/python3.6/site-packages/botocore/stub.py", line 323, in _assert_expected_call_order
     pformat(params)))
 botocore.exceptions.StubResponseError: Error getting response stub for operation DescribeImages: Unexpected API Call: called with parameters:
 {nextToken: string, repositoryName: repo_name}

 ----------------------------------------------------------------------
 Ran 1 test in 0.051s

 FAILED (errors=1)

Am I missing the correct approach to testing this? or is this a bug in boto3/botocore?

Upvotes: 3

Views: 5192

Answers (1)

Bhaskar
Bhaskar

Reputation: 592

It's been a while since this question was asked but since there isn't an answer ..

In your set up you provide a response dictionary as below

describe_images_response = {
    'imageDetails': [
        {
            'registryId': 'string',
            'repositoryName': 'string',
            'imageDigest': 'string',
            'imageTags': [
                'string',
            ],
            'imageSizeInBytes': 123,
            'imagePushedAt': datetime(2015, 1, 1)
        },
    ],
    'nextToken': 'string'
}

The key here is that the first response will include a nextToken value. This will result in a second request from the paginator. So you have to provide an additional response for the stub, ultimately you need to end with a response the does not include a nextToken

Now looking back at you set up, there is only a single add_response call to the stubber

stubber.add_response(
    'describe_images',
    describe_images_response,
    expected_params
)

The net result in that when the paginator makes the second request, there is not response specified in the setup.

This results in the exception, the message on which hopefully now makes more sense

 botocore.exceptions.StubResponseError: Error getting response stub for operation DescribeImages: Unexpected API Call: called with parameters:
 {nextToken: string, repositoryName: repo_name}

Since the second response hasn't been set up, you get an exception with the request that was unexpected, in this request you can see the specification of the nextToken parameter.

Upvotes: 1

Related Questions