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
Metadata
Release files for jleechanorg-testing-utils 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jleechanorg_testing_utils-0.1.2.tar.gz | 36.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jleechanorg_testing_utils-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 80.1 kB
Release files / jleechanorg_testing_utils-0.1.2.tar.gz
| Download URL | jleechanorg_testing_utils-0.1.2.tar.gz |
|---|---|
| Size | 36.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f28344a0699efe1be779a9dc0cccf9adcf800443653293102773ff45a27eb626
|
|
BLAKE2b-256 checksum How to use checksums |
0451065b67e27ae5028e838ccfde9062c3b1ab661712797064e915980d6b27e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / jleechanorg_testing_utils-0.1.2-py3-none-any.whl
| Download URL | jleechanorg_testing_utils-0.1.2-py3-none-any.whl |
|---|---|
| Size | 43.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1f62bfc307db4c845c9ec0b7526a672cda7766a68f97bd8933b6f3103136d49e
|
|
BLAKE2b-256 checksum How to use checksums |
c99989b0c7bd9b6a453c87c41659faa5f81acb4a0f9855e77fd1e1c3156d6f77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|