web-dev-qa-db-fra.com

Mocking boto3 méthode client S3 Python

J'essaie de me moquer d'une méthode unique de l'objet client boto3 s3 pour lever une exception. Mais j'ai besoin de toutes les autres méthodes pour que cette classe fonctionne normalement.

C’est pour que je puisse tester un test d’exception singulier lorsqu’une erreur survient en effectuant un pload_part_copy

1ère tentative

import boto3
from mock import patch

with patch('botocore.client.S3.upload_part_copy', side_effect=Exception('Error Uploading')) as mock:
    client = boto3.client('s3')
    # Should return actual result
    o = client.get_object(Bucket='my-bucket', Key='my-key')
    # Should return mocked exception
    e = client.upload_part_copy()

Cependant, cela donne l'erreur suivante:

ImportError: No module named S3

2e tentative

Après avoir examiné le code source botocore.client.py, j’ai trouvé qu’il faisait quelque chose de malin et la méthode upload_part_copy n'existe pas. J'ai trouvé qu'il semble appeler BaseClient._make_api_call à la place, j'ai donc essayé de me moquer de cela

import boto3
from mock import patch

with patch('botocore.client.BaseClient._make_api_call', side_effect=Exception('Error Uploading')) as mock:
    client = boto3.client('s3')
    # Should return actual result
    o = client.get_object(Bucket='my-bucket', Key='my-key')
    # Should return mocked exception
    e = client.upload_part_copy()

Cela jette une exception ... mais sur le get_object que je veux éviter.

Des idées sur la façon dont je peux seulement lancer l'exception sur le upload_part_copy méthode?

46
ptimson

Dès que j'ai posté sur ce site, j'ai réussi à trouver une solution. Ici on espère que ça aide :)

import botocore
from botocore.exceptions import ClientError
from mock import patch
import boto3

orig = botocore.client.BaseClient._make_api_call

def mock_make_api_call(self, operation_name, kwarg):
    if operation_name == 'UploadPartCopy':
        parsed_response = {'Error': {'Code': '500', 'Message': 'Error Uploading'}}
        raise ClientError(parsed_response, operation_name)
    return orig(self, operation_name, kwarg)

with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
    client = boto3.client('s3')
    # Should return actual result
    o = client.get_object(Bucket='my-bucket', Key='my-key')
    # Should return mocked exception
    e = client.upload_part_copy()

Jordan Philips a également mis en ligne une excellente solution en utilisant la classe botocore.stub.Stubber . Bien qu’une solution plus propre, j’ai été incapable de me moquer d’opérations spécifiques.

17
ptimson

Botocore a un stubber client que vous pouvez utiliser uniquement à cette fin: docs .

Voici un exemple d'insertion d'une erreur dans:

import boto3
from botocore.stub import Stubber

client = boto3.client('s3')
stubber = Stubber(client)
stubber.add_client_error('upload_part_copy')
stubber.activate()

# Will raise a ClientError
client.upload_part_copy()

Voici un exemple de réponse normale. De plus, le stubber peut désormais être utilisé dans un contexte. Il est important de noter que le stubber vérifiera, dans la mesure du possible, que votre réponse fournie correspond à ce que le service retournera réellement. Ce n'est pas parfait, mais cela vous évitera d'insérer des réponses non-sens totales.

import boto3
from botocore.stub import Stubber

client = boto3.client('s3')
stubber = Stubber(client)
list_buckets_response = {
    "Owner": {
        "DisplayName": "name",
        "ID": "EXAMPLE123"
    },
    "Buckets": [{
        "CreationDate": "2016-05-25T16:55:48.000Z",
        "Name": "foo"
    }]
}
expected_params = {}
stubber.add_response('list_buckets', list_buckets_response, expected_params)

with stubber:
    response = client.list_buckets()

assert response == list_buckets_response
66
Jordon Phillips

Voici un exemple d'un simple python unittest qui peut être utilisé pour simuler client = boto3.client ('ec2') appel api ...

import boto3 

class MyAWSModule():
    def __init__(self):
        client = boto3.client('ec2')
        tags = client.describe_tags(DryRun=False)


class TestMyAWSModule(unittest.TestCase):
    @mock.patch("boto3.client.get_tags")
    @mock.patch("boto3.client")
    def test_open_file_with_existing_file(self, mock_boto_client, mock_describe_tags):
        mock_boto_client.return_value = mock_get_tags_response
        my_aws_module = MyAWSModule()

        mock_boto_client.assert_call_once('ec2')
        mock_describe_tags.assert_call_once_with(DryRun=False)

mock_get_tags_response = {
    'Tags': [
        {
            'ResourceId': 'string',
            'ResourceType': 'customer-gateway',
            'Key': 'string',
            'Value': 'string'
        },
    ],
'NextToken': 'string'
}

j'espère que cela aide.

6
Aidan Melen

Qu'en est-il simplement en utilisant moto ?

Il vient avec un très pratique décorateur :

from moto import mock_s3

@mock_s3
def test_my_model_save():
    pass
4
wikier

J'ai dû me moquer du client boto3 Pour des tests d'intégration et c'était un peu pénible! Le problème que j’avais, c’est que moto ne supporte pas très bien KMS, mais je n’ai pas voulu réécrire ma propre maquette pour les compartiments S3. J'ai donc créé ce morphing de toutes les réponses. En outre, cela fonctionne globalement, ce qui est plutôt cool!

Je l'ai installé avec 2 fichiers.

Le premier est aws_mock.py. Pour le KMS moqueur, j'ai reçu des réponses prédéfinies provenant du client live boto3.

from unittest.mock import MagicMock

import boto3
from moto import mock_s3

# `create_key` response
create_resp = { ... }

# `generate_data_key` response
generate_resp = { ... }

# `decrypt` response
decrypt_resp = { ... }

def client(*args, **kwargs):
    if args[0] == 's3':
        s3_mock = mock_s3()
        s3_mock.start()
        mock_client = boto3.client(*args, **kwargs)

    else:
        mock_client = boto3.client(*args, **kwargs)

        if args[0] == 'kms':
            mock_client.create_key = MagicMock(return_value=create_resp)
            mock_client.generate_data_key = MagicMock(return_value=generate_resp)
            mock_client.decrypt = MagicMock(return_value=decrypt_resp)

    return mock_client

Le second est le module de test actuel. Appelons-le test_my_module.py. J'ai omis le code de my_module. Ainsi que des fonctions qui sont sous le test. Appelons ces fonctions foo, bar.

from unittest.mock import patch

import aws_mock
import my_module

@patch('my_module.boto3')
def test_my_module(boto3):
    # Some prep work for the mock mode
    boto3.client = aws_mock.client

    conn = boto3.client('s3')
    conn.create_bucket(Bucket='my-bucket')

    # Actual testing
    resp = my_module.foo()
    assert(resp == 'Valid')

    resp = my_module.bar()
    assert(resp != 'Not Valid')

    # Etc, etc, etc...

Encore une chose, je ne suis pas sûr que cela soit corrigé, mais j’ai découvert que moto n’était pas heureux, à moins que vous ne définissiez certaines variables environnementales telles que les informations d’authentification et la région. Ils ne doivent pas nécessairement être des informations d'identification réelles, mais ils doivent être définis. Il y a une chance que cela soit corrigé au moment où vous lisez ceci! Mais voici du code au cas où vous en auriez besoin, du code Shell cette fois!

export AWS_ACCESS_KEY_ID='foo'
export AWS_SECRET_ACCESS_KEY='bar'
export AWS_DEFAULT_REGION='us-east-1'

Je sais que ce n'est probablement pas le plus joli morceau de code, mais si vous cherchez quelque chose d'universel, cela devrait plutôt bien fonctionner!

3
Barmaley

Voici ma solution pour patcher un client boto utilisé dans les entrailles de mon projet, avec pytest fixtures. J'utilise seulement 'mturk' dans mon projet.

Le truc pour moi était de créer mon propre client, puis de patcher boto3.client avec une fonction qui retourne ce client pré-créé.

@pytest.fixture(scope='session')
def patched_boto_client():
    my_client = boto3.client('mturk')

    def my_client_func(*args, **kwargs):
        return my_client

    with patch('bowels.of.project.other_module.boto3.client', my_client_func):
        yield my_client_func


def test_create_hit(patched_boto_client):    
    client = patched_boto_client()
    stubber = Stubber(client)
    stubber.add_response('create_hit_type', {'my_response':'is_great'})
    stubber.add_response('create_hit_with_hit_type', {'my_other_response':'is_greater'})
    stubber.activate()

    import bowels.of.project # this module imports `other_module`
    bowels.of.project.create_hit_function_that_calls_a_function_in_other_module_which_invokes_boto3_dot_client_at_some_point()

Je définis également un autre appareil qui configure des crédits factices pour que boto ne récupère pas accidentellement d'autres informations d'identification sur le système. Je mets littéralement les mots "foo" et "bar" comme critères de test - ce n'est pas une rédaction.

Il est important que AWS_PROFILE env soit désaffecté car sinon boto cherchera ce profil.

@pytest.fixture(scope='session')
def setup_env():
    os.environ['AWS_ACCESS_KEY_ID'] = 'foo'
    os.environ['AWS_SECRET_ACCESS_KEY'] = 'bar'
    os.environ.pop('AWS_PROFILE', None)

Et puis je spécifie setup_env comme une entrée pytest usefixtures pour qu’elle soit utilisée à chaque exécution de test.

1
deargle

Si vous ne souhaitez utiliser ni moto ni le stubber botocore (le stubber empêche pas d'empêcher les requêtes HTTP d'être adressées aux points de terminaison de l'API AWS), vous pouvez utiliser le mode plus détaillé. manière unittest.mock:

foo/bar.py

import boto3

def my_bar_function():
    client = boto3.client('s3')
    buckets = client.list_buckets()
    ...

bar_test.py

import unittest
from unittest import mock


class MyTest(unittest.TestCase):

     @mock.patch('foo.bar.boto3.client')
     def test_that_bar_works(self, mock_s3_client):
         self.assertTrue(mock_s3_client.return_value.list_buckets.call_count == 1)

0
c4urself