Skip to content

Commit 7c27b32

Browse files
Adding integration tests (#351)
* Add integration tests setup with initial configuration files - Created .gitignore to exclude unnecessary files and directories from version control. - Added .python-version to specify the Python version for the integration tests. - Introduced pyproject.toml for project metadata and dependency management, including required packages for development. - Configured pytest with pytest.ini to define test discovery patterns. - Implemented a basic test case in test_basic.py to validate the testing framework setup. - Generated uv.lock to lock dependencies for consistent environments. * Update pyproject.toml to define build system requirements - Added build-system section to specify setuptools and wheel as requirements for building the project, ensuring compatibility and proper packaging. * Add build-system section to pyproject.toml - Introduced a build-system section specifying setuptools and wheel as requirements for building the project, ensuring proper packaging and compatibility. * Update pyproject.toml and uv.lock to enhance dependency management - Added new dependencies `exospherehost` and `state-manager` to the `pyproject.toml` for improved integration testing. - Configured dependency groups and specified paths for local development. - Updated `uv.lock` to include new package versions and their metadata, ensuring consistent environments and access to the latest features. * Add integration testing setup with UvicornTestServer - Introduced UvicornTestServer class for running a uvicorn server in a background thread, allowing for real HTTP endpoint testing without blocking. - Added pytest fixture `running_server` for session-scoped server management, ensuring isolation between tests. - Created README.md with detailed instructions on integration testing approaches and usage examples. - Removed outdated test_basic.py file and added test_health.py to validate the health endpoint using the new server setup. * Add integration tests workflow for Exosphere - Created a new GitHub Actions workflow for running integration tests, triggered on pushes to the main branch. - Configured MongoDB service for test environment setup, including health checks and initialization parameters. - Set up Python environment and installed development dependencies using Uvicorn. - Implemented steps to run integration tests with pytest, ensuring proper environment variable configuration for database access and API keys. - Removed release trigger from the publish-state-manager workflow to streamline the process. * Update integration-tests/pyproject.toml Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 7c0befb commit 7c27b32

12 files changed

Lines changed: 1480 additions & 2 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Integration Tests for Exosphere
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'integration-tests/**'
8+
- 'state-manager/**'
9+
- 'python-sdk/**'
10+
- '.github/workflows/integration-tests.yml'
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
services:
16+
mongodb:
17+
image: mongo:7
18+
ports:
19+
- 27017:27017
20+
options: >-
21+
--health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
22+
--health-interval 10s
23+
--health-timeout 5s
24+
--health-retries 5
25+
env:
26+
MONGO_INITDB_ROOT_USERNAME: admin
27+
MONGO_INITDB_ROOT_PASSWORD: password
28+
MONGO_INITDB_DATABASE: integration_tests
29+
30+
steps:
31+
- name: Checkout code
32+
uses: actions/checkout@v4
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v5
36+
with:
37+
python-version: '3.12'
38+
39+
- name: Install uv
40+
uses: astral-sh/setup-uv@v2
41+
with:
42+
cache: true
43+
44+
- name: Install dev dependencies with uv
45+
working-directory: integration-tests
46+
run: |
47+
uv sync --group dev
48+
49+
- name: Run integration tests
50+
working-directory: integration-tests
51+
env:
52+
MONGO_URI: mongodb://admin:password@localhost:27017
53+
MONGO_DATABASE_NAME: integration_tests
54+
STATE_MANAGER_SECRET: test-secret-key
55+
EXOSPHERE_API_KEY: test-secret-key
56+
SECRETS_ENCRYPTION_KEY: YTzpUlBGLSwm-3yKJRJTZnb0_aQuQQHyz64s8qAERVU=
57+
run: |
58+
uv run pytest

.github/workflows/publish-state-mangaer.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ on:
55
branches: [main]
66
paths:
77
- 'state-manager/**'
8-
release:
9-
types: [published]
108
workflow_dispatch:
119

1210
env:

integration-tests/.gitignore

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Virtual Environment
24+
venv/
25+
env/
26+
ENV/
27+
.env
28+
.venv/
29+
30+
# IDE
31+
.vscode/
32+
*.swp
33+
*.swo
34+
.idea/
35+
*.iws
36+
*.iml
37+
*.ipr
38+
39+
40+
# Local development
41+
.env.local
42+
.env.development.local
43+
.env.test.local
44+
.env.production.local
45+
46+
# Database
47+
*.db
48+
*.sqlite3
49+
50+
# OS generated files
51+
.DS_Store
52+
.DS_Store?
53+
._*
54+
.Spotlight-V100
55+
.Trashes
56+
ehthumbs.db
57+
Thumbs.db
58+
59+
#logs
60+
*.log
61+
logs/*.*
62+
!logs/.gitkeep
63+
64+
# local files
65+
files/
66+
!files/.gitkeep

integration-tests/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

integration-tests/README.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Integration Testing with Uvicorn Server
2+
3+
This directory contains examples of how to properly run and stop uvicorn servers for integration testing. Unlike the problematic approach of using `uvicorn.run()` (which blocks forever), these examples show you how to create real HTTP endpoints that you can send requests to.
4+
5+
## 🚨 The Problem with `uvicorn.run()`
6+
7+
The original code had this issue:
8+
9+
```python
10+
@pytest.mark.asyncio
11+
async def test_basic():
12+
uvicorn.run(app, host="127.0.0.1", port=8000) # ❌ This blocks forever!
13+
async with ClientSession() as session: # ❌ This never executes
14+
# ... test code that never runs
15+
```
16+
17+
**Problem**: `uvicorn.run()` is a blocking call that never returns, so your test code after it never executes.
18+
19+
## ✅ Proper Solutions
20+
21+
### 1. UvicornTestServer Class (Recommended)
22+
23+
The `UvicornTestServer` class in `test_basic.py` provides a clean way to start and stop uvicorn servers:
24+
25+
```python
26+
from test_basic import UvicornTestServer
27+
28+
# Create and start server
29+
server = UvicornTestServer(app, host="127.0.0.1", port=8000)
30+
server.start()
31+
32+
# Make HTTP requests
33+
async with ClientSession() as session:
34+
async with session.get(f"{server.base_url}/health") as response:
35+
assert response.status == 200
36+
37+
# Stop server
38+
server.stop()
39+
```
40+
41+
**Features:**
42+
- ✅ Automatic port detection (avoids conflicts)
43+
- ✅ Proper startup/shutdown lifecycle
44+
- ✅ Thread-based server execution
45+
- ✅ Waits for server to be ready
46+
- ✅ Graceful cleanup
47+
48+
### 2. Pytest Fixtures
49+
50+
Use pytest fixtures for automatic server management:
51+
52+
```python
53+
@pytest.fixture(scope="session")
54+
def running_server():
55+
"""Server shared across all tests in the session."""
56+
server = UvicornTestServer(app)
57+
server.start()
58+
yield server
59+
server.stop()
60+
61+
@pytest.fixture(scope="function")
62+
def fresh_server():
63+
"""Fresh server for each test."""
64+
server = UvicornTestServer(app)
65+
server.start()
66+
yield server
67+
server.stop()
68+
```
69+
70+
### 3. Manual Server Management
71+
72+
For full control over server lifecycle:
73+
74+
```python
75+
@pytest.mark.asyncio
76+
async def test_manual_server():
77+
server = UvicornTestServer(app)
78+
79+
try:
80+
server.start()
81+
# Your test code here
82+
async with ClientSession() as session:
83+
async with session.get(f"{server.base_url}/health") as response:
84+
assert response.status == 200
85+
finally:
86+
server.stop() # Always cleanup
87+
```
88+
89+
## 🏃‍♂️ Running the Examples
90+
91+
### Install Dependencies
92+
93+
```bash
94+
cd integration-tests
95+
uv sync
96+
```
97+
98+
### Run Tests
99+
100+
```bash
101+
# Run all tests
102+
uv run python -m pytest test_basic.py -v
103+
104+
# Run specific test
105+
uv run python -m pytest test_basic.py::test_basic_with_session_server -v -s
106+
107+
# Run with output
108+
uv run python -m pytest test_basic.py -v -s
109+
```
110+
111+
### Run Demo Server
112+
113+
```bash
114+
# Start a server you can send requests to
115+
uv run python demo_server.py
116+
```
117+
118+
Then in another terminal:
119+
```bash
120+
curl http://127.0.0.1:8000/health
121+
curl http://127.0.0.1:8000/test
122+
```
123+
124+
## 📁 File Overview
125+
126+
- **`test_basic.py`** - Main test file with UvicornTestServer class and examples
127+
- **`test_server_examples.py`** - Comprehensive examples of different testing approaches
128+
- **`demo_server.py`** - Simple script to run a server manually
129+
- **`pyproject.toml`** - Project dependencies
130+
131+
## 🎯 When to Use Each Approach
132+
133+
### Session-Scoped Server (`running_server` fixture)
134+
- ✅ Fast test execution (server starts once)
135+
- ✅ Good for multiple tests that don't interfere
136+
- ❌ Tests share state
137+
- **Use for**: Most integration tests
138+
139+
### Function-Scoped Server (`fresh_server` fixture)
140+
- ✅ Complete isolation between tests
141+
- ✅ Clean state for each test
142+
- ❌ Slower (starts server for each test)
143+
- **Use for**: Tests that modify server state
144+
145+
### Manual Server Management
146+
- ✅ Full control over lifecycle
147+
- ✅ Can test server startup/shutdown
148+
- ❌ More verbose
149+
- **Use for**: Complex scenarios, debugging
150+
151+
### Demo Server Script
152+
- ✅ Perfect for development and debugging
153+
- ✅ Can send real HTTP requests
154+
- ✅ Easy to test endpoints manually
155+
- **Use for**: Development, manual testing
156+
157+
## 🔧 Key Features of UvicornTestServer
158+
159+
1. **Automatic Port Detection**: Finds free ports to avoid conflicts
160+
2. **Proper Lifecycle**: Clean startup and shutdown
161+
3. **Thread Safety**: Runs server in background thread
162+
4. **Health Checking**: Waits for server to be ready
163+
5. **Graceful Shutdown**: Proper cleanup on exit
164+
6. **Error Handling**: Robust error handling and timeouts
165+
166+
## 🚀 Making HTTP Requests
167+
168+
Once you have a running server, you can make requests using:
169+
170+
### With aiohttp (async)
171+
```python
172+
async with ClientSession() as session:
173+
async with session.get(f"{server.base_url}/health") as response:
174+
data = await response.json()
175+
assert data["message"] == "OK"
176+
```
177+
178+
### With httpx (async)
179+
```python
180+
async with httpx.AsyncClient() as client:
181+
response = await client.get(f"{server.base_url}/health")
182+
assert response.status_code == 200
183+
```
184+
185+
### With curl (command line)
186+
```bash
187+
curl http://127.0.0.1:8000/health
188+
curl -X POST http://127.0.0.1:8000/api/data -H "Content-Type: application/json" -d '{"key": "value"}'
189+
```
190+
191+
### With requests (sync)
192+
```python
193+
import requests
194+
response = requests.get(f"{server.base_url}/health")
195+
assert response.status_code == 200
196+
```
197+
198+
## 🎉 Success!
199+
200+
Now you have a proper way to run uvicorn servers for integration testing that:
201+
- ✅ Actually starts and stops properly
202+
- ✅ Provides real HTTP endpoints
203+
- ✅ Handles cleanup automatically
204+
- ✅ Avoids port conflicts
205+
- ✅ Works reliably in CI/CD
206+
207+
No more blocking `uvicorn.run()` calls or tests that never execute!

0 commit comments

Comments
 (0)