Search code examples
pythonflaskflask-wtformsflask-testing

How to send QuerySelectField form data to a Flask view in a unittest?


I am trying to test edit and add views in a flask application I am working on. A version of the website is deployed and the views are working correctly, but the tests I am making are do not seem to be passing the QuerySelectField data correctly. Also when in the test I check to see if the form data validates and it does, so it should be going through.

Below is the test:

class TestingWhileLoggedIn(TestCase):
    def create_app(self):
        app = c_app(TestConfiguration)
        return app

    # executed prior to each test
    def setUp(self):
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

        login(self.client, '******', '******')

    # executed after each test
    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

        logout(self.client)

    def test_add_post_page_li(self):
        p_cat = PostCategory(name='Resources')
        p_cat1 = PostCategory(name='Ressdgources')
        p_cat2 = PostCategory(name='Ressdgsdgources')
        p_cat3 = PostCategory(name='Reurces')
        db.session.add(p_cat)
        db.session.add(p_cat1)
        db.session.add(p_cat2)
        db.session.add(p_cat3)
        db.session.commit()

        all_cats = PostCategory.query.all()

        self.assertEqual([p_cat,p_cat1,p_cat2,p_cat3], all_cats)

        response = self.client.get('/add_post', follow_redirects=False)
        self.assertEqual(response.status_code, 200)

        data = dict(title='Hello', content='fagkjkjas', category=p_cat)

        form = PostForm(data=data)

        # this test passes!
        self.assertEqual(form.validate(), True)

        # printing the data to see what it is
        print(form.data)

        response_1 = self.client.post('/add_post', follow_redirects=False, data=form.data, content_type='multipart/form-data')

        # this one fails
        self.assertEqual(response_1.status_code, 302)

        new_post = db.session.query(Post).filter_by(name='Hello').first()

        self.assertNotEqual(new_post, None)

Below is the terminal output from the tests. The last two fails are the same issue as the one I am posting about so I left them out.

.......................................{'title': None, 'category': None, 'content': None, 'submit': False}
{'title': 'Hello', 'category': <PostCategory 'Resources'>, 'content': 'fagkjkjas', 'submit': False}
{'title': 'Hello', 'category': None, 'content': 'fagkjkjas', 'submit': True}
F.F.......F..
======================================================================
FAIL: test_add_post_page_li (__main__.TestingWhileLoggedIn)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests.py", line 165, in test_add_post_page_li
    self.assertEqual(response_1.status_code, 302)
AssertionError: 200 != 302

The dictionary print outs are from some print statements I injected to help me understand the problem. The first dictionary is when no form is submitted to the add_post view, the second is from the test where it shows the category field filled in and the last one is from the add_post view which shows the category unfilled.

Below is the add_post view:

@blogs.route('/add_post', methods=['GET', 'POST'])
def add_post():
    """
    Add a blog post
    """
    if not session.get('logged_in'):
        return redirect(url_for('other.home'))

    form = PostForm()

    print(form.data)

    if form.validate_on_submit():
        new_post = Post(name=form.title.data, content=form.content.data, category_id=form.category.data.id, category=form.category.data)

        print('hello')

        try:
            db.session.add(new_post)
            db.session.commit()
        except:
            # not the best behaviour and should change
            return redirect(url_for('other.home'))

        return redirect(url_for('other.home'))

    return render_template('add_post.html', form=form)

and here is the forms.py file that contains the PostForm

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from ..models import PostCategory

#
# This function is designed to obtain choices for the categories in the PostForm.
#
def category_choices() :
    return PostCategory.query



#
# PostForm
# Purpose:
#     Gives the user a way to input information into the post table.
#
# Fields:
#     Title: String Field (Required)
#     Category: QuerySelectField (Required)
#       Obtains the categories from the database. As of right now there exists
#       only two categories (Travel and Projects)
#
#     Content: Text Field (Required)
#     Submit: Submit Field
#
class PostForm(FlaskForm):
    """
    Form that lets the user add a new post
    """

    title = StringField('Title', validators=[DataRequired()])
    category = QuerySelectField('Category', validators=[DataRequired()], query_factory=category_choices)
    content = TextAreaField('Content', validators=[DataRequired()])

    submit = SubmitField('Submit')


#
# PostCategoryForm
# Purpose:
#     allows user to add new subcategories
#
# Fields:
#     Name: String Field (Required)
#     Submit: SubmitField
#
class PostCategoryForm(FlaskForm) :
    """
    Form used to submit new subcategories
    """
    name = StringField('Name', validators=[DataRequired()])

    submit = SubmitField('Submit')

Below is the config file

import os
from os.path import abspath, dirname, join

# _cwd = dirname(abspath(__file__))

_basedir = os.path.abspath(os.path.dirname(__file__))


TOP_LEVEL_DIR = os.path.abspath(os.curdir)

class Config(object) :
    pass

class BaseConfiguration(object):
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProductionConfiguration(BaseConfiguration):
    SQLALCHEMY_DATABASE_URI = '**********************'
    SQLALCHEMY_POOL_PRE_PING = True
    SQLALCHEMY_ENGINE_OPTIONS = {'pool_recycle' : 3600}
    SECRET_KEY = '******************'
    UPLOAD_FOLDER = TOP_LEVEL_DIR + '/app/static'


class TestConfiguration(BaseConfiguration):
    TESTING = True
    WTF_CSRF_ENABLED = False

    SECRET_KEY = '*************'

    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(_basedir, 'testing.sqlite')

What it looks like to me is that wtforms is not sending the QuerySelectView when in the testing environment but I do not know why. Any help is appreciated.

Edit: In my original question I did not make it clear that this is only a problem with forms that have a QuerySelectField. Forms that do not have a QuerySelectField are working and passing all their tests.


Solution

  • A helpful person on the Flask discord server was able to answer this for me.

    The problem is that Flask-wtforms does not pass entire instances of a model, and instead only passes the primary key. The solution is to pass only the primary key in the data dictionary like below:

    class TestingWhileLoggedIn(TestCase):
        def create_app(self):
            app = c_app(TestConfiguration)
            return app
    
        # executed prior to each test
        def setUp(self):
            self.app_context = self.app.app_context()
            self.app_context.push()
            db.create_all()
    
            login(self.client, '******', '*****')
    
        # executed after each test
        def tearDown(self):
            db.session.remove()
            db.drop_all()
            self.app_context.pop()
    
            logout(self.client)
    
        def test_add_post_page_li(self):
            p_cat = PostCategory(name='Resources')
            p_cat1 = PostCategory(name='Ressdgources')
            p_cat2 = PostCategory(name='Ressdgsdgources')
            p_cat3 = PostCategory(name='Reurces')
            db.session.add(p_cat)
            db.session.add(p_cat1)
            db.session.add(p_cat2)
            db.session.add(p_cat3)
            db.session.commit()
    
            all_cats = PostCategory.query.all()
    
            self.assertEqual([p_cat,p_cat1,p_cat2,p_cat3], all_cats)
    
            response = self.client.get('/add_post', follow_redirects=False)
            self.assertEqual(response.status_code, 200)
    
            # the following line was changed from having category=p_cat to
            # category=p_cat.id
            data = dict(title='Hello', content='fagkjkjas', category=p_cat.id)
    
            #
            # The following code has been commented out since it is no longer needed
            #
            # form = PostForm(data=data)
            #
            # this would not pass anymore
            # self.assertEqual(form.validate(), True)
            #
            # printing the data to see what it is
            # print(form.data)
    
    
            # This line was changed from having data=form.data to data=data
            response_1 = self.client.post('/add_post', follow_redirects=False, data=data, content_type='multipart/form-data')
    
            # this one fails
            self.assertEqual(response_1.status_code, 302)
    
            new_post = db.session.query(Post).filter_by(name='Hello').first()
    
            self.assertNotEqual(new_post, None)