Search code examples
pythonpytestmoto

My function uses my real dynamodb table instead of the moto mocked one


I have a function which get item from a dynamodb table, add 1, put it back in the table and then return the value

import json
import boto3

dynamodb = boto3.resource('dynamodb', 'us-east-1')
table = dynamodb.Table('cloud-resume-challenge')


def visitors_count(event, context):
    response = table.get_item(
        Key={
            'ID': 'visitors'
        }
    )

    visit_count = response['Item']['visitors']
    visit_count = str(int(visit_count) + 1)

    response = table.put_item(
        Item={
            'ID': 'visitors',
            'visitors': visit_count
        }
    )

    return {
        'statusCode': 200,
        'headers': {
            'Access-Control-Allow-Headers': '*',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': '*'
        },
        'body': json.dumps({'visit_count': visit_count})
   

I am testing this function using moto to mock a dynamodb table then calling my function to use it against the mocked table.

import json
import boto3
import pytest
from moto import mock_dynamodb


@pytest.fixture
def apigw_event():
    """ Generates API GW Event"""

    return {
        "body": '{ "test": "body"}',
        "resource": "/{proxy+}",
        "requestContext": {
            "resourceId": "123456",
            "apiId": "1234567890",
            "resourcePath": "/{proxy+}",
            "httpMethod": "POST",
            "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
            "accountId": "123456789012",
            "identity": {
                "apiKey": "",
                "userArn": "",
                "cognitoAuthenticationType": "",
                "caller": "",
                "userAgent": "Custom User Agent String",
                "user": "",
                "cognitoIdentityPoolId": "",
                "cognitoIdentityId": "",
                "cognitoAuthenticationProvider": "",
                "sourceIp": "127.0.0.1",
                "accountId": "",
            },
            "stage": "prod",
        },
        "queryStringParameters": {"foo": "bar"},
        "headers": {
            "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
            "Accept-Language": "en-US,en;q=0.8",
            "CloudFront-Is-Desktop-Viewer": "true",
            "CloudFront-Is-SmartTV-Viewer": "false",
            "CloudFront-Is-Mobile-Viewer": "false",
            "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
            "CloudFront-Viewer-Country": "US",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Upgrade-Insecure-Requests": "1",
            "X-Forwarded-Port": "443",
            "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
            "X-Forwarded-Proto": "https",
            "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==",
            "CloudFront-Is-Tablet-Viewer": "false",
            "Cache-Control": "max-age=0",
            "User-Agent": "Custom User Agent String",
            "CloudFront-Forwarded-Proto": "https",
            "Accept-Encoding": "gzip, deflate, sdch",
        },
        "pathParameters": {"proxy": "/examplepath"},
        "httpMethod": "POST",
        "stageVariables": {"baz": "qux"},
        "path": "/examplepath",
    }


@mock_dynamodb
def test_visitors_count(apigw_event, mocker):
    from visitors_count import app
    table_name = 'test'
    # Create mock DynamoDB table
    dynamodb = boto3.resource('dynamodb', 'us-east-1')
    table = dynamodb.create_table(
        TableName=table_name,
        KeySchema=[{'AttributeName': 'visitors', 'KeyType': 'HASH'}],
        AttributeDefinitions=[{'AttributeName': 'visitors', 'AttributeType': 'S'}],
        ProvisionedThroughput={'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1}
    )
    table = dynamodb.Table(table_name)

    ret = app.visitors_count(apigw_event, "")
    data = json.loads(ret["body"])

    assert ret["statusCode"] == 200
    assert "visit_count" in ret["body"]
    assert data["visit_count"] == '1'


I get the following error :

apigw_event = {'body': '{ "test": "body"}', 'headers': {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,...ept-Language': 'en-US,en;q=0.8', 'Cache-Control': 'max-age=0', ...}, 'httpMethod': 'POST', 'path': '/examplepath', ...}
mocker = <pytest_mock.plugin.MockerFixture object at 0x00000237DC498310>

    @mock_dynamodb
    def test_visitors_count(apigw_event, mocker):
        from visitors_count import app
        table_name = 'test'
        # Create mock DynamoDB table
        dynamodb = boto3.resource('dynamodb', 'us-east-1')
        table = dynamodb.create_table(
            TableName=table_name,
            KeySchema=[{'AttributeName': 'visitors', 'KeyType': 'HASH'}],
            AttributeDefinitions=[{'AttributeName': 'visitors', 'AttributeType': 'S'}],
            ProvisionedThroughput={'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1}
        )
        table = dynamodb.Table(table_name)

>       ret = app.visitors_count(apigw_event, "")

tests\unit\test_count.py:78:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
visitors_count\app.py:9: in visitors_count
    response = table.get_item(
..\AppData\Local\Programs\Python\Python310\lib\site-packages\boto3\resources\factory.py:580: in do_action
    response = action(self, *args, **kwargs)
..\AppData\Local\Programs\Python\Python310\lib\site-packages\boto3\resources\action.py:88: in __call__
    response = getattr(parent.meta.client, operation_name)(*args, **params)
..\AppData\Local\Programs\Python\Python310\lib\site-packages\botocore\client.py:508: in _api_call
    return self._make_api_call(operation_name, kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <botocore.client.DynamoDB object at 0x00000237DC9C6860>, operation_name = 'GetItem'
api_params = {'Key': {'ID': 'visitors'}, 'TableName': 'cloud-resume-challenge'}

    def _make_api_call(self, operation_name, api_params):
        operation_model = self._service_model.operation_model(operation_name)
        service_name = self._service_model.service_name
        history_recorder.record(
            'API_CALL',
            {
                'service': service_name,
                'operation': operation_name,
                'params': api_params,
            },
        )
        if operation_model.deprecated:
            logger.debug(
                'Warning: %s.%s() is deprecated', service_name, operation_name
            )
        request_context = {
            'client_region': self.meta.region_name,
            'client_config': self.meta.config,
            'has_streaming_input': operation_model.has_streaming_input,
            'auth_type': operation_model.auth_type,
        }
        request_dict = self._convert_to_request_dict(
            api_params, operation_model, context=request_context
        )
        resolve_checksum_context(request_dict, operation_model, api_params)

        service_id = self._service_model.service_id.hyphenize()
        handler, event_response = self.meta.events.emit_until_response(
            'before-call.{service_id}.{operation_name}'.format(
                service_id=service_id, operation_name=operation_name
            ),
            model=operation_model,
            params=request_dict,
            request_signer=self._request_signer,
            context=request_context,
        )

        if event_response is not None:
            http, parsed_response = event_response
        else:
            apply_request_checksum(request_dict)
            http, parsed_response = self._make_request(
                operation_model, request_dict, request_context
            )

        self.meta.events.emit(
            'after-call.{service_id}.{operation_name}'.format(
                service_id=service_id, operation_name=operation_name
            ),
            http_response=http,
            parsed=parsed_response,
            model=operation_model,
            context=request_context,
        )

        if http.status_code >= 300:
            error_code = parsed_response.get("Error", {}).get("Code")
            error_class = self.exceptions.from_code(error_code)
>           raise error_class(parsed_response, operation_name)
E           botocore.errorfactory.ResourceNotFoundException: An error occurred (ResourceNotFoundException) when calling the GetItem operation: Requested resource not found

..\AppData\Local\Programs\Python\Python310\lib\site-packages\botocore\client.py:915: ResourceNotFoundException

As you can see in the error description, my function tries to find my real dynamodb table.

When my function is called, I want it to use my mocked table, how can I fix that ?


Solution

  • Your test creates a table called test - and then tries to find a specific item in a table called cloud-resume-challenge.

    The visitors_count-method assumes that two preconditions are met:

    • a table called cloud-resume-challenge exists
    • an item with Key={ 'ID': 'visitors' } exists

    In your test, only a table called test is created - so Moto will rightfully throw an error that the correct table cannot be found, exactly like AWS would do.

    The test should create a table called cloud-resume-challenge first, and then create the required item. When those preconditions are met, you should be able to call the visitors_count-method.