【问题标题】:How to mock a SendGrid method in Python如何在 Python 中模拟 SendGrid 方法
【发布时间】:2015-10-23 20:37:30
【问题描述】:

我试图在我的 Flask 视图函数中模拟 SendGrid 方法,以便它在测试期间不会发送电子邮件。当我运行以下代码时,我收到错误“ImportError:没有名为 sg 的模块”。如何正确配置 'sg' 方法以便在测试中找到它?

# test_helpers.py
from unittest import TestCase
from views import app

class PhotogTestCase(TestCase):

    def setUp(self):
        app.config['WTF_CSRF_ENABLED'] = False
        app.config['TESTING'] = True
        self.app = app
        self.client = app.test_client()

# test_views.py
import mock
from test_helpers import PhotogTestCase
import sendgrid

class TestAddUser(PhotogTestCase):

    sg = sendgrid.SendGridClient(app.config['SENDGRID_API_KEY'])

    @mock.patch('sg.send')
    def test_add_user_page_loads(self, mocked_send):
        mocked_send.return_value = None  # Do nothing on send

        resp = self.client.post('/add_user', data={
                'email': 'joe@hotmail.com'
            }, follow_redirects=True)
        assert 'Wow' in resp.data

# views.py
import sendgrid
from itsdangerous import URLSafeTimedSerializer
from flask import Flask, redirect, render_template, \
    request, url_for, flash, current_app, abort
from flask.ext.stormpath import login_required
from forms import RegistrationForm, AddContactForm, \
    AddUserForm

@app.route('/add_user', methods=['GET', 'POST'])
@login_required
def add_user():
    """
    Send invite email with token to invited user
    """
    form = AddUserForm()

    if form.validate_on_submit():

        # token serializer
        ts = URLSafeTimedSerializer(app.config['SECRET_KEY'])

        email = request.form['email']
        tenant_id = user.custom_data['tenant_id']

        # create token containing email and tenant_id
        token = ts.dumps([email, tenant_id])

        # create url with token, e.g. /add_user_confirm/asdf-asd-fasdf
        confirm_url = url_for(
            'add_user_confirm',
            token=token,
            _external=True)

        try:
            # sendgrid setup
            sg = sendgrid.SendGridClient(
                app.config['SENDGRID_API_KEY'],
                raise_errors=True
            )

            # email setup
            message = sendgrid.Mail(
                to=request.form['email'],
                subject='Account Invitation',
                html='You have been invited to set up an account on PhotogApp. Click here: ' + confirm_url,
                from_email='support@photogapp.com'
            )

            # send email
            status, msg = sg.send(message)

            flash('Invite sent successfully.')
            return render_template('dashboard/add_user_complete.html')

    return render_template('dashboard/add_user.html', form=form)

【问题讨论】:

    标签: python unit-testing flask mocking


    【解决方案1】:

    说明

    Mocking 必须在您测试的地方实现,而不是在您实现方法的地方。或者,在您的情况下,从 unittest 模拟 sg 对象将不起作用。

    所以,我不确定您的项目的结构是什么。但希望这个例子有所帮助。

    您需要确保您还引用了要模拟的该类所在的适当位置,以正确模拟其方法。

    解决方案

    所以,让我们假设您正在从 test.py 运行测试:

    test.py
        your_app/
            views.py
        tests/
            all_your_tests.py
    

    在 views.py 中,您正在像这样导入发送:

    from module_holding_your_class import SendGridClient
    

    所以,看看你的 mock.patch,它应该是这样的:

    @mock.patch('your_app.views.SendGridClient.send')
    def test_add_user_page_loads(self, mocked_send):
    

    如您所见,您是从 test.py 运行的,因此您的导入是从那里引用的。这是我建议在实际运行真实代码的位置运行测试的地方,这样你就不必搞乱你的导入。

    此外,您正在嘲笑您在views.py 中调用的send

    应该可以。让我知道这是怎么回事。

    额外信息:模拟类的实例

    因此,根据您的代码,如果您实际模拟了您的类的一个实例,它可能对您更有利。这样,您可以非常轻松地在 SendGridClient 甚至 Mail 实例的单个模拟中测试您的所有方法。这样您就可以专注于方法的显式行为,而不必担心外部功能。

    要完成模拟一个类的实例(或者在你的情况下是两个),你必须做这样的事情(内联解释)

    *此具体示例未经测试,可能不完整。目标是让您了解如何操作模拟和数据以帮助您进行测试。

    再往下看,我有一个经过全面测试的示例。*

    @mock.patch('your_app.views.Mail')
    @mock.patch('your_app.views.SendGridClient')
    def test_add_user_page_loads(self, m_sendgridclient, m_mail):
        # get an instance of Mock()
        mock_sgc_obj = mock.Mock()
        mock_mail_obj = mock.Mock()
    
        # the return of your mocked SendGridClient will now be a Mock()
        m_sendgridclient.return_value = mock_sgc_obj
        # the return of your mocked Mail will now be a Mock()
        m_mail.return_value = mock_mail_obj
    
        # Make your actual call
        resp = self.client.post('/add_user', data={
                'email': 'joe@hotmail.com'
            }, follow_redirects=True)
    
        # perform all your tests
        # example
        self.assertEqual(mock_sgc_obj.send.call_count, 1)
        # make sure that send was also called with an instance of Mail.
        mock_sgc_obj.assert_called_once_with(mock_mail_obj)
    

    根据您提供的代码,我不确定 Mail 究竟返回了什么。我假设它是Mail 的对象。如果是这种情况,那么上面的测试用例就足够了。但是,如果您要测试 message 本身的内容并确保每个对象属性中的数据是正确的,我强烈建议您分离单元测试以在 Mail 类中处理它并确保数据是表现如预期。

    这个想法是您的add_user 方法不应该关心验证该数据。只是对对象进行了调用。

    此外,在您的发送方法本身中,您可以在其中进一步进行单元测试,以确保您输入到该方法的数据得到相应处理。这将使您的生活更轻松。

    示例

    这是我整理的一个经过测试的示例,希望有助于进一步澄清这一点。您可以将其复制粘贴到您的编辑器并运行它。注意我使用__main__,它是为了表明我从哪里嘲笑。在这种情况下,它是__main__

    另外,我会使用side_effectreturn_value(看看我的例子)来看看两者之间的不同行为。 side_effect 将返回被执行的东西。在您的情况下,您希望查看执行方法 send 时会发生什么。

    每个单元测试都以不同的方式模拟并展示您可以应用的不同用例。

    import unittest
    from unittest import mock
    
    
    class Doo(object):
        def __init__(self, stuff="", other_stuff=""):
            pass
    
    
    class Boo(object):
        def d(self):
            return 'the d'
    
        def e(self):
            return 'the e'
    
    
    class Foo(object):
    
        data = "some data"
        other_data = "other data"
    
        def t(self):
            b = Boo()
            res = b.d()
            b.e()
            return res
    
        def do_it(self):
            s = Stuff('winner')
            s.did_it(s)
    
        def make_a_doo(self):
            Doo(stuff=self.data, other_stuff=self.other_data)
    
    
    class Stuff(object):
        def __init__(self, winner):
            self.winner = winner
    
        def did_it(self, a_var):
            return 'a_var'
    
    
    class TestIt(unittest.TestCase):
    
        def setUp(self):
            self.f = Foo()
    
        @mock.patch('__main__.Boo.d')
        def test_it(self, m_d):
            '''
                note in this test, one of the methods is not mocked.
            '''
            #m_d.return_value = "bob"
            m_d.side_effect = lambda: "bob"
    
            res = self.f.t()
    
            self.assertEqual(res, "bob")
    
        @mock.patch('__main__.Boo')
        def test_them(self, m_boo):
            mock_boo_obj = mock.Mock()
            m_boo.return_value = mock_boo_obj
    
            self.f.t()
    
            self.assertEqual(mock_boo_obj.d.call_count, 1)
            self.assertEqual(mock_boo_obj.e.call_count, 1)
    
        @mock.patch('__main__.Stuff')
        def test_them_again(self, m_stuff):
            mock_stuff_obj = mock.Mock()
            m_stuff.return_value = mock_stuff_obj
    
            self.f.do_it()
    
            mock_stuff_obj.did_it.assert_called_once_with(mock_stuff_obj)
            self.assertEqual(mock_stuff_obj.did_it.call_count, 1)
    
        @mock.patch('__main__.Doo')
        def test_them(self, m_doo):
    
            self.f.data = "fake_data"
            self.f.other_data = "some_other_fake_data"
    
            self.f.make_a_doo()
    
            m_doo.assert_called_once_with(
                stuff="fake_data", other_stuff="some_other_fake_data"
            )
    
    if __name__ == '__main__':
        unittest.main()
    

    【讨论】:

    • 这太完美了,谢谢!我没有意识到我需要参考在views.py 中导入的实际方法。有没有办法捕获 sg.send(message) 的消息部分中的数据?
    • @Casey 告诉我你对我的更新的看法。这里只是一个关于您希望如何查看消息内容的旁注。您应该看看如何使用固定装置来设置示例模板,并确保您的方法根据您的期望相应地处理数据。让我知道这是否有意义。
    • 这是有道理的,非常有帮助。我仍在研究如何构建我的测试,因此将考虑将我的初始用户设置移动到固定装置中。
    • @Casey。看看,我又更新了。当您想要测试“消息”数据时,它实际上可以帮助您。你可以换个方式想。您可以测试以确保使用正确的参数调用 Mail。因此,如果您查看我更新的示例并查看def test_them 的测试用例。你会看到我在做什么。我只是在测试是否使用正确的参数调用了该类。我认为这也应该对您有所帮助。
    猜你喜欢
    • 2014-08-21
    • 2015-08-23
    • 2020-06-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多