kaste / mockito-python

Mockito is a spying framework
MIT License
123 stars 12 forks source link

Captor example is not working #35

Closed ghsatpute closed 2 years ago

ghsatpute commented 4 years ago

I'm trying out captor example given here

This is my code

from mockito import captor
 captor_test = captor(any(int))

This throws an exception when I ran it

Error
Traceback (most recent call last):
  File "C:\Program Files (x86)\Python37-32\lib\unittest\case.py", line 59, in testPartExecutor
    yield
  File "C:\Program Files (x86)\Python37-32\lib\unittest\case.py", line 628, in run
    testMethod()
  File "C:\Program Files (x86)\Python37-32\lib\unittest\mock.py", line 1626, in _inner
    return f(*args, **kw)
  File "<ommitted>", line 45, in <ommitted function name>
    captor_test = captor(any(int))
TypeError: 'type' object is not iterable

Am I missing something or the example is not up to date?

kaste commented 4 years ago

Both probably. The example assumes you also import from mockito import any with the downside of overwriting the builtin any function.

You can instead also import one of the aliases from mockito import any_, ANY whichever you find easier to read. Or import under a different name from mockito import any as any_ if that would matter.

The docs are a bit sparse here. Some people completely hate the idea of reusing builtin names, other people are okay with it.

ghsatpute commented 4 years ago

Okay. Thanks for the information. I changed the any(...) to ANY(...) in following code.

        argument_captor = captor(ANY(str))
        region_captor = captor(ANY(str))
        when(boto3).client(argument_captor, region_captor).thenReturn(
            self.ses_client_mock) 

The code I want to test has below line

ses_client = boto3.client('ses', region_name='us-west-2')

Here, the first captor captures proper value but the second one doesn't. It gets None value. What might be the reason?

kaste commented 4 years ago

This generally works for me, in that I don't see the None value. On the other hand, the exact code snippet would not work for me.

Because you mock using .client(a, b).thenReturn(...). If I mock using positional arguments, I actually get an error if I then call with keyword arguments (a, b='hello'). You might have actually different code running.

Now, captor is usually not needed anyway. Why don't you just use

when(boto3).client('ses', region_name='us-west-2').thenReturn(someMock)

? Just using this snippet you can be sure client gets called with exact these arguments because it will throw otherwise. (Default mode is strict!) Basically without further ado, with just one when call you kinda expect or only allow these arguments.

ghsatpute commented 4 years ago

Actually, I gave one simplified example and in that example I can get away with the way you mentioned but in the following example, it wouldn't be possible. Because lots of pre-processing.

def send_email(recipient, subject, template): 
    ... 
    ses_client.send_email(
        recipient={
            "ToAddresses": [
            ]
        }, 
        content={
            "HTML": {
                "Body": {
                }, 
                "Text": {
                }
            }
        }, 
        # Few other parameters 

    )

(This is not the actual code, I've just written it out of my head) As you can see, I have to write lots of preprocessing code to generate the output for my when statement matchers.

Rather it would be just simple to use captor and then validate the necessary parts.

kaste commented 4 years ago

Can you write down a reduced testcase that shows that captor.value is None although you used it?

I can only think of that you're actually stubbing (whening) a mock() which is by default not in strict mode. Then you call it with something unexpected, it will not throw, and the the captured value will be None because it didn't match.

E.g.

> client = mock()
> a = captor(any_(str))
> when(client).send_email(a).thenReturn("Foo")
> client.send_email(1)  # Note an int!
> repr(a.value)
'None'
> client.send_email("Hi")
'Foo'
> a.value
'Hi'

Maybe just use mock(strict=True) to see if it receives unexpected "interactions".

Just because you have a lot of arguments doesn't mean you should use captor imo. In above example you could still do

when(client).send_email(
    recipient=expected_recipient, 
    content=expected_content, 
    other_param=ANY, 
    ...
).thenReturn(None)

The ... literally denoting ignore the rest of the arguments.

Or you allow everything when(client).send_email(...) in a setup and then explicitly verify(client).send_email("this", "that", ...) in each test.

ghsatpute commented 4 years ago
  1. I am, indeed, using strict=true.

  2. After I changed the when condition from

    when(boto3).client(service_arg_captor, region_captor).thenReturn(mock_response)

    to

    when(boto3).client(service_arg_captor, region_name=region_captor).thenReturn(mock_response) 

    this worked. As I had used the named parameter in the call so I used the same in when statement. My code to test was as below

      ses_client = boto3.client('ses', region_name='us-west-2')
  3. The example, where I couldn't possibly use when and I had to use captor, even there, the problem was named parameters

  4. Another scenario where the captor1.value could be null, is when you define the wrong data type. For example, instead of str you created captor with dict

  5. One more scenario, where captor value is wrong, when the unwanted parameter is introduced in between. For example, a=1, b=2, if a is unwanted b's captor will get None value

It would really help people if this gets documented. I can contribute if needed.

kaste commented 4 years ago

We're always taking PR's. ❤️

But if a strict mock doesn't throw on unwanted/unexpected invocations that would be a bug, so I would like to replicate and fix that.

I could not reproduce using

class TestBoto:
    def test_a(self, unstub):
        from . import module
        from mockito import captor, any
        service_captor = captor(any(str))
        region_captor = captor(any(str))
        when(module).client(service_captor, region_captor).thenReturn("mocked")

        assert "mocked" == module.client('ses', region_name='us-west-2')

where module.client is just

def client(*args, **kwargs):
    return "Not patched"

(which is probably the same as https://github.com/boto/boto3/blob/dc5a29a372d7d2198dd8b783721bc29acde1eef9/boto3/__init__.py#L85-L91 ) because that gives me:

image

But another pitfall could be that your code under test actually catches all exceptions. In this case some captors that matched have a value while the non-matching captors still have a None. (In the screenshot you can see that service_captor has received 'ses'.)

Beside the ugly repr for the captors and matchers, partially applying is probably confusing here.

kaste commented 2 years ago

Closing as stranded. Note that I reimplemented captor and the confusing partial applying has been fixed.