Testing Flask Applications

Flask provides utilities for testing an application. This documentationgoes over techniques for working with different parts of the applicationin tests.

We will use thepytest framework to set up and run our tests.

$ pip install pytest

Thetutorial goes over how to write tests for100% coverage of the sample Flaskr blog application. Seethe tutorial on tests for a detailedexplanation of specific tests for an application.

Identifying Tests

Tests are typically located in thetests folder. Tests are functionsthat start withtest_, in Python modules that start withtest_.Tests can also be further grouped in classes that start withTest.

It can be difficult to know what to test. Generally, try to test thecode that you write, not the code of libraries that you use, since theyare already tested. Try to extract complex behaviors as separatefunctions to test individually.

Fixtures

Pytestfixtures allow writing pieces of code that are reusable acrosstests. A simple fixture returns a value, but a fixture can also dosetup, yield a value, then do teardown. Fixtures for the application,test client, and CLI runner are shown below, they can be placed intests/conftest.py.

If you’re using anapplication factory, define anappfixture to create and configure an app instance. You can add code beforeand after theyield to set up and tear down other resources, such ascreating and clearing a database.

If you’re not using a factory, you already have an app object you canimport and configure directly. You can still use anapp fixture toset up and tear down resources.

importpytestfrommy_projectimportcreate_app@pytest.fixture()defapp():app=create_app()app.config.update({"TESTING":True,})# other setup can go hereyieldapp# clean up / reset resources here@pytest.fixture()defclient(app):returnapp.test_client()@pytest.fixture()defrunner(app):returnapp.test_cli_runner()

Sending Requests with the Test Client

The test client makes requests to the application without running a liveserver. Flask’s client extendsWerkzeug’s client, see those docs for additionalinformation.

Theclient has methods that match the common HTTP request methods,such asclient.get() andclient.post(). They take many argumentsfor building the request; you can find the full documentation inEnvironBuilder. Typically you’ll usepath,query_string,headers, anddata orjson.

To make a request, call the method the request should use with the pathto the route to test. ATestResponse is returnedto examine the response data. It has all the usual properties of aresponse object. You’ll usually look atresponse.data, which is thebytes returned by the view. If you want to use text, Werkzeug 2.1providesresponse.text, or useresponse.get_data(as_text=True).

deftest_request_example(client):response=client.get("/posts")assertb"<h2>Hello, World!</h2>"inresponse.data

Pass a dictquery_string={"key":"value",...} to set arguments inthe query string (after the? in the URL). Pass a dictheaders={} to set request headers.

To send a request body in a POST or PUT request, pass a value todata. If raw bytes are passed, that exact body is used. Usually,you’ll pass a dict to set form data.

Form Data

To send form data, pass a dict todata. TheContent-Type headerwill be set tomultipart/form-data orapplication/x-www-form-urlencoded automatically.

If a value is a file object opened for reading bytes ("rb" mode), itwill be treated as an uploaded file. To change the detected filename andcontent type, pass a(file,filename,content_type) tuple. Fileobjects will be closed after making the request, so they do not need touse the usualwithopen()asf: pattern.

It can be useful to store files in atests/resources folder, thenusepathlib.Path to get files relative to the current test file.

frompathlibimportPath# get the resources folder in the tests folderresources=Path(__file__).parent/"resources"deftest_edit_user(client):response=client.post("/user/2/edit",data={"name":"Flask","theme":"dark","picture":(resources/"picture.png").open("rb"),})assertresponse.status_code==200

JSON Data

To send JSON data, pass an object tojson. TheContent-Typeheader will be set toapplication/json automatically.

Similarly, if the response contains JSON data, theresponse.jsonattribute will contain the deserialized object.

deftest_json_data(client):response=client.post("/graphql",json={"query":"""            query User($id: String!) {                user(id: $id) {                    name                    theme                    picture_url                }            }        """,variables={"id":2},})assertresponse.json["data"]["user"]["name"]=="Flask"

Following Redirects

By default, the client does not make additional requests if the responseis a redirect. By passingfollow_redirects=True to a request method,the client will continue to make requests until a non-redirect responseis returned.

TestResponse.history isa tuple of the responses that led up to the final response. Eachresponse has arequest attributewhich records the request that produced that response.

deftest_logout_redirect(client):response=client.get("/logout",follow_redirects=True)# Check that there was one redirect response.assertlen(response.history)==1# Check that the second request was to the index page.assertresponse.request.path=="/index"

Accessing and Modifying the Session

To access Flask’s context variables, mainlysession, use the client in awith statement.The app and request context will remain activeafter making a request,until thewith block ends.

fromflaskimportsessiondeftest_access_session(client):withclient:client.post("/auth/login",data={"username":"flask"})# session is still accessibleassertsession["user_id"]==1# session is no longer accessible

If you want to access or set a value in the sessionbefore making arequest, use the client’ssession_transaction() method in awith statement. It returns a session object, and will save thesession once the block ends.

fromflaskimportsessiondeftest_modify_session(client):withclient.session_transaction()assession:# set a user id without going through the login routesession["user_id"]=1# session is saved nowresponse=client.get("/users/me")assertresponse.json["username"]=="flask"

Running Commands with the CLI Runner

Flask providestest_cli_runner() to create aFlaskCliRunner, which runs CLI commands inisolation and captures the output in aResultobject. Flask’s runner extendsClick’s runner,see those docs for additional information.

Use the runner’sinvoke() method tocall commands in the same way they would be called with theflaskcommand from the command line.

importclick@app.cli.command("hello")@click.option("--name",default="World")defhello_command(name):click.echo(f"Hello,{name}!")deftest_hello_command(runner):result=runner.invoke(args="hello")assert"World"inresult.outputresult=runner.invoke(args=["hello","--name","Flask"])assert"Flask"inresult.output

Tests that depend on an Active Context

You may have functions that are called from views or commands, thatexpect an activeapplication context orrequest context because they accessrequest,session, orcurrent_app. Rather than testing them by making arequest or invoking the command, you can create and activate a contextdirectly.

Usewithapp.app_context() to push an application context. Forexample, database extensions usually require an active app context tomake queries.

deftest_db_post_model(app):withapp.app_context():post=db.session.query(Post).get(1)

Usewithapp.test_request_context() to push a request context. Ittakes the same arguments as the test client’s request methods.

deftest_validate_user_edit(app):withapp.test_request_context("/user/2/edit",method="POST",data={"name":""}):# call a function that accesses `request`messages=validate_edit_user()assertmessages["name"][0]=="Name cannot be empty."

Creating a test request context doesn’t run any of the Flask dispatchingcode, sobefore_request functions are not called. If you need tocall these, usually it’s better to make a full request instead. However,it’s possible to call them manually.

deftest_auth_token(app):withapp.test_request_context("/user/2/edit",headers={"X-Auth-Token":"1"}):app.preprocess_request()assertg.user.name=="Flask"