From 46e3811f8d890d4f8b4194412dafa478f2d75f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Apr 2019 20:15:05 +0400 Subject: [PATCH] :sparkles: Add testing docs and tests (#151) * :pencil2: Fix typo in security intro * :sparkles: Add testing docs and tests * :bug: Debug Travis coverage * :bug: Debug Travis coverage, report XML * :green_heart: Make Travis/Flit use same code install * :rewind: Revert Travis/Codecov debugging changes --- .travis.yml | 2 +- docs/src/app_testing/__init__.py | 0 docs/src/app_testing/main.py | 8 +++ docs/src/app_testing/test_main.py | 11 +++ docs/src/app_testing/tutorial001.py | 18 +++++ docs/src/app_testing/tutorial002.py | 31 +++++++++ docs/src/app_testing/tutorial003.py | 24 +++++++ docs/src/events/tutorial001.py | 2 +- docs/tutorial/security/intro.md | 2 +- docs/tutorial/testing.md | 69 +++++++++++++++++++ mkdocs.yml | 1 + .../test_events/test_tutorial001.py | 4 +- tests/test_tutorial/test_testing/__init__.py | 0 tests/test_tutorial/test_testing/test_main.py | 30 ++++++++ .../test_testing/test_tutorial001.py | 30 ++++++++ .../test_testing/test_tutorial002.py | 9 +++ .../test_testing/test_tutorial003.py | 5 ++ 17 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 docs/src/app_testing/__init__.py create mode 100644 docs/src/app_testing/main.py create mode 100644 docs/src/app_testing/test_main.py create mode 100644 docs/src/app_testing/tutorial001.py create mode 100644 docs/src/app_testing/tutorial002.py create mode 100644 docs/src/app_testing/tutorial003.py create mode 100644 docs/tutorial/testing.md create mode 100644 tests/test_tutorial/test_testing/__init__.py create mode 100644 tests/test_tutorial/test_testing/test_main.py create mode 100644 tests/test_tutorial/test_testing/test_tutorial001.py create mode 100644 tests/test_tutorial/test_testing/test_tutorial002.py create mode 100644 tests/test_tutorial/test_testing/test_tutorial003.py diff --git a/.travis.yml b/.travis.yml index e97301248..c37021e93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: install: - pip install flit - - flit install + - flit install --symlink script: - bash scripts/test.sh diff --git a/docs/src/app_testing/__init__.py b/docs/src/app_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/src/app_testing/main.py b/docs/src/app_testing/main.py new file mode 100644 index 000000000..4679aec9c --- /dev/null +++ b/docs/src/app_testing/main.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_main(): + return {"msg": "Hello World"} diff --git a/docs/src/app_testing/test_main.py b/docs/src/app_testing/test_main.py new file mode 100644 index 000000000..b02d7f010 --- /dev/null +++ b/docs/src/app_testing/test_main.py @@ -0,0 +1,11 @@ +from starlette.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_main(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"msg": "Hello World"} diff --git a/docs/src/app_testing/tutorial001.py b/docs/src/app_testing/tutorial001.py new file mode 100644 index 000000000..9b00acb47 --- /dev/null +++ b/docs/src/app_testing/tutorial001.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from starlette.testclient import TestClient + +app = FastAPI() + + +@app.get("/") +async def read_main(): + return {"msg": "Hello World"} + + +client = TestClient(app) + + +def test_read_main(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"msg": "Hello World"} diff --git a/docs/src/app_testing/tutorial002.py b/docs/src/app_testing/tutorial002.py new file mode 100644 index 000000000..0c3b9b9f0 --- /dev/null +++ b/docs/src/app_testing/tutorial002.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from starlette.testclient import TestClient +from starlette.websockets import WebSocket + +app = FastAPI() + + +@app.get("/") +async def read_main(): + return {"msg": "Hello World"} + + +@app.websocket_route("/ws") +async def websocket(websocket: WebSocket): + await websocket.accept() + await websocket.send_json({"msg": "Hello WebSocket"}) + await websocket.close() + + +def test_read_main(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"msg": "Hello World"} + + +def test_websocket(): + client = TestClient(app) + with client.websocket_connect("/ws") as websocket: + data = websocket.receive_json() + assert data == {"msg": "Hello WebSocket"} diff --git a/docs/src/app_testing/tutorial003.py b/docs/src/app_testing/tutorial003.py new file mode 100644 index 000000000..8843de174 --- /dev/null +++ b/docs/src/app_testing/tutorial003.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from starlette.testclient import TestClient + +app = FastAPI() + +items = {} + + +@app.on_event("startup") +async def startup_event(): + items["foo"] = {"name": "Fighters"} + items["bar"] = {"name": "Tenders"} + + +@app.get("/items/{item_id}") +async def read_items(item_id: str): + return items[item_id] + + +def test_read_items(): + with TestClient(app) as client: + response = client.get("/items/foo") + assert response.status_code == 200 + assert response.json() == {"name": "Fighters"} diff --git a/docs/src/events/tutorial001.py b/docs/src/events/tutorial001.py index 6a0bd5a20..128004c9f 100644 --- a/docs/src/events/tutorial001.py +++ b/docs/src/events/tutorial001.py @@ -12,5 +12,5 @@ async def startup_event(): @app.get("/items/{item_id}") -async def read_item(item_id: str): +async def read_items(item_id: str): return items[item_id] diff --git a/docs/tutorial/security/intro.md b/docs/tutorial/security/intro.md index 0ec81dbd7..74330a0a2 100644 --- a/docs/tutorial/security/intro.md +++ b/docs/tutorial/security/intro.md @@ -20,7 +20,7 @@ It is quite an extensive specification and covers several complex use cases. It includes ways to authenticate using a "third party". -That's what all the system with "login with Facebook, Google, Twitter, GitHub" use underneath. +That's what all the systems with "login with Facebook, Google, Twitter, GitHub" use underneath. ### OAuth 1 diff --git a/docs/tutorial/testing.md b/docs/tutorial/testing.md new file mode 100644 index 000000000..dcb50ab4c --- /dev/null +++ b/docs/tutorial/testing.md @@ -0,0 +1,69 @@ +Thanks to Starlette's TestClient, testing **FastAPI** applications is easy and enjoyable. + +It is based on Requests, so it's very familiar and intuitive. + +With it, you can use pytest directly with **FastAPI**. + +## Using `TestClient` + +Import `TestClient` from `starlette.testclient`. + +Create a `TestClient` passing to it your **FastAPI**. + +Create functions with a name that starts with `test_` (this is standard `pytest` conventions). + +Use the `TestClient` object the same way as you do with `requests`. + +Write simple `assert` statements with the standard Python expressions that you need to check (again, standard `pytest`). + +```Python hl_lines="2 12 15 16 17 18" +{!./src/app_testing/tutorial001.py!} +``` + +!!! tip + Notice that the testing functions are normal `def`, not `async def`. + + And the calls to the client are also normal calls, not using `await`. + + This allows you to use `pytest` directly without complications. + + +## Separating tests + +In a real application, you probably would have your tests in a different file. + +And your **FastAPI** application might also be composed of several files/modules, etc. + +### **FastAPI** app file + +Let's say you have a file `main.py` with your **FastAPI** app: + +```Python +{!./src/app_testing/main.py!} +``` + +### Testing file + +Then you could have a file `test_main.py` with your tests, and import your `app` from the `main` module (`main.py`): + +```Python +{!./src/app_testing/test_main.py!} +``` + +## Testing WebSockets + +You can use the same `TestClient` to test WebSockets. + +For this, you use the `TestClient` in a `with` statement, connecting to the WebSocket: + +```Python hl_lines="27 28 29 30 31" +{!./src/app_testing/tutorial002.py!} +``` + +## Testing Events, `startup` and `shutdown` + +When you need your event handlers (`startup` and `shutdown`) to run in your tests, you can use the `TestClient` with a `with` statement: + +```Python hl_lines="9 10 11 12 20 21 22 23 24" +{!./src/app_testing/tutorial003.py!} +``` diff --git a/mkdocs.yml b/mkdocs.yml index d7cf5f242..ef463f38b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,7 @@ nav: - GraphQL: 'tutorial/graphql.md' - WebSockets: 'tutorial/websockets.md' - 'Events: startup - shutdown': 'tutorial/events.md' + - Testing: 'tutorial/testing.md' - Debugging: 'tutorial/debugging.md' - Extending OpenAPI: 'tutorial/extending-openapi.md' - Concurrency and async / await: 'async.md' diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index 2b05f1375..2fedbc3c3 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -24,8 +24,8 @@ openapi_schema = { }, }, }, - "summary": "Read Item Get", - "operationId": "read_item_items__item_id__get", + "summary": "Read Items Get", + "operationId": "read_items_items__item_id__get", "parameters": [ { "required": True, diff --git a/tests/test_tutorial/test_testing/__init__.py b/tests/test_tutorial/test_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_testing/test_main.py b/tests/test_tutorial/test_testing/test_main.py new file mode 100644 index 000000000..cdd4bbc73 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main.py @@ -0,0 +1,30 @@ +from app_testing.test_main import client, test_read_main + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "Fast API", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main Get", + "operationId": "read_main__get", + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + test_read_main() diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py new file mode 100644 index 000000000..a0b8df401 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -0,0 +1,30 @@ +from app_testing.tutorial001 import client, test_read_main + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "Fast API", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Main Get", + "operationId": "read_main__get", + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + test_read_main() diff --git a/tests/test_tutorial/test_testing/test_tutorial002.py b/tests/test_tutorial/test_testing/test_tutorial002.py new file mode 100644 index 000000000..c1e1e4ebf --- /dev/null +++ b/tests/test_tutorial/test_testing/test_tutorial002.py @@ -0,0 +1,9 @@ +from app_testing.tutorial002 import test_read_main, test_websocket + + +def test_main(): + test_read_main() + + +def test_ws(): + test_websocket() diff --git a/tests/test_tutorial/test_testing/test_tutorial003.py b/tests/test_tutorial/test_testing/test_tutorial003.py new file mode 100644 index 000000000..074c42a4c --- /dev/null +++ b/tests/test_tutorial/test_testing/test_tutorial003.py @@ -0,0 +1,5 @@ +from app_testing.tutorial003 import test_read_items + + +def test_main(): + test_read_items()