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?
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.
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
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.
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
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!
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.
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)