Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
Start a server in a subprocess during a pytest session#
I wanted to start an actual server process, run it for the duration of my pytest session and shut it down at the end.
Here’s the recipe I came up with. This fixture lives in conftest.py:
1import pytest2import sqlite_utils3import subprocess4
5@pytest.fixture(scope="session")6def ds_server(tmp_path_factory):7 db_directory = tmp_path_factory.mktemp("dbs")8 db_path = db_directory / "test.db"9 db = sqlite_utils.Database(db_path)10 insert_test_data(db)11 ds_proc = subprocess.Popen(12 [13 "datasette",14 str(db_path),15 "-p",16 "8041"17 ],18 stdout=subprocess.PIPE,19 stderr=subprocess.STDOUT,20 )21 # Give the server time to start22 time.sleep(2)23 # Check it started successfully24 assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8")25 yield ds_proc26 # Shut it down at the end of the pytest session27 ds_proc.terminate()A test looks like this:
1import httpx2
3def test_server_starts(ds_server):4 response = httpx.get("http://127.0.0.1:8041/")5 assert response.status_code == 200Alternative recipe for serving static files#
While adding tests to Datasette Lite I found myself needing to run a localhost server that served static files directly.
I completely forgot about this TIL, and instead took inspiration from pytest-simplehttpserver - coming up with this pattern:
1from subprocess import Popen, PIPE2import pathlib3import pytest4import time5from http.client import HTTPConnection6
7root = pathlib.Path(__file__).parent.parent.absolute()8
9
10@pytest.fixture(scope="module")11def static_server():12 process = Popen(13 ["python", "-m", "http.server", "8123", "--directory", root], stdout=PIPE14 )15 retries = 516 while retries > 0:17 conn = HTTPConnection("localhost:8123")18 try:19 conn.request("HEAD", "/")20 response = conn.getresponse()21 if response is not None:22 yield process23 break24 except ConnectionRefusedError:25 time.sleep(1)26 retries -= 127
28 if not retries:29 raise RuntimeError("Failed to start http server")30 else:31 process.terminate()32 process.wait()Again, including static_server as a fixture is enough to ensure requests to http://localhost:8123/ will be served by that temporary server.
I like how this version polls for a successful HEAD request (a trick inspired by pytest-simplehttpserver) rather than just sleeping.