This skill should be used when Agent 9 needs to "write tests", "create test cases", "set up test fixtures", "run test suite", "improve coverage", "debug test failures", or work with pytest/Playwright in TheMoon project.
This skill provides testing patterns, fixture management, and test automation guides for Agent 9 (Test Engineer).
backend/
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── test_utils.py # Test utilities
│ ├── unit/ # Unit tests
│ │ ├── test_services.py
│ │ └── test_repositories.py
│ ├── integration/ # Integration tests
│ │ └── test_api.py
│ └── e2e/ # End-to-end tests
│ └── test_workflows.py
frontend/
├── tests/
│ └── e2e/
│ ├── beans.spec.ts
│ └── roasting.spec.ts
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
asyncio_mode = auto
addopts = -v --tb=short
markers =
unit: Unit tests
integration: Integration tests
e2e: End-to-end tests
slow: Slow tests
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.database import Base
TEST_DATABASE_URL = "postgresql://test:test@localhost:5432/test_db"
@pytest.fixture(scope="session")
def engine():
"""Create test database engine."""
engine = create_engine(TEST_DATABASE_URL)
Base.metadata.create_all(engine)
yield engine
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine):
"""Create a new database session for each test."""
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.rollback()
session.close()
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.core.database import get_db
@pytest.fixture
def client(db_session):
"""Create test client with overridden database."""
def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()
# tests/conftest.py
@pytest.fixture
def sample_bean(db_session):
"""Create a sample bean for testing."""
from app.models.bean import Bean
bean = Bean(name="Test Bean", origin="Ethiopia")
db_session.add(bean)
db_session.commit()
db_session.refresh(bean)
return bean
@pytest.fixture
def sample_beans(db_session):
"""Create multiple sample beans."""
from app.models.bean import Bean
beans = [
Bean(name="Ethiopian", origin="Ethiopia", roast_level="light"),
Bean(name="Colombian", origin="Colombia", roast_level="medium"),
Bean(name="Brazilian", origin="Brazil", roast_level="dark"),
]
db_session.add_all(beans)
db_session.commit()
return beans
# tests/unit/test_bean_service.py
import pytest
from unittest.mock import Mock, patch
from app.services.bean_service import BeanService
class TestBeanService:
"""Unit tests for BeanService."""
def test_get_by_origin_returns_beans(self):
"""Test that get_by_origin returns beans from repository."""
# Arrange
mock_repo = Mock()
mock_repo.get_by_origin.return_value = [
Mock(id=1, name="Bean1", origin="Ethiopia")
]
service = BeanService(mock_repo)
# Act
result = service.get_by_origin("Ethiopia")
# Assert
assert len(result) == 1
mock_repo.get_by_origin.assert_called_once_with("Ethiopia")
def test_get_by_origin_empty_list_when_none(self):
"""Test that empty list returned when no beans found."""
mock_repo = Mock()
mock_repo.get_by_origin.return_value = []
service = BeanService(mock_repo)
result = service.get_by_origin("Unknown")
assert result == []
# tests/integration/test_beans_api.py
import pytest
class TestBeansAPI:
"""Integration tests for beans API endpoints."""
def test_list_beans_success(self, client, sample_beans):
"""Test GET /api/v1/beans returns list of beans."""
response = client.get("/api/v1/beans")
assert response.status_code == 200
data = response.json()
assert len(data) == 3
def test_list_beans_with_filter(self, client, sample_beans):
"""Test filtering beans by origin."""
response = client.get("/api/v1/beans?origin=Ethiopia")
assert response.status_code == 200
data = response.json()
assert all(b["origin"] == "Ethiopia" for b in data)
def test_create_bean_success(self, client):
"""Test POST /api/v1/beans creates new bean."""
payload = {"name": "New Bean", "origin": "Kenya"}
response = client.post("/api/v1/beans", json=payload)
assert response.status_code == 201
data = response.json()
assert data["name"] == "New Bean"
assert data["id"] is not None
def test_create_bean_validation_error(self, client):
"""Test validation error for invalid input."""
payload = {"name": "", "origin": "Kenya"} # Empty name
response = client.post("/api/v1/beans", json=payload)
assert response.status_code == 422
def test_get_bean_not_found(self, client):
"""Test 404 for non-existent bean."""
response = client.get("/api/v1/beans/99999")
assert response.status_code == 404
# tests/unit/test_bean_repository.py
import pytest
from app.repositories.bean_repository import BeanRepository
from app.schemas.bean import BeanCreate
class TestBeanRepository:
"""Tests for BeanRepository."""
def test_create_bean(self, db_session):
"""Test creating a new bean."""
repo = BeanRepository(db_session)
bean_in = BeanCreate(name="Test", origin="Ethiopia")
bean = repo.create(bean_in)
assert bean.id is not None
assert bean.name == "Test"
assert bean.origin == "Ethiopia"
def test_get_by_origin(self, db_session, sample_beans):
"""Test filtering by origin."""
repo = BeanRepository(db_session)
beans = repo.get_by_origin("Ethiopia")
assert len(beans) >= 1
assert all(b.origin == "Ethiopia" for b in beans)
def test_get_multi_with_pagination(self, db_session, sample_beans):
"""Test pagination works correctly."""
repo = BeanRepository(db_session)
page1 = repo.get_multi(skip=0, limit=2)
page2 = repo.get_multi(skip=2, limit=2)
assert len(page1) == 2
assert page1[0].id != page2[0].id if page2 else True
// tests/e2e/pages/BeansPage.ts
import { Page, Locator } from '@playwright/test';
export class BeansPage {
readonly page: Page;
readonly beanCards: Locator;
readonly addButton: Locator;
readonly searchInput: Locator;
constructor(page: Page) {
this.page = page;
this.beanCards = page.locator('[data-testid="bean-card"]');
this.addButton = page.locator('[data-testid="add-bean"]');
this.searchInput = page.locator('[data-testid="search-input"]');
}
async goto() {
await this.page.goto('/beans');
}
async search(query: string) {
await this.searchInput.fill(query);
}
async getBeanCount() {
return await this.beanCards.count();
}
}
// tests/e2e/beans.spec.ts
import { test, expect } from '@playwright/test';
import { BeansPage } from './pages/BeansPage';
test.describe('Beans Page', () => {
test('should display list of beans', async ({ page }) => {
const beansPage = new BeansPage(page);
await beansPage.goto();
const count = await beansPage.getBeanCount();
expect(count).toBeGreaterThan(0);
});
test('should filter beans by search', async ({ page }) => {
const beansPage = new BeansPage(page);
await beansPage.goto();
await beansPage.search('Ethiopian');
const cards = beansPage.beanCards;
await expect(cards.first()).toContainText('Ethiopian');
});
});
# Run all tests
cd backend && pytest
# Run with coverage
pytest --cov=app --cov-report=html
# Run specific marker
pytest -m unit
pytest -m integration
# Run specific file
pytest tests/unit/test_services.py
# Run specific test
pytest tests/unit/test_services.py::TestBeanService::test_get_by_origin
# Run Playwright tests
cd frontend && npx playwright test
# Run with UI
npx playwright test --ui
# Run specific test file
npx playwright test beans.spec.ts
| Type | Target | Current |
|---|---|---|
| Unit Tests | 90% | Check with --cov |
| Integration | 80% | |
| E2E | Critical paths |
| Task | Command |
|---|---|
| Run all tests | pytest |
| With coverage | pytest --cov=app |
| Verbose output | pytest -v |
| Stop on first fail | pytest -x |
| Run marked tests | pytest -m unit |
| E2E tests | npx playwright test |