简体   繁体   中英

Testing Flask-WTF forms that use `FieldList` and `FormField`

I wrote some forms using flask-wtf that use FieldList and FormField , and I want to test them using pytest . It's a bulk insertion of data from an uploaded CSV

These are my forms:

# myapp/admin/forms.py

from wtforms import Form as NoCsrfForm

class SimpleRegistrationForm(NoCsrfForm):

    email = StringField('Email')
    username = StringField('Username')
    password = StringField('Password')

from flask_wtf import FlaskForm    

class BulkUserCreationForm(FlaskForm):

    users = FieldList(FormField(SimpleRegistrationForm))
    submitted = HiddenField()
    submit = SubmitField('Register all')

    def is_submitted(self):
        from flask import request
        return super().is_submitted() and 'submitted' in request.form

Note that I'm skipping imports and other stuff. Also, I used submitted to stop my @app.route from passing validate_on_submit() .

This is a part of my test:

# a part of a test
# (...) mumble mumble
        from myapp.admin.forms import (
            BulkUserCreationForm, SimpleRegistrationForm)

        usr_form_1 = SimpleRegistrationForm(username="user1",
                                            email="user1@mail.com",
                                            password="pwd1",)

        usr_form_2 = SimpleRegistrationForm(username="user2",
                                            email="user2@mail.com",
                                            password="pwd2",)

        usr_form_full = BulkUserCreationForm(
            users=[usr_form_1, usr_form_2])

        # user issues a POST request
        rv = client.post(
            url_for('bulk-import-users.edit_users')
            follow_redirects=True,
            data=usr_form_full.data)

        assert something_happened()

I'm struggling on how to craft the data argument of post() . So far I've read three approaches

  • This solution uses data as tuples, but I don't understand the binding between tuples and forms for something like BulkUserCreationForm.users
  • This solution relies on usr_form_full.data of a form instance, which returns a dictionary. In the mentioned SO answer, it seems to work, but in my case (and for the code I show) I'm getting an error of the form:

     /src/venv/lib/python3.6/site-packages/werkzeug/test.py:349: DeprecationWarning: it's no longer possible to pass dicts as `data`. Use tuples or FileStorage objects instead
  • Googling this error returned this (and not much more). This solution uses hardcoded values, so instead I went for something (I believe) more or less better:

     data_full = {field.label.field_id: field.data for form in usr_form_full.users for field in form}

and passed this to the data attribute. It didn't work. For some reason the rendered .data attribute does not behave like expected, returning a different repr (I expected to see the actual value ).

    >>> print(data_full)
    {'users-0-email': <wtforms.fields.core.StringField object at 0x7f1704f68cc0>, 'users-0-username': <wtforms.fields.core.StringField object at 0x7f1704fa5e10>, 'users-0-password': <wtforms.fields.core.StringField object at 0x7f1704f539e8>, 'users-1-email': <wtforms.fields.core.StringField object at 0x7f1704f26a90>, 'users-1-username': <wtforms.fields.core.StringField object at 0x7f1704f26f98>, 'users-1-password': <wtforms.fields.core.StringField object at 0x7f1704f26828>}

In short, none of the above approaches worked in my test. What's the right approach? Do I need to pass submit and submitted values to my form instance as well?

For anyone who lands here and is trying to pytest the FieldList/FormField combination, which foiled me for much too long, here's how I handled it in my Flask app.

I eventually realized that the form information being sent to my code by the POST request could be accessed using request.form . So I put in return request.form in my endpoint function just after the form was submitted, which let me view what the page was returning as a dictionary. It looked something like this:

{
    "my_fields-0-help_text": "test", 
    "my_fields-1-help_text": "", 
    "my_fields-2-help_text": ""
}

WTForms takes the name of the field list and attaches a hyphen, a numerical identifier, another hyphen, and the attribute of the FormField.

So this is a simplified version of what I needed to test:

class MyFieldForm(FlaskForm):
    help_text = TextAreaField()

    def __init__(self, field_id=None, field_name=None, *args, **kwargs):
        super(MyFieldForm, self).__init__(*args, **kwargs)
        self.field_id = field_id
        self.field_name = field_name

class MyHelpTextForm(FlaskForm):
    my_fields = FieldList(FormField(MyFieldForm))
    submit = SubmitField('Submit Changes')

And this is a simplified version of the working test I used:

def test_page(client):
    with client as c:
        obj = get_obj(ParentObject) # Function written elsewhere to get the parent object
        my_field = get_obj(my_field, obj) # Function written elsewhere to get the child object
        assert my_field.parent == obj
        assert not my_field.help_text
        vals = {
            'my_fields-0-help_text': 'help text'
        }
        response = c.post('path/to/page', data=vals, follow_redirects=True)
        assert b'help text' in response.data
        assert my_field.help_text == 'help text'

In my case I wanted to test that the parameters passed to my form get passed to a downstream function.

    form = MyForm()
    if form.validate_on_submit():
       args = unpack_form(form)
       do_something(args)

I had the same issue with mocking nested FormField values, so the solution I came up with is to provide a data dictionary where keys are the fields of your form. If a field is a FormField add a dash between parent field name and nested form field name:

{"parent_form_field-nested_form_field" : "value"}

For example to test this

class ContactDetailsForm(FlaskForm):
    phone_number = StringField("Phone number")
    email = StringField("Email")


class UserForm(FlaskForm):
    name = StringField("Name")
    work_contact = FormField(ContactDetailsForm)
    home_contact = FormField(ContactDetailsForm)

Write the following mock

def test_user_form(app):
    data = {
        "name": "John",
        "work_contact-phone_number": "1234",
        "work_contact-email": "john@work.com",
        "home_contact-phone_number": "5678",
        "home_contact-email": "john@home.com"
    }
    app.post("/user_form", data=data)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM