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