Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to mock.patch smtplib.SMTP

Trying to mock.patch a call to smtplib.SMTP.sendmail in a unittest. The sendmail method appears to be successfully mocked and we can query it as MagicMock, but the called and called_args attributes of the sendmail mock are not correctly updated. It seems likely I'm not applying the patch correctly.

Here's a simplified example of what I'm trying:

import unittest.mock
with unittest.mock.patch('smtplib.SMTP', autospec=True) as mock:
    import smtplib
    smtp = smtplib.SMTP('localhost')
    smtp.sendmail('me', 'me', 'hello world\n')
    mock.assert_called()           # <--- this succeeds
    mock.sendmail.assert_called()  # <--- this fails

This example generates:

AssertionError: Expected 'sendmail' to have been called.

If I alter the patch to smtp.SMTP.sendmail; eg:

with unittest.mock.patch('smtplib.SMTP.sendmail.', autospec=True) as mock:
    ...

I can successfully access the called_args and called attributes of the mock in this case, but because the smtplib.SMTP initialization was allowed to take place, an actual smtp-session is established with a host. This is unittesting, and I'd prefer no actual networking take place.

like image 254
user590028 Avatar asked Mar 03 '23 02:03

user590028


2 Answers

I had the same issue today and forgot that I'm using a context, so just change

mock.sendmail.assert_called()

to

mock.return_value.__enter__.return_value.sendmail.assert_called()

That looks messy but here's my example:

msg = EmailMessage()
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'
msg['Subject'] = 'subject'
msg.set_content('content');

with patch('smtplib.SMTP', autospec=True) as mock_smtp:
    misc.send_email(msg)

    mock_smtp.assert_called()

    context = mock_smtp.return_value.__enter__.return_value
    context.ehlo.assert_called()
    context.starttls.assert_called()
    context.login.assert_called()
    context.send_message.assert_called_with(msg)
like image 56
dustymugs Avatar answered Mar 05 '23 16:03

dustymugs


I marked dustymugs's post as the answer, but I discovered another technique to unittest the call that depends on the mocks method_calls.

import unittest.mock
with unittest.mock.patch('smtplib.SMTP', autospec=True) as mock:
    import smtplib
    smtp = smtplib.SMTP('localhost')
    smtp.sendmail('me', 'you', 'hello world\n')

    # Validate sendmail() was called
    name, args, kwargs = smtpmock.method_calls.pop(0)
    self.assertEqual(name, '().sendmail')
    self.assertEqual({}, kwargs)

    # Validate the sendmail() parameters
    from_, to_, body_ = args
    self.assertEqual('me', from_)
    self.assertEqual(['you'], to_)
    self.assertIn('hello world', body_)
like image 27
user590028 Avatar answered Mar 05 '23 16:03

user590028