Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When should I use `autospec=True` with the mock library?

When should I use autospec=True when using mock.patch and its variants?

On one hand, this article warns us to always use autospec=True:

... you should always use the create_autospec method and the autospec parameter with the @patch and @patch.object decorators.

On the other hand, autospec has serious drawbacks and limits, as explained in idjaw's answer to this question.

So my question is: when should I use autospec=True or create_autospec, and when should I not use it?

I fear not using autospec may result in tests not breaking when they really should break, as described in the mentioned article. However autospec has its drawbacks. How should I act?

like image 536
Aviv Cohn Avatar asked Nov 09 '16 03:11

Aviv Cohn


1 Answers

I can understand the motivation to suggest the enforcing of using autospec.

Maybe the following could help provide more clarity on what you get and don't get with autospec.

In short, using autospec ensures that the attributes you use in your mock are in fact part of the class you are mocking.

So, with the example below, I'll illustrate how a test will pass when technically you might not want it to:

Take this simple example we will test:

class Foo:
    def __init__(self, x):
        self.x = x

class Bar:
    def __init__(self):
        self.y = 4
        self.c = Foo('potato')

And the test code:

class TestAutoSpec(unittest.TestCase):
    @patch('some_module.Foo')
    def test_autospec(self, mock_foo_class):
        mock_foo_obj = mock_foo_class.return_value

        bar_obj = some_module.Bar()

        self.assertTrue(hasattr(bar_obj.c, 'you_should_fail'))

Now, if you look back at the Foo class, you will clearly see you_should_fail is clearly not an attribute in Foo. However, if you run this test code, it will in fact pass. Which is very misleading.

This is because if an attribute does not exist in a MagicMock, it will still be of type MagicMock. If you print type(bar_obj.c.you_should_fail) in that test, you will end up getting:

<class 'unittest.mock.MagicMock'>

This will certainly cause the hasattr test to pass. If you run the above test again, except change your patch to be: @patch('some_module.Foo', autospec=True), it will fail as it should.

Now, to write a successful test for this and still use autospec=True, you simply create the attribute in your mock testing as needed. Remember, the reason this is needed, is because autospec cannot know about the attributes created dynamically, i.e. in the __init__ when you create an instance.

So, the autospec way to do this, would be:

class TestAutoSpec(unittest.TestCase):

    @patch('some_module.Foo', autospec=True)
    def test_autospec(self, mock_foo_class):
        mock_foo_obj = mock_foo_class.return_value

        # create the attribute you need from mocked Foo
        mock_foo_obj.x = "potato"

        bar_obj = some_module.Bar()

        self.assertEqual(bar_obj.c.x, 'potato')
        self.assertFalse(hasattr(bar_obj.c, 'poof'))

Now, your test will successfully pass at validating your x attribute, while also validating that you don't have some bogus attribute that does not exist in your real Foo class.

Here is also another explanation by Martijn Pieters, that does not necessarily directly answer your question, but gives a very good example and explanation of using autospec that can help further your understanding:

https://stackoverflow.com/a/31710001/1832539

like image 153
idjaw Avatar answered Oct 07 '22 02:10

idjaw