This repository contains a Python testing and scripting project centered around a FastAPI gig tracking application plus supporting course scripts. The main application is located in the gig_tracker folder and uses FastAPI, SQLModel, Jinja templates, authentication helpers, route-based API organization, seed data, and development tooling for PyTest, Ruff, and Mypy.
At the time this README was updated, the repository included application code and course scripts, but no committed pytest test files were found in the indexed repository. This README documents the current application and adds a recommended pytest test plan with sample code that can be added next.
Repository:
- Project Summary
- Languages Used
- Tools and Frameworks
- Project File Links
- File Structure
- Application Architecture
- Current Function and Route Inventory
- Detailed Test Inventory
- Recommended PyTest Test Suite
- Code Methodology Summary
- Sample Code From the Project
- More Example PyTest Code
- Tutorial and Setup Guide
- PyTest Automation Tips
- Recommended Improvements
- Author
| Language / Format | Purpose |
|---|---|
| Python | Main application, API, routes, schemas, scripts, and future pytest tests |
| TOML | Poetry project and dependency configuration |
| HTML / Jinja | Server-rendered UI templates |
| CSS | Static styling |
| Markdown | Repository documentation |
| Tool / Framework | Purpose |
|---|---|
| Python 3.12 | Main runtime configured in Poetry |
| FastAPI | API and web application framework |
| PyTest | Python test framework included as a development dependency |
| SQLModel | Database models, validation, and ORM behavior |
| SQLite | Local database engine |
| Poetry | Dependency and environment management |
| Jinja2 / jinja2-fragments | Server-rendered UI templates and partial rendering |
| SlowAPI | Rate limiting middleware |
| Passlib / bcrypt | Password hashing and verification support |
| PyJWT | JWT token support |
| Faker | Fake seed data generation |
| Requests | HTTP request scripting support |
| Ruff | Python linting |
| Mypy | Static type checking |
| File / Folder | Description |
|---|---|
| README.md | Main project documentation |
| gig_tracker/pyproject.toml | Poetry configuration, dependencies, and dev tools |
| gig_tracker/app.py | Main FastAPI app setup, lifespan, token route, middleware, routers, and static files |
| gig_tracker/security.py | Password hashing, token creation, token verification, and authentication helpers |
| gig_tracker/schema/base.py | Database engine, sessions, table creation/drop, and seed data |
| gig_tracker/schema/user.py | User SQLModel classes |
| gig_tracker/schema/client.py | Client SQLModel classes |
| gig_tracker/schema/venue.py | Venue SQLModel classes |
| gig_tracker/schema/gig.py | Gig SQLModel classes |
| gig_tracker/routes/gig_routes.py | Gig REST API CRUD routes |
| gig_tracker/routes/client_routes.py | Protected client API routes |
| gig_tracker/routes/user_routes.py | User API routes using Basic authentication flow |
| gig_tracker/routes/venue_routes.py | Venue REST API CRUD routes |
| gig_tracker/routes/ui_routes.py | Server-rendered UI routes, form handlers, search, filters, and snippets |
| course_scripts | Python scripting practice/course files |
Python-PyTest-Scripting/
|
|-- README.md
|
|-- course_scripts/
| |-- 01_10/
| |-- 02_01/
| |-- 02_02/
| |-- 02_03/
| |-- 02_04/
| |-- 02_06/
| |-- 03_03/
| |-- 03_04/
| |-- 03_06/
| |-- 04_02/
| |-- 04_03/
| |-- 04_04/
| |-- 04_05/
| |-- 04_07/
| `-- 04_09/
|
`-- gig_tracker/
|-- app.py
|-- security.py
|-- pyproject.toml
|-- poetry.lock
|
|-- routes/
| |-- client_routes.py
| |-- gig_routes.py
| |-- ui_routes.py
| |-- user_routes.py
| `-- venue_routes.py
|
|-- schema/
| |-- base.py
| |-- client.py
| |-- gig.py
| |-- user.py
| `-- venue.py
|
|-- static/
| `-- css/
| `-- main.css
|
`-- templates/
|-- index.html
|-- gigs.html
|-- clients.html
|-- venue.html
|-- search.html
`-- snippets/
Recommended future test structure:
gig_tracker/
`-- tests/
|-- conftest.py
|-- test_auth.py
|-- test_gigs_api.py
|-- test_clients_api.py
|-- test_venues_api.py
|-- test_users_api.py
|-- test_ui_routes.py
|-- test_security.py
`-- test_database_seed.py
The gig_tracker application follows a layered FastAPI design.
Main file:
Responsibilities:
- Creates the FastAPI app.
- Runs startup/shutdown lifecycle logic.
- Creates and seeds database tables.
- Drops database tables during shutdown.
- Configures rate limiting middleware.
- Defines the
/tokenauthentication route. - Includes API and UI routers.
- Mounts static files.
| Router | Responsibility |
|---|---|
gig_routes.py |
Create, read, update, and delete gigs |
client_routes.py |
Read and create clients using bearer-token validation |
venue_routes.py |
Create, read, update, and delete venues |
user_routes.py |
Create and read users using Basic authentication logic |
ui_routes.py |
Render HTML pages, forms, search results, filters, and snippets |
| Schema File | Models |
|---|---|
user.py |
UserBase, User, UserPublic, UserCreate |
client.py |
ClientBase, Client, ClientCreate, ClientPublic |
venue.py |
VenueBase, Venue, VenueCreate, VenuePublic, Venues |
gig.py |
GigBase, Gig, GigCreate, GigPublic, Gigs |
Security helpers are defined in:
Responsibilities:
- Hash passwords.
- Verify passwords.
- Create access tokens.
- Verify access tokens.
- Authenticate users.
Security note: production applications should load secret values from environment variables or a secret manager rather than keeping them directly in source code.
File: gig_tracker/app.py
Purpose:
- Creates database tables.
- Seeds initial data.
- Drops database tables when the app shuts down.
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables()
seed_db()
yield
drop_db_and_tables()Route: POST /token
File: gig_tracker/app.py
Purpose:
- Looks up a user by username.
- Validates the submitted password.
- Returns an access token response when authentication succeeds.
- Returns HTTP
401when authentication fails.
File: gig_tracker/schema/base.py
| Function | Purpose |
|---|---|
create_db_and_tables() |
Creates SQLModel database tables |
drop_db_and_tables() |
Drops SQLModel database tables |
get_session() |
Provides a database session dependency |
create_venues(session) |
Creates fake venue records and a duplicate venue |
create_user(session) |
Creates a seeded user with a hashed password |
create_clients(session) |
Creates fake client records |
create_gigs(session, venue_ids, client_ids) |
Creates fake gig records tied to venues and clients |
seed_db() |
Runs the full seed process |
def get_session():
with Session(engine) as session:
yield sessiondef seed_db():
with Session(engine) as session:
venue_ids = create_venues(session)
create_user(session)
client_ids = create_clients(session)
create_gigs(session, venue_ids, client_ids)File: gig_tracker/routes/gig_routes.py
| Route | Function | Purpose |
|---|---|---|
POST /api/gigs |
create_gig |
Creates a gig and returns 201 |
GET /api/gigs |
get_gigs |
Returns all gigs |
PUT /api/gigs/{gig_id} |
update_gig |
Updates an existing gig or returns 404 |
DELETE /api/gigs/{gig_id} |
delete_gig |
Deletes an existing gig or returns 404 |
@gig_router.post("/gigs", response_model=GigPublic, status_code=201)
def create_gig(request: Request, gig: GigCreate, session: SessionDep):
db_gig = Gig.model_validate(gig)
session.add(db_gig)
session.commit()
session.refresh(db_gig)
return db_gigFile: gig_tracker/routes/client_routes.py
| Route | Function | Purpose |
|---|---|---|
GET /api/clients |
get_clients |
Returns clients after bearer token validation |
GET /api/clients/{client_id} |
get_client |
Returns one client and can simulate intermittent server errors |
POST /api/clients |
create_client |
Creates a client after bearer token validation |
The IntermitentErrorGenerator class intentionally simulates intermittent server behavior so tests can cover retry and resilience scenarios.
File: gig_tracker/routes/venue_routes.py
| Route | Function | Purpose |
|---|---|---|
POST /api/venues |
create_venue |
Creates a venue and returns 201 |
GET /api/venues |
get_venues |
Returns all venues |
GET /api/venues/{venue_id} |
get_venue |
Returns one venue or 404 |
PUT /api/venues/{venue_id} |
update_venue |
Updates one venue or 404 |
DELETE /api/venues/{venue_id} |
delete_venue |
Deletes one venue or 404 |
@venue_router.get("/venues/{venue_id}", response_model=VenuePublic)
def get_venue(venue_id: int, session: SessionDep):
venue = session.get(Venue, venue_id)
if not venue:
raise HTTPException(status_code=404, detail="Venue not found")
return venueFile: gig_tracker/routes/user_routes.py
| Route | Function | Purpose |
|---|---|---|
GET /api/users/{user_id} |
get_user |
Returns one user after Basic authentication |
GET /api/users |
get_users |
Returns the authenticated user |
POST /api/users |
create_user |
Creates a new user with a hashed password |
File: gig_tracker/routes/ui_routes.py
| Route | Function | Purpose |
|---|---|---|
GET / |
index |
Renders the home page |
GET /gigs |
gigs |
Shows upcoming gigs |
GET /clients |
get_clients |
Shows clients page |
POST /clients |
create_client |
Creates a client from form data |
GET /venues |
venues |
Shows venues page |
POST /venues |
create_venue |
Creates a venue from form data |
POST /gigs |
create_gig |
Creates a gig from form data |
GET /load_venue_options |
get_venues_as_options |
Returns venue dropdown options snippet |
GET /load_client_options |
get_clients_as_options |
Returns client dropdown options snippet |
POST /search |
search |
Searches gigs by name |
GET /filter_gigs |
filter_gigs |
Filters gigs by past/upcoming status |
@ui_router.post("/search", include_in_schema=False)
def search(request: Request, session: SessionDep, search: Annotated[str, Form()]):
search_results = session.exec(
select(Gig).where(col(Gig.name).contains(search))
).all()
return templates.TemplateResponse(
"search.html", {"request": request, "search_results": search_results}
)No committed pytest test files were found in the indexed repository at the time this README was updated.
| Test Area | Current Status | Recommended File |
|---|---|---|
| App startup/lifespan test | Not yet implemented | tests/test_app_startup.py |
| Token login success/failure tests | Not yet implemented | tests/test_auth.py |
| Password hashing tests | Not yet implemented | tests/test_security.py |
| Token verification tests | Not yet implemented | tests/test_security.py |
| Venue CRUD API tests | Not yet implemented | tests/test_venues_api.py |
| Gig CRUD API tests | Not yet implemented | tests/test_gigs_api.py |
| Client protected API tests | Not yet implemented | tests/test_clients_api.py |
| User Basic Auth tests | Not yet implemented | tests/test_users_api.py |
| UI route rendering tests | Not yet implemented | tests/test_ui_routes.py |
| Database seed tests | Not yet implemented | tests/test_database_seed.py |
| Rate limit behavior tests | Not yet implemented | tests/test_rate_limits.py |
Recommended file: gig_tracker/tests/test_ui_routes.py
from fastapi.testclient import TestClient
from gig_tracker.app import app
client = TestClient(app)
def test_root_page_loads():
response = client.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]Purpose: Confirms the FastAPI app can render the root UI page.
Methodologies demonstrated: UI route smoke testing, FastAPI TestClient usage, response-header assertion.
Recommended file: gig_tracker/tests/test_auth.py
def test_token_login_rejects_invalid_credentials():
response = client.post(
"/token",
data={"username": "invalid-user", "password": "invalid-value"},
)
assert response.status_code == 401
assert response.json()["detail"] == "Incorrect username or password"Purpose: Validates negative authentication behavior.
Methodologies demonstrated: Negative testing, API security testing, response body assertion.
Recommended file: gig_tracker/tests/test_security.py
from gig_tracker.security import hash_password, verify_password
def test_hash_password_can_be_verified():
value = "sample-test-value"
hashed_value = hash_password(value)
assert hashed_value != value
assert verify_password(value, hashed_value.decode("utf-8")) is TruePurpose: Confirms that hashing and verification work together.
Methodologies demonstrated: Unit testing, security helper testing, behavior validation.
Recommended file: gig_tracker/tests/test_security.py
from gig_tracker.security import create_access_token, verify_token
def test_create_access_token_can_be_verified():
token = create_access_token({"sub": "sample-user"})
assert isinstance(token, str)
assert verify_token(token) is TruePurpose: Confirms token creation and verification behavior.
Methodologies demonstrated: Unit testing, token validation, security helper testing.
Recommended file: gig_tracker/tests/test_venues_api.py
def test_get_venues_returns_list():
response = client.get("/api/venues")
assert response.status_code == 200
assert isinstance(response.json(), list)Purpose: Confirms the venues API returns a list response.
Methodologies demonstrated: API smoke testing, response schema validation.
Recommended file: gig_tracker/tests/test_venues_api.py
def test_create_venue_returns_201():
payload = {
"name": "QA Test Venue",
"address": "123 Test Street",
"contact_number": "555-0100",
"contact_email": "venue@example.com",
"capacity": 250,
"notes": "Created by pytest",
}
response = client.post("/api/venues", json=payload)
assert response.status_code == 201
body = response.json()
assert body["name"] == "QA Test Venue"
assert "id" in bodyPurpose: Validates venue creation.
Methodologies demonstrated: POST testing, create API validation, response field assertions.
Recommended file: gig_tracker/tests/test_venues_api.py
def test_get_missing_venue_returns_404():
response = client.get("/api/venues/999999")
assert response.status_code == 404
assert response.json()["detail"] == "Venue not found"Purpose: Validates not-found behavior.
Methodologies demonstrated: Negative API testing, error response validation.
Recommended file: gig_tracker/tests/test_gigs_api.py
def test_create_gig_returns_201():
gig_payload = {
"date": "2026-06-01",
"time": "19:30:00",
"name": "QA Automation Gig",
"venue_id": 1,
"client_id": 1,
}
response = client.post("/api/gigs", json=gig_payload)
assert response.status_code == 201
assert response.json()["name"] == "QA Automation Gig"Purpose: Validates gig creation using related venue and client IDs.
Methodologies demonstrated: API workflow testing, related data validation, create route testing.
Recommended file: gig_tracker/tests/test_ui_routes.py
def test_filter_gigs_returns_html_snippet():
response = client.get("/filter_gigs?filter=past")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]Purpose: Confirms the UI filtering endpoint returns an HTML snippet.
Methodologies demonstrated: Server-rendered UI testing, query parameter testing, HTML response validation.
Recommended file: gig_tracker/tests/test_ui_routes.py
def test_search_route_returns_html():
response = client.post("/search", data={"search": "test"})
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]Purpose: Validates the search form route.
Methodologies demonstrated: Form post testing, UI route validation, server-rendered response testing.
The project separates behavior by resource type: gigs, clients, users, venues, and UI. This keeps responsibilities clear and improves maintainability.
The project uses SQLModel to define database tables and request/response models.
FastAPI dependencies are used for database sessions and authentication.
SessionDep = Annotated[Session, Depends(get_session)]The app includes token-based authentication helpers and Basic authentication flow for selected routes.
SlowAPI middleware and decorators are used to rate-limit selected endpoints.
@limiter.limit("5/minute")The seed functions create fake venues, users, clients, and gigs so the app can run with sample data.
app = FastAPI(lifespan=lifespan)
app.state.limiter = limiter
app.add_exception_handler(429, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)app.include_router(ui_router)
app.include_router(gig_router)
app.include_router(user_router)
app.include_router(client_router)
app.include_router(venue_router)app.mount("/static", StaticFiles(directory="./static"), name="static")db_venue = Venue.model_validate(venue)
session.add(db_venue)
session.commit()
session.refresh(db_venue)
return db_venuevenue_data = venue.model_dump(exclude_unset=True)
db_venue.sqlmodel_update(venue_data)
session.add(db_venue)
session.commit()
session.refresh(db_venue)import pytest
from fastapi.testclient import TestClient
from gig_tracker.app import app
@pytest.fixture
def client():
with TestClient(app) as test_client:
yield test_clientdef test_get_venues_returns_list(client):
response = client.get("/api/venues")
assert response.status_code == 200
assert isinstance(response.json(), list)import pytest
@pytest.mark.parametrize(
"path, expected_status",
[
("/", 200),
("/gigs", 200),
("/clients", 200),
("/venues", 200),
],
)
def test_ui_pages_load(client, path, expected_status):
response = client.get(path)
assert response.status_code == expected_statusdef test_create_venue_requires_name(client):
response = client.post(
"/api/venues",
json={"address": "123 Missing Name Street"},
)
assert response.status_code == 422git clone https://github.com/BrianGator/Python-PyTest-Scripting.git
cd Python-PyTest-Scriptingcd gig_trackerpython --versionThe project is configured for Python ^3.12.
pip install poetrypoetry installpoetry run fastapi dev app.pyAlternative:
poetry run uvicorn gig_tracker.app:app --reloadAfter tests are added under gig_tracker/tests, run:
poetry run pytestpoetry run pytest tests/test_venues_api.pypoetry run pytest tests/test_venues_api.py::test_get_venues_returns_listpoetry run pytest -vpoetry run ruff check .poetry run mypy .@pytest.fixture
def venue_payload():
return {
"name": "QA Venue",
"address": "123 Test Street",
}Each test should create or control the data it needs. Avoid depending on another test to run first.
For each route, test success and failure cases.
Examples:
GET /api/venuesreturns200.GET /api/venues/999999returns404.POST /api/venueswith valid data returns201.POST /api/venueswith invalid data returns422.
Good:
def test_get_missing_venue_returns_404():Avoid:
def test_1():assert response.status_code == 200
body = response.json()
assert isinstance(body, list)Use environment variables for secrets and tokens. Application secret keys should not be committed in source code for production systems.
@pytest.mark.parametrize("path", ["/", "/gigs", "/clients", "/venues"])
def test_pages_load(client, path):
assert client.get(path).status_code == 200Recommended organization:
tests/unit/
tests/api/
tests/ui/
Add actual pytest files under:
gig_tracker/tests/
Use a separate test database so test runs do not interfere with development data.
Move application secrets into environment variables or a .env file that is not committed.
Recommended path:
.github/workflows/python-tests.yml
Example workflow:
name: Python PyTest
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Poetry
run: pip install poetry
- name: Install dependencies
working-directory: gig_tracker
run: poetry install
- name: Run tests
working-directory: gig_tracker
run: poetry run pytest -vpoetry add --group dev pytest-cov
poetry run pytest --cov=gig_tracker --cov-report=term-missingValidate response schemas and required fields for each API endpoint.
The app uses SlowAPI rate limiting. Add tests to verify configured limits return 429 when exceeded.
For full end-to-end browser testing, add Playwright or Selenium-based UI tests after the API and route tests are stable.
Written by Brian McCarthy
Project repository: