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 session
def 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_gig
File: 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 venue
File: 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 True
Purpose: 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 True
Purpose: 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": "[email protected]", "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 body
Purpose: 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_venue
venue_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_client
def 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_status
def test_create_venue_requires_name(client): response = client.post( "/api/venues", json={"address": "123 Missing Name Street"}, ) assert response.status_code == 422
git clone https://github.com/BrianGator/Python-PyTest-Scripting.git
cd Python-PyTest-Scriptingcd gig_trackerpython --version
The project is configured for Python ^3.12.
pip install poetry
poetry install
poetry run fastapi dev app.py
Alternative:
poetry run uvicorn gig_tracker.app:app --reload
After tests are added under gig_tracker/tests, run:
poetry run pytest
poetry run pytest tests/test_venues_api.py
poetry run pytest tests/test_venues_api.py::test_get_venues_returns_list
poetry run pytest -v
poetry 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 == 200
Recommended 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 -v
poetry add --group dev pytest-cov poetry run pytest --cov=gig_tracker --cov-report=term-missing
Validate 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: