Skip to content

Commit d21db80

Browse files
authored
Merge pull request #27 from mongodb/python-unit-tests
test(python): Add testing infrastructure and tests
2 parents 8276d84 + 5d26e64 commit d21db80

File tree

6 files changed

+416
-0
lines changed

6 files changed

+416
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Generate Test Summary from Pytest JUnit XML Output
5+
# Usage: ./generate-test-summary-pytest.sh <path-to-junit-xml>
6+
7+
XML_FILE="${1:-test-results.xml}"
8+
9+
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
10+
echo "" >> $GITHUB_STEP_SUMMARY
11+
12+
# Parse test results from JUnit XML
13+
if [ -f "$XML_FILE" ]; then
14+
# Extract test counts from XML
15+
# JUnit XML structure: <testsuite tests="N" failures="N" errors="N" skipped="N">
16+
17+
tests=$(grep -oP 'tests="\K[0-9]+' "$XML_FILE" | head -1)
18+
failures=$(grep -oP 'failures="\K[0-9]+' "$XML_FILE" | head -1)
19+
errors=$(grep -oP 'errors="\K[0-9]+' "$XML_FILE" | head -1)
20+
skipped=$(grep -oP 'skipped="\K[0-9]+' "$XML_FILE" | head -1)
21+
22+
# Default to 0 if values are empty
23+
tests=${tests:-0}
24+
failures=${failures:-0}
25+
errors=${errors:-0}
26+
skipped=${skipped:-0}
27+
28+
passed=$((tests - failures - errors - skipped))
29+
30+
echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY
31+
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
32+
echo "| ✅ Passed | $passed |" >> $GITHUB_STEP_SUMMARY
33+
echo "| ❌ Failed | $((failures + errors)) |" >> $GITHUB_STEP_SUMMARY
34+
echo "| ⏭️ Skipped | $skipped |" >> $GITHUB_STEP_SUMMARY
35+
echo "| **Total** | **$tests** |" >> $GITHUB_STEP_SUMMARY
36+
echo "" >> $GITHUB_STEP_SUMMARY
37+
38+
# List failed tests if any
39+
if [ $((failures + errors)) -gt 0 ]; then
40+
echo "### ❌ Failed Tests" >> $GITHUB_STEP_SUMMARY
41+
echo "" >> $GITHUB_STEP_SUMMARY
42+
43+
# Extract failed test names from XML
44+
failed_tests_file=$(mktemp)
45+
46+
# Find testcase elements with failure or error children
47+
grep -oP '<testcase[^>]*classname="[^"]*"[^>]*name="[^"]*"[^>]*>.*?<(failure|error)' "$XML_FILE" | \
48+
grep -oP 'classname="\K[^"]*|name="\K[^"]*' | \
49+
paste -d '.' - - >> "$failed_tests_file" 2>/dev/null || true
50+
51+
if [ -s "$failed_tests_file" ]; then
52+
while IFS= read -r test; do
53+
echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY
54+
done < "$failed_tests_file"
55+
else
56+
echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY
57+
fi
58+
59+
echo "" >> $GITHUB_STEP_SUMMARY
60+
echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY
61+
rm -f "$failed_tests_file"
62+
exit 1
63+
else
64+
echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY
65+
fi
66+
else
67+
echo "⚠️ No test results found at: $XML_FILE" >> $GITHUB_STEP_SUMMARY
68+
exit 1
69+
fi
70+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Run Python Tests
2+
3+
on:
4+
pull_request_target:
5+
branches:
6+
- development
7+
push:
8+
branches:
9+
- development
10+
11+
jobs:
12+
test:
13+
name: Run Python Tests
14+
runs-on: ubuntu-latest
15+
# Require manual approval for fork PRs
16+
environment: testing
17+
18+
defaults:
19+
run:
20+
working-directory: server/python
21+
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v5
25+
with:
26+
ref: ${{ github.event.pull_request.head.sha }}
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: '3.13'
32+
cache: 'pip'
33+
34+
- name: Install dependencies
35+
run: |
36+
python -m pip install --upgrade pip
37+
pip install -r requirements.txt
38+
39+
- name: Run tests
40+
run: pytest --verbose --tb=short --junit-xml=test-results.xml || true
41+
env:
42+
MONGO_URI: ${{ secrets.MFLIX_URI }}
43+
MONGO_DB: sample_mflix
44+
45+
- name: Upload test results
46+
uses: actions/upload-artifact@v4
47+
if: always()
48+
with:
49+
name: test-results
50+
path: |
51+
server/python/test-results.xml
52+
server/python/htmlcov/
53+
retention-days: 30
54+
55+
- name: Generate Test Summary
56+
if: always()
57+
working-directory: .
58+
run: |
59+
chmod +x .github/scripts/generate-test-summary-pytest.sh
60+
.github/scripts/generate-test-summary-pytest.sh server/python/test-results.xml
61+

server/python/pytest.ini

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[pytest]
2+
# Pytest configuration for Python FastAPI backend tests
3+
4+
# Test discovery patterns
5+
python_files = test_*.py *_test.py
6+
python_classes = Test*
7+
python_functions = test_*
8+
9+
# Test paths
10+
testpaths = tests
11+
12+
# Output options
13+
addopts =
14+
-v
15+
--strict-markers
16+
--tb=short
17+
--asyncio-mode=auto
18+
--color=yes
19+
20+
# Markers for categorizing tests
21+
markers =
22+
unit: Unit tests with mocked dependencies
23+
integration: Integration tests requiring database
24+
slow: Tests that take longer to run
25+
26+
# Async settings
27+
asyncio_mode = auto
28+
asyncio_default_fixture_loop_scope = function
29+
30+
# Coverage settings (optional)
31+
# Uncomment to enable coverage reporting
32+
# addopts = --cov=src --cov-report=html --cov-report=term
33+

server/python/tests/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Tests package
2+

server/python/tests/conftest.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Pytest configuration and fixtures for testing.
3+
4+
This file contains shared fixtures and configuration for all tests.
5+
"""
6+
7+
import pytest
8+
import sys
9+
from pathlib import Path
10+
11+
# Add the parent directory to the path so we can import from src
12+
sys.path.insert(0, str(Path(__file__).parent.parent))
13+
14+
# Configure pytest-asyncio
15+
pytest_plugins = ('pytest_asyncio',)
16+
17+
18+
@pytest.fixture
19+
def sample_movie():
20+
"""Sample movie data for testing."""
21+
return {
22+
"_id": "507f1f77bcf86cd799439011",
23+
"title": "Test Movie",
24+
"year": 2024,
25+
"plot": "A test movie plot",
26+
"genres": ["Action", "Drama"],
27+
"directors": ["Test Director"],
28+
"cast": ["Actor 1", "Actor 2"],
29+
"runtime": 120,
30+
"rated": "PG-13"
31+
}
32+
33+
34+
@pytest.fixture
35+
def sample_movies():
36+
"""Multiple sample movies for testing."""
37+
return [
38+
{
39+
"_id": "507f1f77bcf86cd799439011",
40+
"title": "Test Movie 1",
41+
"year": 2024,
42+
"plot": "First test movie",
43+
"genres": ["Action"],
44+
},
45+
{
46+
"_id": "507f1f77bcf86cd799439012",
47+
"title": "Test Movie 2",
48+
"year": 2023,
49+
"plot": "Second test movie",
50+
"genres": ["Comedy"],
51+
},
52+
{
53+
"_id": "507f1f77bcf86cd799439013",
54+
"title": "Test Movie 3",
55+
"year": 2024,
56+
"plot": "Third test movie",
57+
"genres": ["Drama"],
58+
}
59+
]
60+
61+
62+
@pytest.fixture
63+
def mock_success_response():
64+
"""Mock success response structure."""
65+
def _create_response(data, message="Success"):
66+
return {
67+
"success": True,
68+
"message": message,
69+
"data": data,
70+
"timestamp": "2024-01-01T00:00:00.000Z"
71+
}
72+
return _create_response
73+
74+
75+
@pytest.fixture
76+
def mock_error_response():
77+
"""Mock error response structure."""
78+
def _create_response(message, code=None, details=None):
79+
return {
80+
"success": False,
81+
"message": message,
82+
"error": {
83+
"message": message,
84+
"code": code,
85+
"details": details
86+
},
87+
"timestamp": "2024-01-01T00:00:00.000Z"
88+
}
89+
return _create_response
90+

0 commit comments

Comments
 (0)