10

I'm trying to mock one particular boto3 function. My module, Cleanup, imports boto3. Cleanup also has a class, "cleaner". During init, cleaner creates an ec2 client:

self.ec2_client = boto3.client('ec2')

I want to mock the ec2 client method: desribe_tags(), which python says is:

<bound method EC2.describe_tags of <botocore.client.EC2 object at 0x7fd98660add0>>

the furthest I've gotten is importing botocore in my test file and trying:

mock.patch(Cleaner.botocore.client.EC2.describe_tags)

which fails with:

AttributeError: 'module' object has no attribute 'EC2'

How do I mock this method?

Cleanup looks like:

import boto3
class cleaner(object):
    def __init__(self):
        self.ec2_client = boto3.client('ec2')

The ec2_client object is the one that has the desribe_tags() method. It's a botocore.client.EC2 object, but I never directly import botocore.

idjaw
  • 25,487
  • 7
  • 64
  • 83
Jeff Tang
  • 1,723
  • 3
  • 11
  • 7
  • 1
    Inside your cleanup module. How exactly are you importing EC2 to use it? From the looks of it you are doing something like `import boto3`. Right? So, I would suspect that your patching should be something like `Cleanup.boto3.EC2`. If you named your module `Cleanup`. Some more info to be sure would help. – idjaw Apr 14 '16 at 15:36
  • Sample of module added – Jeff Tang Apr 14 '16 at 16:10

2 Answers2

17

I found a solution to this when trying to mock a different method for the S3 client

import botocore
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 == 'DescribeTags':
        # Your Operation here!
        print(kwarg)
    return orig(self, operation_name, kwarg)

with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
    client = boto3.client('ec2')
    # Calling describe tags will perform your mocked operation e.g. print args
    e = client.describe_tags()

Hope it helps :)

Community
  • 1
  • 1
ptimson
  • 5,533
  • 8
  • 35
  • 53
  • 1
    This was exceptionally helpful to bridge using moto.mock_service to get the full use of the AWS toolkit, but then to also patch the very specific API call I wanted to fail (in my case was put_events (PutEvents) for EventBridge). Wasted a lot of time trying to mock out botocore.client.EventBridge and others. – Poken1151 Aug 18 '22 at 20:32
3

You should be mocking with respect to where you are testing. So, if you are testing your cleaner class (Which I suggest you use PEP8 standards here, and make it Cleaner), then you want to mock with respect to where you are testing. So, your patching should actually be something along the lines of:

class SomeTest(Unittest.TestCase):
    @mock.patch('path.to.Cleaner.boto3.client', return_value=Mock())
    def setUp(self, boto_client_mock):
        self.cleaner_client = boto_client_mock.return_value

    def your_test(self):
        # call the method you are looking to test here

        # simple test to check that the method you are looking to mock was called
        self.cleaner_client.desribe_tags.assert_called_with()

I suggest reading through the mocking documentation which has many examples to do what you are trying to do

idjaw
  • 25,487
  • 7
  • 64
  • 83
  • I'm not trying to mock `boto3.client`. I'm trying to mock a method, `describe_tags`, of an object `botocore.client.EC2`, which is returned by `boto3.client` – Jeff Tang Apr 14 '16 at 16:23
  • So, you are still instantiating a working client of EC2, but want to mock a method from that object you create? Typically when you are unit-testing and mock patching, you want to mock out the externals, like that EC2 client. You will still be able to validate that method you want to test via the return_value of `boto_client_mock`. – idjaw Apr 14 '16 at 16:27
  • @JeffTang I provided a more detailed example (untested, just wanted to provide overall idea) of what you *might* be looking to do. – idjaw Apr 14 '16 at 16:36