Search code examples
python-3.xflaskauthenticationresponsepytest

Unexpected AssertionError: single test not using logged in user from previous step


I am following the tutorial by http://www.patricksoftwareblog.com/flask-tutorial/, which I believe is based on https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world. Great stuff for a beginner.

I am getting different results when testing my code through frontend manually (which works fine) v.s. through pytest.

My test tries to show the "groups" endpoint which requires a login (standard @login_required decorator).

  • I initially test the user getting a login page ("Knock knock") when trying to get the endpoint without a login. This works manually and through pytest.
  • I login a user. If I inspect the response from the login I can clearly see a "Welcome back Pete!" success message.
  • My second assert receives a response from URL /login?next=%2Fgroups indicating the /groups endpoint is called without a login/authentication preceding it and the assert fails. Testing this manually works as expected. Why is that single test not using the same user/session combination in the next step(s)?

Test with the problem is the first snippet below:

def test_groups(app):
    assert b'Knock knock' in get(app, "/groups").data
    login(app, "[email protected]", "pete123")
    assert b'Test group 1' in get(app, "/groups").data

My "get" function for reference:

def get(app, endpoint: str):
    return app.test_client().get(endpoint, follow_redirects=True)

My "login" function for reference:

def login(app, email="[email protected]", password="testing"):
    return app.test_client().post('/login', data=dict(email=email, password=password), follow_redirects=True)

The app (from a conftest fixture imported in the test module by @pytest.mark.usefixtures('app')) for reference:

@pytest.fixture
def app():
    """An application for the tests."""
    _app = create_app(DevConfig)
    ctx = _app.test_request_context()
    ctx.push()

    yield _app

    ctx.pop()

The login route for reference:

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST':
        if form.validate_on_submit():
            user = User.query.filter_by(email=form.email.data).first()
            if user is not None and user.is_correct_password(form.password.data):
                user.authenticated = True
                user.last_login = user.current_login
                user.current_login = datetime.now()
                user.insert_user()
                login_user(user)
                flash(f'Welcome back {user.name}!', 'success')
                return redirect(url_for('our_awesome_group.index'))
            else:
                flash('Incorrect credentials! Did you already register?', 'error')
        else:
            flash_errors(form)
    return render_template('login.html', form=form)

The groups route for reference:

@app.route('/groups')
@login_required
def groups():
    groups_and_users = dict()
    my_group_uuids = Membership.list_groups_per_user(current_user)
    my_groups = [Group.query.filter_by(uuid=group).first() for group in my_group_uuids]
    for group in my_groups:
        user_uuids_in_group = Membership.list_users_per_group(group)
        users_in_group = [User.query.filter_by(uuid=user).first() for user in user_uuids_in_group]
        groups_and_users[group] = users_in_group
    return render_template('groups.html', groups_and_users=groups_and_users)

Solution

  • Im going to sum up the comments I made that gave the answer on how to solve this issue.

    When creating a test app using Pytest and Flask there are a few different ways to go about it.

    The suggested way to create a test client with proper app context is to use something like:

    @pytest.fixture
    def client():
        """ Creates the app from testconfig, activates test client and context then makes the db and allows the test client
        to be used """
    app = create_app(TestConfig)
    
    client = app.test_client()
    
    ctx = app.app_context()
    ctx.push()
    
    db.create_all()
    
    
    yield client
    
    db.session.close()
    db.drop_all() 
    ctx.pop()
    

    That creates the client while pushing the app context so you can register things like your database and create the tables to the test client.

    The second way is show in OP's question where use app.test_request context

    @pytest.fixture
    def app():
        """An application for the tests."""
        _app = create_app(DevConfig)
        ctx = _app.test_request_context()
        ctx.push()
    
        yield _app
    
        ctx.pop()
    

    and then create the test client in another pytest fixture

    @pytest.fixture 
    def client(app): 
       return app.test_client()
    

    Creating a test client allows you to use various testing features and gives access to flask requests with the proper app context.