简体   繁体   English

如何在Python中模拟SendGrid方法

[英]How to mock a SendGrid method in Python

I'm trying to mock the SendGrid method within my Flask view function, so that it does not send an email during testing. 我正在尝试在我的Flask视图函数中模拟SendGrid方法,以便它在测试期间不发送电子邮件。 When I run the below code I get an error 'ImportError: No module named sg'. 当我运行下面的代码时,我得到一个错误'ImportError:没有名为sg的模块'。 How can I properly configure the 'sg' method so it is found in testing? 如何正确配置'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)

Explanation 说明

Mocking has to be implemented with respect to where you are testing, and not where you have implemented the method. 必须针对您测试的位置实施模拟,而不是实现方法的位置。 Or, also in your case, mocking the sg object from unittest will not work. 或者,在您的情况下,从unittest模拟sg对象将不起作用。

So, I am not exactly sure what the structure of your project is. 所以,我不确定你的项目结构是什么。 But hopefully this example helps. 但希望这个例子有所帮助。

You need to make sure that you are also referencing the appropriate location of where that class is that you want to mock out, to properly mock out its methods. 您需要确保您还引用了要模拟的类的适当位置,以正确模拟其方法。

Solution

So, let us assume you are running your tests from test.py: 那么,让我们假设您正在从test.py运行测试:

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

Inside views.py, you are importing send like this: 在views.py中,您导入的发送方式如下:

from module_holding_your_class import SendGridClient

So, to look at your mock.patch, it should look like this: 所以,看看你的mock.patch,它应该是这样的:

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

As you can see, you are running from test.py, so your imports are with reference from there. 如您所见,您从test.py运行,因此您的导入是从那里引用的。 This is where I suggest running your tests with respect to where you actually run your real code, so that you don't have to mess around with your imports. 这就是我建议你在实际运行实际代码的地方运行测试的地方,这样你就不必乱用你的导入了。

Furthermore, you are mocking the send that you are calling in views.py. 此外,您正在模拟您在views.py中调用的send

That should work. 这应该工作。 Let me know how that goes. 让我知道这是怎么回事。

Extra Info: Mocking instance of a Class 额外信息:模拟类的实例

So, based on your code, it would probably be more beneficial for you if you actually mocked out an instance of your class. 因此,根据您的代码,如果您实际模拟了类的实例,那么对您来说可能会更有益。 This way you can very easily test all your methods within that single mock of the instance of SendGridClient , or even Mail . 这样,您可以非常轻松地在SendGridClient实例的单个模拟中测试所有方法,甚至是Mail This way you can focus on the explicit behaviour of your method without worrying about functionality from externals. 这样,您可以专注于方法的显式行为,而无需担心外部功能。

To accomplish mocking out an instance of a Class (or in your case two), you will have to do something like this (explanation inline) 要完成模拟类的实例(或者在你的情况下是两个),你将不得不做这样的事情(内联解释)

*This specific example is untested and probably not complete. *此具体示例未经测试,可能不完整。 The goal is to get you to understand how to manipulate the mock and the data to help your testing. 目标是让您了解如何操作模拟和数据以帮助您进行测试。

Further down below I have a fully tested example to play around with.* 再往下面,我有一个经过充分测试的例子来玩。*

@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)

Based on the code that you provided, I am not sure exactly what Mail is returning. 根据您提供的代码,我不确定Mail究竟返回了什么。 I am assuming it is an object of Mail . 我假设它是Mail一个对象。 If that is the case, then the above test case would suffice. 如果是这种情况,那么上述测试用例就足够了。 However, if you are looking to test the content of message itself and make sure the data inside each of those object properties is correct, I strongly recommend separating your unittests to handle it in the Mail class and ensure that the data is behaving as expected. 但是,如果您要测试message本身的内容并确保每个对象属性中的数据都是正确的,我强烈建议将您的单元测试分开以在Mail类中处理它,并确保数据的行为符合预期。

The idea is that your add_user method should not care about validating that data yet. 这个想法是你的add_user方法不应该关心验证那些数据。 Just that a call was made with the object. 就是用对象调用了。

Furthermore, inside your send method itself, you can further unittest in there to make sure that the data you are inputting to the method is treated accordingly. 此外,在send方法本身内部,您可以在其中进一步进行单元测试,以确保相应地处理您输入到方法的数据。 This would make your life much easier. 这会让你的生活更轻松。

Example

Here is a example I put together that I tested that I hope will help clarify this further. 以下是我测试的一个例子,我希望这将有助于进一步澄清这一点。 You can copy paste this in to your editor and run it. 您可以将其粘贴到编辑器中并运行它。 Pay attention to my use of __main__ , it is to indicate where I am mocking from. 注意我对__main__使用,它是指示我在哪里嘲笑。 In this case it is __main__ . 在这种情况下,它是__main__

Also, I would play around with side_effect and return_value (look at my examples) to see the different behaviour between the two. 另外,我会使用side_effectreturn_value (查看我的示例)来查看两者之间的不同行为。 side_effect will return something that gets executed. side_effect将返回被执行的内容。 In your case you are wanting to see what happens when you execute the method send. 在您的情况下,您希望看到执行方法发送时会发生什么。

Each unittest is mocking in different ways and showcasing the different use cases you can apply. 每个单元测试都以不同的方式进行模拟,并展示您可以应用的不同用例。

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()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM