I have an application written with Uvicorn + FastAPI. I am testing the response time using PyTest.
Referring to How to start a Uvicorn + FastAPI in background when testing with PyTest, I wrote the test. However, I found the application process alive after completing the test when workers >= 2.
I want to terminate the application process cleanly at the end of the test.
Do you have any idea?
The details are as follows.
Environment
- Windows 10
- Bash 4.4.23 (https://cmder.net/)
- python 3.7.5
Libraries
- fastapi == 0.68.0
- uvicorn == 0.14.0
- requests == 2.26.0
- pytest == 6.2.4
Sample Codes
- Application: main.py
JavaScriptx81
from fastapi import FastAPI
2
3app = FastAPI()
4
5@app.get("/")
6def hello_world():
7return "hello world"
8
- Test: test_main.py
JavaScript164641
from multiprocessing import Process
2import pytest
3import requests
4import time
5import uvicorn
6
7HOST = "127.0.0.1"
8PORT = 8765
9WORKERS = 1
10
11
12def run_server(host: str, port: int, workers: int, wait: int = 15) -> Process:
13proc = Process(
14target=uvicorn.run,
15args=("main:app",),
16kwargs={
17"host": host,
18"port": port,
19"workers": workers,
20},
21)
22proc.start()
23time.sleep(wait)
24assert proc.is_alive()
25return proc
26
27
28def shutdown_server(proc: Process):
29proc.terminate()
30for _ in range(5):
31if proc.is_alive():
32time.sleep(5)
33else:
34return
35else:
36raise Exception("Process still alive")
37
38
39def check_response(host: str, port: int):
40assert requests.get(f"http://{host}:{port}").text == '"hello world"'
41
42
43def check_response_time(host: str, port: int, tol: float = 1e-2):
44s = time.time()
45requests.get(f"http://{host}:{port}")
46e = time.time()
47assert e-s < tol
48
49
50@pytest.fixture(scope="session")
51def server():
52proc = run_server(HOST, PORT, WORKERS)
53try:
54yield
55finally:
56shutdown_server(proc)
57
58
59def test_main(server):
60check_response(HOST, PORT)
61check_response_time(HOST, PORT)
62check_response(HOST, PORT)
63check_response_time(HOST, PORT)
64
Execution Result
JavaScript
1
28
28
1
$ curl http://localhost:8765
2
curl: (7) Failed to connect to localhost port 8765: Connection refused
3
$ pytest test_main.py
4
=============== test session starts =============== platform win32 -- Python 3.7.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
5
rootdir: .
6
collected 1 item
7
8
test_main.py . [100%]
9
10
=============== 1 passed in 20.23s ===============
11
$ curl http://localhost:8765
12
curl: (7) Failed to connect to localhost port 8765: Connection refused
13
$ sed -i -e "s/WORKERS = 1/WORKERS = 3/g" test_main.py
14
$ curl http://localhost:8765
15
curl: (7) Failed to connect to localhost port 8765: Connection refused
16
$ pytest test_main.py
17
=============== test session starts =============== platform win32 -- Python 3.7.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
18
rootdir: .
19
collected 1 item
20
21
test_main.py . [100%]
22
23
=============== 1 passed in 20.21s ===============
24
$ curl http://localhost:8765
25
"hello world"
26
27
$ # Why is localhost:8765 still alive?
28
Advertisement
Answer
I have found a solution myself.
Thanks > https://stackoverflow.com/a/27034438/16567832
Solution
After install psutil by pip install psutil
, update test_main.py
JavaScript
1
73
73
1
from multiprocessing import Process
2
import psutil
3
import pytest
4
import requests
5
import time
6
import uvicorn
7
8
HOST = "127.0.0.1"
9
PORT = 8765
10
WORKERS = 3
11
12
13
def run_server(host: str, port: int, workers: int, wait: int = 15) -> Process:
14
proc = Process(
15
target=uvicorn.run,
16
args=("main:app",),
17
kwargs={
18
"host": host,
19
"port": port,
20
"workers": workers,
21
},
22
)
23
proc.start()
24
time.sleep(wait)
25
assert proc.is_alive()
26
return proc
27
28
29
def shutdown_server(proc: Process):
30
31
##### SOLUTION #####
32
pid = proc.pid
33
parent = psutil.Process(pid)
34
for child in parent.children(recursive=True):
35
child.kill()
36
##### SOLUTION END ####
37
38
proc.terminate()
39
for _ in range(5):
40
if proc.is_alive():
41
time.sleep(5)
42
else:
43
return
44
else:
45
raise Exception("Process still alive")
46
47
48
def check_response(host: str, port: int):
49
assert requests.get(f"http://{host}:{port}").text == '"hello world"'
50
51
52
def check_response_time(host: str, port: int, tol: float = 1e-2):
53
s = time.time()
54
requests.get(f"http://{host}:{port}")
55
e = time.time()
56
assert e-s < tol
57
58
59
@pytest.fixture(scope="session")
60
def server():
61
proc = run_server(HOST, PORT, WORKERS)
62
try:
63
yield
64
finally:
65
shutdown_server(proc)
66
67
68
def test_main(server):
69
check_response(HOST, PORT)
70
check_response_time(HOST, PORT)
71
check_response(HOST, PORT)
72
check_response_time(HOST, PORT)
73
Execution Result
JavaScript
1
13
13
1
$ curl http://localhost:8765
2
curl: (7) Failed to connect to localhost port 8765: Connection refused
3
$ pytest test_main.py
4
================== test session starts ================== platform win32 -- Python 3.7.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
5
rootdir: .
6
collected 1 item
7
8
test_main.py . [100%]
9
10
================== 1 passed in 20.24s ==================
11
$ curl http://localhost:8765
12
curl: (7) Failed to connect to localhost port 8765: Connection refused
13