Kyle
Kyle

Reputation: 153

Mock nested method in Python

Wracking my brain on this. I want to mock generator methods self.api.redditor(username).comments.new(limit=num) and self.api.redditor(username).submissions.new(limit=num) below, in which self.api is assigned to a class instance, as in self.api = PrawReddit()

I'm trying to test the size of the result: self.assertEqual(len(result), 5)

So far, I tried MockPraw.return_value.redditor.return_value.comments.return_value.new.return_value.__iter__.return_value = iter(['c' * 10]) but the test fails with AssertionError: 0 != 5

Any tips much appreciated.

def get_comments_submissions(self, username, num=5):
    """Return max `num` of comments and submissions by `username`."""
    coms = [
        dict(
            title=comment.link_title,
            text=comment.body_html,
            subreddit=comment.subreddit_name_prefixed,
            url=comment.link_url,
            created=datetime.fromtimestamp(comment.created_utc, pytz.utc),
        )
        for comment in self.api.redditor(username).comments.new(limit=num)
    ]
    subs = [
        dict(
            title=submission.title,
            text=submission.selftext_html,
            subreddit=submission.subreddit_name_prefixed,
            url=submission.url,
            created=datetime.fromtimestamp(submission.created_utc, pytz.utc),
        )
        for submission in self.api.redditor(username).submissions.new(limit=num)
    ]
    return coms + subs if len(coms + subs) < num else (coms + subs)[:num]

Upvotes: 1

Views: 3356

Answers (2)

ipaleka
ipaleka

Reputation: 3957

Writting like you use pytest-mock and everything happens in mymodule (you imported the class at the top of the module like from xy import PrawReddit):

mocker.patch("datetime.fromtimestamp")
mocked_comment = mocker.MagicMock()
mocked_submission = mocker.MagicMock()
mocked = mocker.patch("mymodule.PrawReddit")
mocked.return_value.redditor.return_value.comments.new.return_value = [mocker.MagicMock(), mocked_comment]
mocked.return_value.redditor.return_value.submisions.new.return_value = [mocker.MagicMock(), mocked_submission]

returned = instance.get_comments_submissions("foo", num=2)
assert mocked.return_value.redditor.call_count = 2
mocked.return_value.assert_called_with("foo")
assert returned[-1]["link_title"] == mocked_comment.link_title

Another test call with the same intro:

# ...
returned = instance.get_comments_submissions("foo")
assert mocked.return_value.redditor.call_count = 2
mocked.return_value.assert_called_with("foo")
assert returned[1]["link_title"] == mocked_comment.link_title
assert returned[-1]["title"] == mocked_submission.title

Upvotes: 1

Tim
Tim

Reputation: 2637

To mock a generator (unless you are using specific generator features) you can use an iterator as a stand-in eg

import unittest.mock as mock

generator_mock = Mock(return_value=iter(("foo", "bar")))

When you have nested structures like in your example this gets a little more complex, attribute access is automatically handled but return_value from a function must be defined. From your example:

# API mock
mock_api = Mock()
mock_api.redditor.return_value = mock_subs = Mock()

# Submissions mock
mock_subs.new.return_value = iter(("foo", "bar"))

This can then be called and asserted

for item in mock_api.api.redditor("user").submissions.new(limit=5):
    print(item)

mock_api.redditor.assert_called_with("user")
mock_subs.new.assert_called_with(limit=5)

As the API is a member of the same class, this is going to have to be monkey patched eg:

target = Praw()
target.api = mock_api()
target.get_comments_submissions("user")

mock_api.redditor.assert_called_with("user")
mock_subs.new.assert_called_with(limit=5)

Note that the iterator in return value is a single instance and a second call to get the iterator will return the same instance.

Upvotes: 2

Related Questions