Search code examples
unit-testingflaskpytestwerkzeug

Unit testing a Flask form containing multiple submit buttons


I am writing unit tests for a form validation method in a Flask application that contains several different Submit buttons to control logical flow.

The form validation method expects to receive an ImmutibleMultiDict object that includes the button name and value like ('btn', 'Save') or ('btn', 'Update')or ('btn', 'Delete'). Unfortunately, I can't figure out how to mock or provide the different button responses in pytest.

Below is example code from the form validate method with some different actions depending on which button was used in submit (either 'Update' or 'Save'):

def validate(self):
    if request.form['btn'] == 'Update':
            if cn_continent_name and en_continent_name:
                flash('You have not made a change. There is nothing to update.', 'warning')
                return False
            if not _check_clean_chinese():
                return False

    if request.form['btn'] == 'Save':
            # check if Chinese name already exists in the DB
            if cn_continent_name:
                self.cn_name.errors.append("Chinese Continent Name already registered")
                return False
            # check the if English name already exists in the DB
            en_continent_name = ContinentsTable.query.filter_by(en_name=self.en_name.data).first()
            if en_continent_name:
                self.en_name.errors.append("English Country Name already registered")
                return False

The below test of the form validation method is not working because there is missing button name-value information to match up to the form validation logic under test, which expects to check for the presence of request.form['btn'] = 'Save' or request.form['btn'] = 'Update'.

class TestContinentsForm:
"""Continents form."""

def test_validate_continent_cn_name_already_registered(self, continent):
    """Enter Continent cn_name that is already registered."""
    form = ContinentsForm(cn_name=continent.cn_name, en_name='NewEngName')
    assert form.validate() is False
    assert 'Chinese Continent Name already registered' in form.cn_name.errors

Below is the test fail with error code and the reason it has an error is because the validation is expecting a werkzeug ImmutibleMutltiDict object that includes the name of the button that was used to submit the form, but I have not properly provided the button name in the ImmutibleMultiDict object.

I've tried dozens of things but commented out in below test is one example request.form.add('btn','Save') which doesn't work because can't modify the ImmutibleMutliDict object directly:

self = <tests.test_forms.TestContinentsForm object at 0x10f8be908>
continent = Asia, 亚洲, yà zhōu!

def test_validate_continent_cn_name_already_registered(self, continent):
    """Enter Continent cn_name that is already registered."""
    form = ContinentsForm(cn_name=continent.cn_name, en_name='NewEngName')
    #request.form.add('btn','Save')
    #assert 'Save' in request.form
    >assert form.validate() is False

test_forms.py:96: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../aoscrdb_app/user/forms/locations/continents_form.py:70: in validate
if 'Delete' in request.form['btn']:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = ImmutableMultiDict([]), key = 'btn'

def __getitem__(self, key):
    """Return the first data value for this key;
        raises KeyError if not found.

        :param key: The key to be looked up.
        :raise KeyError: if the key does not exist.
        """
    if key in self:
        return dict.__getitem__(self, key)[0]
    >raise exceptions.BadRequestKeyError(key)
   E werkzeug.exceptions.HTTPException.wrap.<locals>.newcls: 400: Bad Request

../venv/lib/python3.5/site-packages/werkzeug/datastructures.py:402: BadRequestKeyError

To properly test the form validation, the ImmutableMultiDict object should look like this including the ('btn', 'Save') data:

This is reqest.form =>ImmutableMultiDict([('cn_name', '中地'), ('btn', 'Save'), 
('en_name', 'Middle Earth'), 
('csrf_token', '1455956207##90932fcb2d1481be007f90e32040b6aba3e5fe68')])

I am using pytest and factory-boy and below is the relevant pytest fixture and factory. I've tried creating other pytest fixtures that include the button data but that also has not worked for me:

@pytest.fixture()
def continent(db):
    """A continent for the tests."""
    continent = ContinentFactory()
    db.session.commit()
    return continent

class ContinentFactory(BaseFactory):
"""Continent factory."""
cn_name = '亚洲'
en_name = 'Asia'

class Meta:
    """Factory configuration."""
    model = ContinentsTable

I believe the buttons should be stored in a dictionary like {'btn': 'Save'} and made accessible to the test framework but I can't find the best way to implement. Thanks!


Solution

  • If you're trying to test out your flask logic (including Form behavior), Flask has a built in way of doing that already and you can inject your own POST, GET values: http://flask.pocoo.org/docs/0.10/testing/

    But it seems what you're trying to do is test specifically the validation logic of your form. In that case, you'll want to do is modify the request context and inject your button values into request.form (essentially replace the ImmutableMultiDict() with your own). This has to be done within a request context. See the link above.

    Below is some sample code that shows how to achieve this:

    Form

    import wtforms
    class SampleForm(wtforms.Form):
        btn = wtforms.fields.SubmitField('Cancel')
    
        def validate(self):
            if request.form['btn'] == 'Save':
                print('Saving...')
            elif request.form['btn'] == 'Update':
                print('Updating!')
            else:
                print('Some other btn action')
    

    Test

    from flask import Flask, request
    from werkzeug import ImmutableMultiDict
    
    def test_sample_form_validate():
        app = Flask(__name__)
        form = SampleForm()
        with app.test_request_context('/'):
            request.form = ImmutableMultiDict([('btn', 'Save')])
            form.validate() # Prints 'Saving...'
            request.form = ImmutableMultiDict([('btn', 'Update')])
            form.validate() # Prints 'Updating!'
    

    Running the test_sample_form_validate function should print 'Saving...' followed by 'Updating!'. You will, of course, need to add the rest of your relevant data to the ImmutableMultiDict.