test-generator
2
总安装量
1
周安装量
#72322
全站排名
安装命令
npx skills add https://github.com/u9401066/copilot-capability-manager --skill test-generator
Agent 安装分布
openclaw
1
opencode
1
cursor
1
claude-code
1
gemini-cli
1
Skill 文档
æ¸¬è©¦çææè½
æè¿°
çºæå®çç¨å¼ç¢¼èªåçæå®æ´æ¸¬è©¦å¥ä»¶ï¼å å«éæ åæãå®å æ¸¬è©¦ãæ´åæ¸¬è©¦åè¦èçå ±åã
è§¸ç¼æ¢ä»¶
- ãçææ¸¬è©¦ããã寫測試ãããtest thisã
- ã建ç«å®å 測試ãããå»ºç«æ´å測試ã
- ãéæ åæãããtype checkã
- ãè¦èçãããcoverageã
測試éåå¡
/\
/ \ E2E Tests (å°é)
/----\
/ \ Integration Tests (ä¸ç)
/--------\
/ \ Unit Tests (大é)
/------------\
/ Static Analysis (åºç¤)
Python 測試çç¥
1ï¸â£ éæ åæ (Static Analysis)
å·¥å ·é ç½®
| å·¥å · | ç¨é | é ç½®æª |
|---|---|---|
| mypy | é¡åæª¢æ¥ | pyproject.toml / mypy.ini |
| ruff | Linting + Formatting (å代 pylint/flake8/black) | pyproject.toml |
| bandit | å®å ¨æ§ææ | .bandit |
mypy é ç½®ç¯ä¾
# pyproject.toml
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false
ruff é ç½®ç¯ä¾
# pyproject.toml
[tool.ruff]
target-version = "py311"
line-length = 88
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
]
ignore = ["E501"] # line too long (handled by formatter)
[tool.ruff.isort]
known-first-party = ["src"]
2ï¸â£ å®å 測試 (Unit Tests)
pytest é ç½®
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--tb=short",
"-ra",
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
]
æ¸¬è©¦çµæ§
tests/
âââ __init__.py
âââ conftest.py # å
±ç¨ fixtures
âââ unit/ # å®å
測試
â âââ __init__.py
â âââ conftest.py
â âââ test_domain/ # Domain 層測試
â âââ test_application/ # Application 層測試
â âââ test_utils/
âââ integration/ # æ´å測試
â âââ __init__.py
â âââ conftest.py
â âââ test_api/
â âââ test_database/
â âââ test_external/
âââ e2e/ # 端å°ç«¯æ¸¬è©¦
âââ ...
å®å 測試ç¯ä¾
# tests/unit/test_domain/test_user.py
import pytest
from src.domain.entities import User
from src.domain.exceptions import ValidationError
class TestUser:
"""User entity å®å
測試"""
# === Happy Path ===
def test_create_user_with_valid_data(self):
"""æ£å¸¸å»ºç«ä½¿ç¨è
"""
user = User(name="Alice", email="alice@example.com")
assert user.name == "Alice"
assert user.email == "alice@example.com"
# === éçæ¢ä»¶ ===
def test_create_user_with_minimum_name_length(self):
"""å稱æå°é·åº¦"""
user = User(name="A", email="a@b.c")
assert len(user.name) == 1
@pytest.mark.parametrize("name,expected", [
("A" * 100, 100),
("䏿åå", 4),
])
def test_name_length_variations(self, name: str, expected: int):
"""å稱é·åº¦è®å測試"""
user = User(name=name, email="test@test.com")
assert len(user.name) == expected
# === é¯èª¤èç ===
def test_create_user_with_empty_name_raises_error(self):
"""空å稱ææåº ValidationError"""
with pytest.raises(ValidationError, match="Name cannot be empty"):
User(name="", email="test@test.com")
def test_create_user_with_invalid_email_raises_error(self):
"""ç¡æ email ææåº ValidationError"""
with pytest.raises(ValidationError, match="Invalid email format"):
User(name="Test", email="not-an-email")
# === Null/None èç ===
def test_create_user_with_none_name_raises_error(self):
"""None å稱ææåº TypeError"""
with pytest.raises(TypeError):
User(name=None, email="test@test.com")
3ï¸â£ æ´å測試 (Integration Tests)
API æ´å測試
# tests/integration/test_api/test_user_api.py
import pytest
from httpx import AsyncClient
from src.main import app
@pytest.mark.integration
@pytest.mark.asyncio
class TestUserAPI:
"""User API æ´å測試"""
async def test_create_user_endpoint(self, async_client: AsyncClient):
"""POST /users 建ç«ä½¿ç¨è
"""
response = await async_client.post(
"/api/v1/users",
json={"name": "Test User", "email": "test@example.com"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test User"
assert "id" in data
async def test_get_user_endpoint(self, async_client: AsyncClient, created_user):
"""GET /users/{id} åå¾ä½¿ç¨è
"""
response = await async_client.get(f"/api/v1/users/{created_user.id}")
assert response.status_code == 200
assert response.json()["id"] == str(created_user.id)
async def test_get_nonexistent_user_returns_404(self, async_client: AsyncClient):
"""åå¾ä¸åå¨ç使ç¨è
æè¿å 404"""
response = await async_client.get("/api/v1/users/nonexistent-id")
assert response.status_code == 404
è³æåº«æ´å測試
# tests/integration/test_database/test_user_repository.py
import pytest
from src.infrastructure.repositories import UserRepository
from src.domain.entities import User
@pytest.mark.integration
class TestUserRepository:
"""UserRepository æ´å測試 (實éè³æåº«)"""
@pytest.fixture
def repository(self, db_session):
return UserRepository(session=db_session)
async def test_save_and_retrieve_user(self, repository: UserRepository):
"""å²å並åå使ç¨è
"""
user = User(name="Test", email="test@test.com")
saved_user = await repository.save(user)
retrieved = await repository.get_by_id(saved_user.id)
assert retrieved is not None
assert retrieved.name == "Test"
async def test_find_by_email(self, repository: UserRepository):
"""éé email æ¥è©¢"""
user = User(name="Test", email="unique@test.com")
await repository.save(user)
found = await repository.find_by_email("unique@test.com")
assert found is not None
assert found.email == "unique@test.com"
conftest.py (æ´å測試 fixtures)
# tests/integration/conftest.py
import pytest
import pytest_asyncio
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from src.main import app
from src.infrastructure.database import Base
# === æ¸¬è©¦è³æåº« ===
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture(scope="session")
def event_loop():
"""å»ºç« event loop for async tests"""
import asyncio
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="function")
async def db_engine():
"""å»ºç«æ¸¬è©¦è³æåº«å¼æ"""
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest_asyncio.fixture(scope="function")
async def db_session(db_engine):
"""å»ºç«æ¸¬è©¦è³æåº« session"""
async_session = sessionmaker(
db_engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
await session.rollback()
# === HTTP Client ===
@pytest_asyncio.fixture
async def async_client():
"""建ç«é忥 HTTP 測試客æ¶ç«¯"""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield client
5ï¸â£ E2E 測試 (End-to-End Tests)
E2E æ¸¬è©¦å·¥å ·é¸æ
| å·¥å · | é©ç¨å ´æ¯ | ç¹é» |
|---|---|---|
| Playwright | Web UI 測試 | è·¨ç覽å¨ãèªåçå¾ ãæªå/éå½± |
| Selenium | å³çµ± Web 測試 | å»£æ³æ¯æ´ãæçç©©å® |
| pytest + httpx | API E2E | è¼éãå¿«é |
| Locust | è² è¼/æè½æ¸¬è©¦ | 忣å¼ãPython åç |
Playwright é ç½® (æ¨è¦)
# pyproject.toml
[tool.pytest.ini_options]
markers = [
"e2e: End-to-end tests (require running application)",
]
# tests/e2e/conftest.py
import pytest
from playwright.async_api import async_playwright, Browser, Page
@pytest.fixture(scope="session")
def browser_type():
"""å¯ééç°å¢è®æ¸åæç覽å¨"""
import os
return os.getenv("BROWSER", "chromium") # chromium, firefox, webkit
@pytest.fixture(scope="session")
async def browser(browser_type: str):
"""建ç«ç覽å¨å¯¦ä¾ (session ç´å¥)"""
async with async_playwright() as p:
browser = await getattr(p, browser_type).launch(
headless=True,
slow_mo=100, # æ¾æ
¢æä½ä»¥ä¾¿è§å¯
)
yield browser
await browser.close()
@pytest.fixture
async def page(browser: Browser):
"""å»ºç«æ°é é¢ (æ¯å測試ç¨ç«)"""
context = await browser.new_context(
viewport={"width": 1280, "height": 720},
record_video_dir="test-results/videos", # é製影ç
)
page = await context.new_page()
yield page
await context.close()
@pytest.fixture
def base_url():
"""æç¨ç¨å¼ base URL"""
import os
return os.getenv("APP_URL", "http://localhost:8000")
E2E 測試ç¯ä¾
# tests/e2e/test_user_journey.py
import pytest
from playwright.async_api import Page, expect
@pytest.mark.e2e
@pytest.mark.asyncio
class TestUserJourney:
"""使ç¨è
æ
ç¨ E2E 測試"""
async def test_user_registration_flow(self, page: Page, base_url: str):
"""æ¸¬è©¦å®æ´è¨»åæµç¨"""
# 1. åå¾è¨»åé é¢
await page.goto(f"{base_url}/register")
await expect(page).to_have_title("Register")
# 2. 填寫表å®
await page.fill("input[name='username']", "testuser")
await page.fill("input[name='email']", "test@example.com")
await page.fill("input[name='password']", "SecureP@ss123")
await page.fill("input[name='confirm_password']", "SecureP@ss123")
# 3. æäº¤è¡¨å®
await page.click("button[type='submit']")
# 4. é©èçµæ
await expect(page).to_have_url(f"{base_url}/dashboard")
await expect(page.locator(".welcome-message")).to_contain_text("Welcome, testuser")
async def test_login_logout_flow(self, page: Page, base_url: str):
"""測試ç»å
¥ç»åºæµç¨"""
# ç»å
¥
await page.goto(f"{base_url}/login")
await page.fill("input[name='email']", "test@example.com")
await page.fill("input[name='password']", "SecureP@ss123")
await page.click("button[type='submit']")
await expect(page.locator(".user-menu")).to_be_visible()
# ç»åº
await page.click(".logout-button")
await expect(page).to_have_url(f"{base_url}/")
async def test_create_item_flow(self, page: Page, base_url: str, authenticated_page):
"""測試建ç«é
ç®æµç¨ (éç»å
¥)"""
await authenticated_page.goto(f"{base_url}/items/new")
await authenticated_page.fill("input[name='title']", "Test Item")
await authenticated_page.fill("textarea[name='description']", "Description")
await authenticated_page.click("button[type='submit']")
await expect(authenticated_page.locator(".success-toast")).to_be_visible()
API E2E 測試 (ç¡ UI)
# tests/e2e/test_api_e2e.py
import pytest
import httpx
@pytest.mark.e2e
@pytest.mark.asyncio
class TestAPIEndToEnd:
"""API E2E 測試 - æ¸¬è©¦å®æ´ API æµç¨"""
@pytest.fixture
async def client(self, base_url: str):
async with httpx.AsyncClient(base_url=base_url) as client:
yield client
async def test_complete_crud_flow(self, client: httpx.AsyncClient):
"""æ¸¬è©¦å®æ´ CRUD æµç¨"""
# Create
response = await client.post("/api/v1/items", json={"name": "Test"})
assert response.status_code == 201
item_id = response.json()["id"]
# Read
response = await client.get(f"/api/v1/items/{item_id}")
assert response.status_code == 200
assert response.json()["name"] == "Test"
# Update
response = await client.put(
f"/api/v1/items/{item_id}",
json={"name": "Updated"}
)
assert response.status_code == 200
# Delete
response = await client.delete(f"/api/v1/items/{item_id}")
assert response.status_code == 204
# Verify deletion
response = await client.get(f"/api/v1/items/{item_id}")
assert response.status_code == 404
6ï¸â£ è¦èç (Coverage)
pytest-cov é ç½®
# pyproject.toml
[tool.coverage.run]
source = ["src"]
branch = true
parallel = true
omit = [
"*/tests/*",
"*/__init__.py",
"*/migrations/*",
]
[tool.coverage.paths]
source = ["src"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
"@abstractmethod",
]
fail_under = 80
show_missing = true
skip_covered = true
[tool.coverage.html]
directory = "htmlcov"
[tool.coverage.xml]
output = "coverage.xml"
å·è¡è¦èç
# å®å
測試è¦èç
pytest tests/unit -v --cov=src --cov-report=term-missing --cov-report=html
# æ´å測試è¦èç
pytest tests/integration -v --cov=src --cov-report=xml --cov-append
# å
¨é¨æ¸¬è©¦ + è¦èçå ±å
pytest --cov=src --cov-report=term-missing --cov-report=html --cov-report=xml
æ¸¬è©¦æ¡æ¶å°ç §è¡¨
| èªè¨ | å®å 測試 | æ´å測試 | è¦èç | éæ åæ |
|---|---|---|---|---|
| Python | pytest | pytest + httpx | pytest-cov | mypy, ruff, bandit |
| JavaScript | Jest / Vitest | Supertest | c8 / istanbul | ESLint, TypeScript |
| TypeScript | Jest / Vitest | Supertest | c8 / istanbul | tsc –noEmit, ESLint |
| Go | testing | testing + testcontainers | go test -cover | golangci-lint |
| Rust | cargo test | cargo test | cargo-tarpaulin | clippy |
CI æ´å Checklist
çææ¸¬è©¦ææåæ¥ç¢ºèªï¼
-
pyproject.tomlå å«å®æ´æ¸¬è©¦é ç½® -
requirements-dev.txtæpyproject.tomlå 嫿¸¬è©¦ä¾è³´ - CI workflow å å«æææ¸¬è©¦é段
- è¦èçéæª»å·²è¨å®ï¼å»ºè° ⥠80%ï¼
- æ¸¬è©¦å ±åä¸å³è³ CI artifacts
測試ä¾è³´ (Python)
# pyproject.toml [project.optional-dependencies] æ requirements-dev.txt
[project.optional-dependencies]
dev = [
# Testing - Core
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"pytest-xdist>=3.3.0", # å¹³è¡æ¸¬è©¦
"pytest-mock>=3.11.0",
"pytest-timeout>=2.1.0",
"httpx>=0.24.0", # Async HTTP client for API tests
"factory-boy>=3.3.0", # Test data factories
"faker>=19.0.0", # Fake data generation
# E2E Testing
"playwright>=1.40.0", # Browser automation
"pytest-playwright>=0.4.0", # Playwright pytest plugin
"locust>=2.20.0", # Load testing (optional)
# Static Analysis
"mypy>=1.5.0",
"ruff>=0.0.290",
"bandit[toml]>=1.7.5",
# Type stubs
"types-requests",
"types-python-dateutil",
]
è¼¸åºæ ¼å¼
## 測試å¥ä»¶çæå ±å
### ð æªæ¡çµæ§
[çæç測試ç®éçµæ§]
### ð æ¸¬è©¦æ¸
å®
#### éæ
åæ
- [ ] mypy é¡å檢æ¥
- [ ] ruff linting
- [ ] bandit å®å
¨ææ
#### å®å
測試 (`tests/unit/`)
- â
æ£å¸¸æµç¨ (Happy Path)
- â
éçæ¢ä»¶ (Edge Cases)
- â
é¯èª¤èç (Error Handling)
- â
Null/None èç
#### æ´å測試 (`tests/integration/`)
- â
API ç«¯é»æ¸¬è©¦
- â
è³æåº«æä½æ¸¬è©¦
- â
å¤é¨æå測試 (mocked)
#### E2E 測試 (`tests/e2e/`)
- â
使ç¨è
æ
ç¨æ¸¬è©¦
- â
é鵿µç¨é©è
- â
è·¨çè¦½å¨æ¸¬è©¦ (Playwright)
### ð è¦èçç®æ¨
- å®å
測試ï¼â¥ 90%
- æ´å測試ï¼â¥ 70%
- 總é«è¦èï¼â¥ 80%
### âï¸ å·è¡æä»¤
[ç¸é測試å·è¡å½ä»¤]