getmoto / moto

A library that allows you to easily mock out tests based on AWS infrastructure.
http://docs.getmoto.org/en/latest/
Apache License 2.0
7.65k stars 2.05k forks source link

Building @mock_translate ? #2827

Closed jhw closed 4 years ago

jhw commented 4 years ago

Hi,

I use moto a lot for testing event- based Lambda apps and love it :-)

I have a new app which uses AWS Translate a lot, but then notice that moto doesn't (yet) support Translate

:-(

However I really want to mock Translate to maintain testing consistency with other AWS components in my CI pipeline. So looks like I will have to roll my own, however basic.

My Lambda is very simple -

index.py
import boto3, logging

logger=logging.getLogger()
logger.setLevel(logging.INFO)    

logging.getLogger('botocore').setLevel(logging.WARNING)

Ru, En = "ru", "en"

def handler(event, context):
    translate=boto3.client("translate")
    return translate.translate_text(Text=event["text"],
                                    SourceLanguageCode=Ru,
                                    TargetLanguageCode=En)

and the test pattern I'm looking to achieve also very simple -

test.py
import unittest

@mock_translate    
class IndexTest(unittest.TestCase):

    def setUp(self):
        pass

    def test_handler(self):
        from index import handler
        handler({"text": "привет"}, None)

    def tearDown(self):
        pass

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

but try as I might I can't figure out the moto mocking API. I've been looking closely at the Polly mocking code and have come up with this -

moto_ext.py
from moto.core import BaseBackend
from moto.core.models import base_decorator

class TranslateBackend(BaseBackend):

    def __init__(self, region_name=None):        
        super(TranslateBackend, self).__init__()

    def translate_text(self, *args, **kwargs):
        return (args, kwargs)

url_bases=["https?://translate.(.+).amazonaws.com"]

url_paths={}

mock_translate=base_decorator({'EU': TranslateBackend()}) # <-- @mock_translate decorator for use in test

which seems like I am in the right zone but that some stuff isn't quite right, as I get -

(slow_russian) justin@justin-XPS-13-9360:~/work/slow_russian$ python lambda/translator/test.py 
E
======================================================================
ERROR: test_handler (__main__.IndexTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/justin/work/slow_russian/lib/python3.6/site-packages/moto/core/models.py", line 95, in wrapper
    result = func(*args, **kwargs)
  File "lambda/translator/test.py", line 32, in test_handler
    handler({"text": "привет"}, None)
  File "/home/justin/work/slow_russian/lambda/translator/index.py", line 14, in handler
    TargetLanguageCode=En)
  File "/home/justin/work/slow_russian/lib/python3.6/site-packages/botocore/client.py", line 316, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/justin/work/slow_russian/lib/python3.6/site-packages/botocore/client.py", line 626, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (UnrecognizedClientException) when calling the TranslateText operation: The security token included in the request is invalid.

----------------------------------------------------------------------
Ran 1 test in 0.188s

FAILED (errors=1)

could someone possibly point me in the right direction to get this working ?

Many thanks.

bblommers commented 4 years ago

Hi @jhw, looks like you're missing a Response, which would be the missing link between the http-request and your backend.

If you compare it with the StepFunctions-mock, the url_paths need to be set: https://github.com/spulec/moto/blob/master/moto/stepfunctions/urls.py#L6

This links the actual HTTP request to a generic dispatch function. The dispatch function will call 'StepFunctionResponse.method' for each AWS method that you're trying to call.

Try adding this:

class TranslateResponse(BaseResponse):
    def translate_text(self):
        return TranslateBackend().translate_text(...)

And change the url_paths as appropriate to point to this Base response class

jhw commented 4 years ago

@bblommers works! Many thanks.

One final problem -

test_moto_import.py
import datetime

def timestamp():
    return datetime.datetime.now().strftime("%H:%M:%S")

print ("%s before" % timestamp())

from moto.core import BaseBackend

print ("%s after" % timestamp())

then -

 justin@justin-XPS-13-9360:~/work$ python dev/test_moto_import.py 
22:02:11 before
22:02:41 after

Gah! Why so long ? Any way of reducing this ? (by somehow importing less ?)

Thanks again.

bblommers commented 4 years ago

Hey @jhw, happy to hear you got it working!

The import time is a known issue. The way moto is set up, it will always initialize everything - this can take some time. 30 secs is excessive though - does it get any better in subsequent runs?

(FYI, an outstanding PR to reduce the import time: https://github.com/spulec/moto/pull/2697)

jhw commented 4 years ago
justin@justin-XPS-13-9360:~/work$ python dev/test_moto_import.py 
11:38:04 before
11:38:08 after

Better. Thing something up with my internet/machine last night. Many thanks for your help.

bblommers commented 4 years ago

@jhw If you want to contribute, a PR is always welcome! Looks like a good addition to moto :)

jhw commented 4 years ago

Ah! Yes I should do that. I am generally an extremely poor contributor :-( I will have to investigate the Translate API a bit better, am only mocking a single method at the moment. Leave it with me. I promise to get back. Many thanks.