Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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. When I run the below code I get an error 'ImportError: No module named sg'. How can I properly configure the 'sg' method so it is found in testing?

# 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': '[email protected]'
            }, 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='[email protected]'
            )

            # 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)
like image 454
Casey Avatar asked Oct 23 '15 20:10

Casey


1 Answers

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.

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
    your_app/
        views.py
    tests/
        all_your_tests.py

Inside views.py, you are importing send like this:

from module_holding_your_class import SendGridClient

So, to look at your mock.patch, it should look like this:

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

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. 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': '[email protected]'
        }, 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. I am assuming it is an object of 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.

The idea is that your add_user method should not care about validating that data yet. 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. 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. In this case it is __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_effect will return something that gets executed. 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()
like image 183
idjaw Avatar answered Sep 23 '22 10:09

idjaw