jleechanorg-testing-utils
Generic server/MCP/browser testing utilities for Python projects.
Features
- HTTP client with request/response capture, credential redaction, and retry logic
- MCP client for Model Context Protocol (JSON-RPC 2.0) servers
- Evidence bundles – structured
/tmp/<project>/<branch>/iteration_NNN/directories with git provenance - Local server management – subprocess server lifecycle with port reservation
- Generic test base – abstract
BaseTestRunnerfor server integration tests - Browser test base – Playwright-based
BrowserTestBasefor UI tests - HTTP test base –
HttpTestBaseusingrequests/httpiefor web server tests - Server-side logging –
LLMCallLoggerandHttpRequestLoggerthat servers can import to capture raw request/response payloads
Installation
pip install jleechanorg-testing-utils
# For browser testing:
pip install "jleechanorg-testing-utils[browser]"
Usage
MCP Server Testing
from testing_utils import MCPClient, BaseTestRunner, TestResult
class SmokeTest(BaseTestRunner):
TEST_NAME = "mcp_smoke"
PROJECT_NAME = "myproject"
DEFAULT_BASE_URL = "http://localhost:8081"
def run_scenarios(self) -> list[TestResult]:
tools = self.client.tools_list()
return [TestResult("tools_list", passed=bool(tools), detail=f"{len(tools)} tools")]
if __name__ == "__main__":
SmokeTest().run()
HTTP Web Server Testing
from testing_utils.http_test import HttpTestBase
class ApiTest(HttpTestBase):
TEST_NAME = "api_smoke"
BASE_URL = "http://localhost:9000"
PROJECT_NAME = "myproject"
def run_scenarios(self):
r = self.get("/health")
r.assert_ok()
r.assert_json_key("status", "healthy")
# httpie-style subprocess
rc, out, err = self.httpie("GET", "/api/users")
return [{"name": "health_ok", "passed": r.ok}]
if __name__ == "__main__":
test = ApiTest()
results = test.run()
Browser Testing (requires [browser] extra)
from testing_utils.browser import BrowserTestBase
class HomepageTest(BrowserTestBase):
BASE_URL = "http://localhost:3000"
TEST_NAME = "homepage_smoke"
PROJECT_NAME = "myproject"
HEADLESS = True
def run_scenarios(self):
self.page.goto(self.BASE_URL)
title = self.page.title()
self.take_screenshot("homepage")
return [{"name": "page_loads", "passed": bool(title), "detail": title}]
if __name__ == "__main__":
HomepageTest().run()
Server-Side LLM Logging
Import in your server to capture raw LLM request/response payloads for evidence bundles:
from testing_utils.logging_capture import LLMCallLogger
# Use default env var names:
_llm_logger = LLMCallLogger(
capture_path_env="RAW_LLM_CAPTURE_PATH",
enabled_env="CAPTURE_RAW_LLM",
max_chars_env="CAPTURE_RAW_LLM_MAX_CHARS",
)
def call_llm(provider, model, messages, system_instruction=None):
_llm_logger.log_request(
provider=provider,
model=model,
request_payload={"messages": messages},
system_instruction=system_instruction,
)
response_text = _actual_llm_call(provider, model, messages)
_llm_logger.log_response(
provider=provider,
model=model,
response_text=response_text,
)
return response_text
Set RAW_LLM_CAPTURE_PATH=/tmp/myproject/llm_captures.jsonl before running tests to enable capture.
Evidence Bundles
from testing_utils.evidence import (
get_evidence_dir,
create_evidence_bundle,
write_with_checksum,
capture_provenance,
)
# Get branch-scoped evidence directory
evidence_dir = get_evidence_dir("my_test", project_name="myproject")
# → /tmp/myproject/<branch>/my_test/
# Create a structured bundle after running tests
create_evidence_bundle(
evidence_dir,
results={"passed": 5, "failed": 0},
test_name="my_test",
project_name="myproject",
http_captures=client.get_captures_as_dict(),
)
# → /tmp/myproject/<branch>/my_test/iteration_001/
# ├── README.md
# ├── metadata.json
# ├── results.json
# └── request_responses.jsonl
Environment Variables
| Variable | Module | Default | Description |
|---|---|---|---|
RAW_LLM_CAPTURE_PATH |
logging_capture |
`` (disabled) | Path to JSONL file for LLM captures |
RAW_HTTP_CAPTURE_PATH |
logging_capture |
`` (disabled) | Path to JSONL file for HTTP captures |
CAPTURE_RAW_LLM |
logging_capture |
true |
Enable/disable LLM capture |
CAPTURE_RAW_LLM_MAX_CHARS |
logging_capture |
20000 |
Max chars per LLM payload field |
TEST_SCREENSHOT_DIR |
browser |
branch-scoped /tmp |
Screenshot output directory |
TEST_VIDEO_DIR |
browser |
branch-scoped /tmp |
Video output directory |
TEST_RECORD_VIDEO |
browser |
false |
Enable Playwright video recording |
TEST_BASE_URL |
browser, http_test |
http://localhost:8081 |
Server URL override |
Design Principles
- No hardcoded project names – everything is parameterized
- No hardcoded URLs or credentials – all configurable via constructor or env vars
- Thread-safe – logging capture uses file locks
- Credential redaction – API keys, JWTs, and auth tokens are automatically redacted in captures
- Evidence-first – every test run creates a structured evidence bundle with git provenance
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jleechanorg_testing_utils-0.1.2.tar.gz.
File metadata
- Download URL: jleechanorg_testing_utils-0.1.2.tar.gz
- Upload date:
- Size: 36.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f28344a0699efe1be779a9dc0cccf9adcf800443653293102773ff45a27eb626
|
|
| MD5 |
41b2aaefc6499527a5d8eb7cdd7cd4fb
|
|
| BLAKE2b-256 |
0451065b67e27ae5028e838ccfde9062c3b1ab661712797064e915980d6b27e5
|
File details
Details for the file jleechanorg_testing_utils-0.1.2-py3-none-any.whl.
File metadata
- Download URL: jleechanorg_testing_utils-0.1.2-py3-none-any.whl
- Upload date:
- Size: 43.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f62bfc307db4c845c9ec0b7526a672cda7766a68f97bd8933b6f3103136d49e
|
|
| MD5 |
dba18b20d487d1adceb21b98268e280a
|
|
| BLAKE2b-256 |
c99989b0c7bd9b6a453c87c41659faa5f81acb4a0f9855e77fd1e1c3156d6f77
|