Python模拟上下文管理器

时间:2018-03-10 13:39:55

标签: python python-3.x unit-testing mocking python-unittest

我正在测试一些python3代码,它通过上下文管理器在SMTP类上调用sendmail,并尝试捕获异常以记录它们。我可以成功地模拟SMTP类并在其他测试中对它进行一些检查(例如,验证send_message实际上已被调用),但我似乎无法调用{{1}的方法在类上引发异常以记录错误。

要测试的代码(来自siteidentity_proxy_monitoring.py):

send_message

单元测试方法:

def send_alert(message, email_address):
    with SMTP('localhost') as email:
        try:
            email.send_message(message)
        except SMTPException:
            # retry the send
            print('exception raised') # debugging statement
            try:
                email.send_message(message)
            except:
                logging.error(
                    'Could not send email alert to %s', email_address
                )

测试结果的输出:

@unittest.mock.patch('siteidentity_proxy_monitoring.SMTP')
@unittest.mock.patch('siteidentity_proxy_monitoring.logging')
def test_logging_when_email_fails(self, mock_logger, mock_smtp):
    """
    Test that when alert email fails to send, an error is logged
    """
    test_print('Testing logging when email send fails')
    email_instance = mock_smtp.return_value
    email_instance.send_message.side_effect = SMTPException
    siteidentity_proxy_monitoring.send_alert(
        'test message',
        'email@example.com'
    )
    mock_logger.error.assert_called_with(
        'Could not send email alert to %s', 'email@example.com'
    )

我觉得我错过了与[TEST] ==> Testing logging when email send fails F ====================================================================== FAIL: test_logging_when_email_fails (__main__.TestSiteidentityMonitors) ---------------------------------------------------------------------- Traceback (most recent call last): File "/lib/python3.6/unittest/mock.py",line 1179, in patched return func(*args, **keywargs) File "tests/siteidentity_monitor_tests.py", line 108, in test_logging_when_email_fails 'Could not send email alert to %s', 'email@example.com' File "/lib/python3.6/unittest/mock.py", line 805, in assert_called_with raise AssertionError('Expected call: %s\nNot called' % (expected,)) AssertionError: Expected call: error('Could not send email alert to %s', 'email@example.com') Not called ---------------------------------------------------------------------- Ran 4 tests in 0.955s FAILED (failures=1) __enter__的来电相关的内容,但我似乎无法理解为什么我的修补似乎不会引发副作用在哪里,我期待它。不幸的是,我遇到的大多数示例和文档都没有深入探讨上下文中的模拟方法调用(无论如何我都理解它们。)

2 个答案:

答案 0 :(得分:0)

刚才遇到类似问题,这就是解决方法:

  1. 在行import pdb;pdb.set_trace()之前放置email.send_message(message)
  2. 运行测试。
  3. 进入PDB会话后,输入email.send_message,您会看到类似<Mock name='mock_smtp().__enter__().send_message' id='...'>的内容。
  4. 在测试用例中,将所有()替换为return_value。对于您的情况:mock_smtp.return_value.enter.return_value.send_message.side_effect = SMTPException

答案 1 :(得分:0)

这是使用pytest和mocker fixture的相同测试:

def test_logging_when_email_fails(mocker):
    mock_logging = mocker.MagicMock(name='logging')
    mocker.patch('siteidentity_proxy_monitoring.logging', new=mock_logging)
    mock_SMTP = mocker.MagicMock(name='SMTP',
                                 spec=siteidentity_proxy_monitoring.SMTP)
    mocker.patch('siteidentity_proxy_monitoring.SMTP', new=mock_SMTP)
    mock_SMTP.return_value.__enter__.return_value.send_message.side_effect = SMTPException

    siteidentity_proxy_monitoring.send_alert(
        'test message',
        'email@example.com'
    )

    mock_logging.error.assert_called_once_with(
        'Could not send email alert to %s', 'email@example.com')

您可能会发现我编写测试的方式比测试本身更有趣-我创建了python library来帮助我使用语法。

这是我以系统的方式解决您的问题的方式:

我们从所需的测试调用和我的帮助程序库开始,以生成对引用的模块和类的模拟,同时还准备断言:

from mock_autogen.pytest_mocker import PytestMocker

import siteidentity_proxy_monitoring

def test_logging_when_email_fails(mocker):
    print(PytestMocker(siteidentity_proxy_monitoring).mock_referenced_classes().mock_modules().prepare_asserts_calls().generate())
    siteidentity_proxy_monitoring.send_alert(
        'test message',
        'email@example.com'
    )

现在,由于我们尚未嘲笑SMTP,因此测试显然失败了,但是打印输出非常有用:

# mocked modules
mock_logging = mocker.MagicMock(name='logging')
mocker.patch('siteidentity_proxy_monitoring.logging', new=mock_logging)
# mocked classes
mock_SMTP = mocker.MagicMock(name='SMTP', spec=siteidentity_proxy_monitoring.SMTP)
mocker.patch('siteidentity_proxy_monitoring.SMTP', new=mock_SMTP)
mock_SMTPException = mocker.MagicMock(name='SMTPException', spec=siteidentity_proxy_monitoring.SMTPException)
mocker.patch('siteidentity_proxy_monitoring.SMTPException', new=mock_SMTPException)
# calls to generate_asserts, put this after the 'act'
import mock_autogen
print(mock_autogen.generator.generate_asserts(mock_logging, name='mock_logging'))
print(mock_autogen.generator.generate_asserts(mock_SMTP, name='mock_SMTP'))
print(mock_autogen.generator.generate_asserts(mock_SMTPException, name='mock_SMTPException'))

我将相关的模拟放置在测试调用之前,并在之后添加了generate asserts调用:

def test_logging_when_email_fails(mocker):
    mock_logging = mocker.MagicMock(name='logging')
    mocker.patch('siteidentity_proxy_monitoring.logging', new=mock_logging)
    mock_SMTP = mocker.MagicMock(name='SMTP',
                                 spec=siteidentity_proxy_monitoring.SMTP)
    mocker.patch('siteidentity_proxy_monitoring.SMTP', new=mock_SMTP)

    siteidentity_proxy_monitoring.send_alert(
        'test message',
        'email@example.com'
    )

    import mock_autogen
    print(mock_autogen.generator.generate_asserts(mock_logging,
                                                  name='mock_logging'))
    print(mock_autogen.generator.generate_asserts(mock_SMTP, name='mock_SMTP'))

这次,测试执行给了我一些有用的断言:

mock_logging.assert_not_called()
assert 1 == mock_SMTP.call_count
mock_SMTP.assert_called_once_with('localhost')
mock_SMTP.return_value.__enter__.assert_called_once_with()
mock_SMTP.return_value.__enter__.return_value.send_message.assert_called_once_with('test message')
mock_SMTP.return_value.__exit__.assert_called_once_with(None, None, None)

因为我们没有抛出SMTPException,所以我们从未进入日志记录部分!幸运的是,我们可以以较小的方式更改一个断言,以创建所需的副作用(这是正常进行大量猜测的工具,该工具非常有帮助):

def test_logging_when_email_fails(mocker):
    mock_logging = mocker.MagicMock(name='logging')
    mocker.patch('siteidentity_proxy_monitoring.logging', new=mock_logging)
    mock_SMTP = mocker.MagicMock(name='SMTP',
                                 spec=siteidentity_proxy_monitoring.SMTP)
    mocker.patch('siteidentity_proxy_monitoring.SMTP', new=mock_SMTP)
    mock_SMTP.return_value.__enter__.return_value.send_message.side_effect = SMTPException

    siteidentity_proxy_monitoring.send_alert(
        'test message',
        'email@example.com'
    )

    import mock_autogen
    print(mock_autogen.generator.generate_asserts(mock_logging,
                                                  name='mock_logging'))
    print(mock_autogen.generator.generate_asserts(mock_SMTP,
                                                  name='mock_SMTP'))

生成器为嘲笑日志创建了正确的模仿物:

mock_logging.error.assert_called_once_with('Could not send email alert to %s', 'email@example.com')

您还获得了更精确的断言,可用于确保您测试的代码重试:

from mock import call
mock_SMTP.return_value.__enter__.return_value.send_message.assert_has_calls(
    calls=[call('test message'), call('test message'), ])

这就是我最初放置代码的方式!