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)
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.
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.
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.
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With