1

I built a user invitation system in my Flask application and want to test it with Python unittest. I'm using SendGrid to send emails. How can I stop SendGrid from actually sending an email while in a test? I can I receive the user invitation link and pull it into my test? My code is below:

# views.py
@app.route('/add_user', methods=['GET', 'POST'])
@login_required
@groups_required(['site_admin'])
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')

        # catch and display SendGrid errors
        except SendGridClientError as err:
            flash(err.message.get('message'))
        except SendGridServerError as err:
            flash(err.message.get('message'))
    return render_template('dashboard/add_user.html', form=form)


@app.route('/add_user_confirm/<token>', methods=['GET', 'POST'])
def add_user_confirm(token):
    """
    Decode invite token and create new user account
    """
    form = RegistrationForm()
    decoded = None
    try:
        ts = URLSafeTimedSerializer(app.config['SECRET_KEY'])
        decoded = ts.loads(token, max_age=86400)
        email = decoded[0]
    except:
        abort(404)

    if form.validate_on_submit():
        try:
            tenant_id = decoded[1]

            data = {}
            data['email'] = email
            data['password'] = request.form['password']

            # given_name and surname are required fields
            data['given_name'] = 'Anonymous'
            data['surname'] = 'Anonymous'

            # set tenant id and site_admin status
            data['custom_data'] = {
                'tenant_id': tenant_id,
                'site_admin': 'False'
            }

            # create account
            account = User.create(**data)

            # add user to tenant group
            account.add_group(tenant_id)

            # login user
            login_user(account, remember=True)

            # success redirect
            return render_template('account/add_user_complete.html')
        except StormpathError as err:
            flash(err.message.get('message'))

    elif request.method == 'POST':
        flash("Passwords don't match.")

    return render_template('account/add_user_setpassword.html',
                           form=form,
                           email=email)

# tests.py
def test_add_user(self):
    resp = self.client.post('/add_user', data={
            'email': self.test_email
        }, follow_redirects=True)
assert 'User invitation sent' in resp.data
Casey
  • 2,611
  • 6
  • 34
  • 60

1 Answers1

1

So this is a pretty perfect case for mocking. The essential idea behind mocking is that you replace the current functionality (in this case, sending an e-mail) with a mundane pass, fail, side-effect.

If you're using Python 2.7 you'll have to download the mock third party package

Otherwise you can just say

from unittest import mock

In this particular case I'd suggest using mock.patch

# tests.py
from unittest import mock

@mock.patch("sg.send") 
def test_add_user(self, mocked_send):
    mocked_send.return_value = None # Do nothing on send
    resp = self.client.post('/add_user', data={
            'email': self.test_email
        }, follow_redirects=True)
assert 'User invitation sent' in resp.data

This will make it so that when sg.send is called it'll call the mocked function rather than the real function. Since mocks are blank by default no scary e-mail sending.

NOTE: SendGrid needs to be imported and in scope in your test.py file.

AlexLordThorsen
  • 8,057
  • 5
  • 48
  • 103
  • This is great. I've never heard of mock before. I'm trying to get it working but am running into 'ImportError: Here is where I setup 'sg': https://github.com/caseydm/photog/blob/master/project/tests/test_views.py#L63 – Casey Oct 23 '15 at 02:10
  • I don't have time to look at your code tonight but I'll try to run it tomorrow and report back. If I forget, poke me. – AlexLordThorsen Oct 23 '15 at 03:45
  • I ended up asking as a separate question and got a great response. Thanks! http://stackoverflow.com/questions/33311001/how-to-mock-a-sendgrid-method-in-python – Casey Oct 24 '15 at 10:11