Skip to content

Latest commit

History

History
1043 lines (738 loc) · 26.2 KB

File metadata and controls

1043 lines (738 loc) · 26.2 KB

Python PyTest Scripting

PythonPyTestFastAPISQLModelPortfolio

Written by Brian McCarthy


Project Summary

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:

Python-PyTest-Scripting


Table of Contents

  1. Project Summary
  2. Languages Used
  3. Tools and Frameworks
  4. Project File Links
  5. File Structure
  6. Application Architecture
  7. Current Function and Route Inventory
  8. Detailed Test Inventory
  9. Recommended PyTest Test Suite
  10. Code Methodology Summary
  11. Sample Code From the Project
  12. More Example PyTest Code
  13. Tutorial and Setup Guide
  14. PyTest Automation Tips
  15. Recommended Improvements
  16. Author

Languages Used

Language / FormatPurpose
PythonMain application, API, routes, schemas, scripts, and future pytest tests
TOMLPoetry project and dependency configuration
HTML / JinjaServer-rendered UI templates
CSSStatic styling
MarkdownRepository documentation

Tools and Frameworks

Tool / FrameworkPurpose
Python 3.12Main runtime configured in Poetry
FastAPIAPI and web application framework
PyTestPython test framework included as a development dependency
SQLModelDatabase models, validation, and ORM behavior
SQLiteLocal database engine
PoetryDependency and environment management
Jinja2 / jinja2-fragmentsServer-rendered UI templates and partial rendering
SlowAPIRate limiting middleware
Passlib / bcryptPassword hashing and verification support
PyJWTJWT token support
FakerFake seed data generation
RequestsHTTP request scripting support
RuffPython linting
MypyStatic type checking

Project File Links

File / FolderDescription
README.mdMain project documentation
gig_tracker/pyproject.tomlPoetry configuration, dependencies, and dev tools
gig_tracker/app.pyMain FastAPI app setup, lifespan, token route, middleware, routers, and static files
gig_tracker/security.pyPassword hashing, token creation, token verification, and authentication helpers
gig_tracker/schema/base.pyDatabase engine, sessions, table creation/drop, and seed data
gig_tracker/schema/user.pyUser SQLModel classes
gig_tracker/schema/client.pyClient SQLModel classes
gig_tracker/schema/venue.pyVenue SQLModel classes
gig_tracker/schema/gig.pyGig SQLModel classes
gig_tracker/routes/gig_routes.pyGig REST API CRUD routes
gig_tracker/routes/client_routes.pyProtected client API routes
gig_tracker/routes/user_routes.pyUser API routes using Basic authentication flow
gig_tracker/routes/venue_routes.pyVenue REST API CRUD routes
gig_tracker/routes/ui_routes.pyServer-rendered UI routes, form handlers, search, filters, and snippets
course_scriptsPython scripting practice/course files

File Structure

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

Application Architecture

The gig_tracker application follows a layered FastAPI design.

Main Application Layer

Main file:

gig_tracker/app.py

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 /token authentication route.
  • Includes API and UI routers.
  • Mounts static files.

Route Layer

RouterResponsibility
gig_routes.pyCreate, read, update, and delete gigs
client_routes.pyRead and create clients using bearer-token validation
venue_routes.pyCreate, read, update, and delete venues
user_routes.pyCreate and read users using Basic authentication logic
ui_routes.pyRender HTML pages, forms, search results, filters, and snippets

Schema / Model Layer

Schema FileModels
user.pyUserBase, User, UserPublic, UserCreate
client.pyClientBase, Client, ClientCreate, ClientPublic
venue.pyVenueBase, Venue, VenueCreate, VenuePublic, Venues
gig.pyGigBase, Gig, GigCreate, GigPublic, Gigs

Security Layer

Security helpers are defined in:

gig_tracker/security.py

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.


Current Function and Route Inventory

Main App Functions

lifespan(app: FastAPI)

File:gig_tracker/app.py

Purpose:

  • Creates database tables.
  • Seeds initial data.
  • Drops database tables when the app shuts down.
@asynccontextmanagerasyncdeflifespan(app: FastAPI):
create_db_and_tables()
seed_db()
yielddrop_db_and_tables()

login(session, form_data)

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 401 when authentication fails.

Database and Seed Functions

File:gig_tracker/schema/base.py

FunctionPurpose
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
defget_session():
withSession(engine) assession:
yieldsession
defseed_db():
withSession(engine) assession:
venue_ids=create_venues(session)
create_user(session)
client_ids=create_clients(session)
create_gigs(session, venue_ids, client_ids)

Gig API Routes

File:gig_tracker/routes/gig_routes.py

RouteFunctionPurpose
POST /api/gigscreate_gigCreates a gig and returns 201
GET /api/gigsget_gigsReturns all gigs
PUT /api/gigs/{gig_id}update_gigUpdates an existing gig or returns 404
DELETE /api/gigs/{gig_id}delete_gigDeletes an existing gig or returns 404
@gig_router.post("/gigs", response_model=GigPublic, status_code=201)defcreate_gig(request: Request, gig: GigCreate, session: SessionDep):
db_gig=Gig.model_validate(gig)
session.add(db_gig)
session.commit()
session.refresh(db_gig)
returndb_gig

Client API Routes

File:gig_tracker/routes/client_routes.py

RouteFunctionPurpose
GET /api/clientsget_clientsReturns clients after bearer token validation
GET /api/clients/{client_id}get_clientReturns one client and can simulate intermittent server errors
POST /api/clientscreate_clientCreates a client after bearer token validation

The IntermitentErrorGenerator class intentionally simulates intermittent server behavior so tests can cover retry and resilience scenarios.


Venue API Routes

File:gig_tracker/routes/venue_routes.py

RouteFunctionPurpose
POST /api/venuescreate_venueCreates a venue and returns 201
GET /api/venuesget_venuesReturns all venues
GET /api/venues/{venue_id}get_venueReturns one venue or 404
PUT /api/venues/{venue_id}update_venueUpdates one venue or 404
DELETE /api/venues/{venue_id}delete_venueDeletes one venue or 404
@venue_router.get("/venues/{venue_id}", response_model=VenuePublic)defget_venue(venue_id: int, session: SessionDep):
venue=session.get(Venue, venue_id)
ifnotvenue:
raiseHTTPException(status_code=404, detail="Venue not found")
returnvenue

User API Routes

File:gig_tracker/routes/user_routes.py

RouteFunctionPurpose
GET /api/users/{user_id}get_userReturns one user after Basic authentication
GET /api/usersget_usersReturns the authenticated user
POST /api/userscreate_userCreates a new user with a hashed password

UI Routes

File:gig_tracker/routes/ui_routes.py

RouteFunctionPurpose
GET /indexRenders the home page
GET /gigsgigsShows upcoming gigs
GET /clientsget_clientsShows clients page
POST /clientscreate_clientCreates a client from form data
GET /venuesvenuesShows venues page
POST /venuescreate_venueCreates a venue from form data
POST /gigscreate_gigCreates a gig from form data
GET /load_venue_optionsget_venues_as_optionsReturns venue dropdown options snippet
GET /load_client_optionsget_clients_as_optionsReturns client dropdown options snippet
POST /searchsearchSearches gigs by name
GET /filter_gigsfilter_gigsFilters gigs by past/upcoming status
@ui_router.post("/search", include_in_schema=False)defsearch(request: Request, session: SessionDep, search: Annotated[str, Form()]):
search_results=session.exec(
select(Gig).where(col(Gig.name).contains(search))
).all()
returntemplates.TemplateResponse(
"search.html", {"request": request, "search_results": search_results}
)

Detailed Test Inventory

Current Test Status

No committed pytest test files were found in the indexed repository at the time this README was updated.

Test AreaCurrent StatusRecommended File
App startup/lifespan testNot yet implementedtests/test_app_startup.py
Token login success/failure testsNot yet implementedtests/test_auth.py
Password hashing testsNot yet implementedtests/test_security.py
Token verification testsNot yet implementedtests/test_security.py
Venue CRUD API testsNot yet implementedtests/test_venues_api.py
Gig CRUD API testsNot yet implementedtests/test_gigs_api.py
Client protected API testsNot yet implementedtests/test_clients_api.py
User Basic Auth testsNot yet implementedtests/test_users_api.py
UI route rendering testsNot yet implementedtests/test_ui_routes.py
Database seed testsNot yet implementedtests/test_database_seed.py
Rate limit behavior testsNot yet implementedtests/test_rate_limits.py

Recommended PyTest Test Suite

Test 1: Root Page Loads

Recommended file:gig_tracker/tests/test_ui_routes.py

fromfastapi.testclientimportTestClientfromgig_tracker.appimportappclient=TestClient(app)
deftest_root_page_loads():
response=client.get("/")
assertresponse.status_code==200assert"text/html"inresponse.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.


Test 2: Invalid Login Returns 401

Recommended file:gig_tracker/tests/test_auth.py

deftest_token_login_rejects_invalid_credentials():
response=client.post(
"/token",
data={"username": "invalid-user", "password": "invalid-value"},
)
assertresponse.status_code==401assertresponse.json()["detail"] =="Incorrect username or password"

Purpose: Validates negative authentication behavior.

Methodologies demonstrated: Negative testing, API security testing, response body assertion.


Test 3: Password Hashing Can Be Verified

Recommended file:gig_tracker/tests/test_security.py

fromgig_tracker.securityimporthash_password, verify_passworddeftest_hash_password_can_be_verified():
value="sample-test-value"hashed_value=hash_password(value)
asserthashed_value!=valueassertverify_password(value, hashed_value.decode("utf-8")) isTrue

Purpose: Confirms that hashing and verification work together.

Methodologies demonstrated: Unit testing, security helper testing, behavior validation.


Test 4: Access Token Can Be Created and Verified

Recommended file:gig_tracker/tests/test_security.py

fromgig_tracker.securityimportcreate_access_token, verify_tokendeftest_create_access_token_can_be_verified():
token=create_access_token({"sub": "sample-user"})
assertisinstance(token, str)
assertverify_token(token) isTrue

Purpose: Confirms token creation and verification behavior.

Methodologies demonstrated: Unit testing, token validation, security helper testing.


Test 5: Get Venues Returns a List

Recommended file:gig_tracker/tests/test_venues_api.py

deftest_get_venues_returns_list():
response=client.get("/api/venues")
assertresponse.status_code==200assertisinstance(response.json(), list)

Purpose: Confirms the venues API returns a list response.

Methodologies demonstrated: API smoke testing, response schema validation.


Test 6: Create Venue Returns 201

Recommended file:gig_tracker/tests/test_venues_api.py

deftest_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)
assertresponse.status_code==201body=response.json()
assertbody["name"] =="QA Test Venue"assert"id"inbody

Purpose: Validates venue creation.

Methodologies demonstrated: POST testing, create API validation, response field assertions.


Test 7: Missing Venue Returns 404

Recommended file:gig_tracker/tests/test_venues_api.py

deftest_get_missing_venue_returns_404():
response=client.get("/api/venues/999999")
assertresponse.status_code==404assertresponse.json()["detail"] =="Venue not found"

Purpose: Validates not-found behavior.

Methodologies demonstrated: Negative API testing, error response validation.


Test 8: Create Gig Returns 201

Recommended file:gig_tracker/tests/test_gigs_api.py

deftest_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)
assertresponse.status_code==201assertresponse.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.


Test 9: UI Gig Filter Returns HTML

Recommended file:gig_tracker/tests/test_ui_routes.py

deftest_filter_gigs_returns_html_snippet():
response=client.get("/filter_gigs?filter=past")
assertresponse.status_code==200assert"text/html"inresponse.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.


Test 10: Search Route Returns HTML

Recommended file:gig_tracker/tests/test_ui_routes.py

deftest_search_route_returns_html():
response=client.post("/search", data={"search": "test"})
assertresponse.status_code==200assert"text/html"inresponse.headers["content-type"]

Purpose: Validates the search form route.

Methodologies demonstrated: Form post testing, UI route validation, server-rendered response testing.


Code Methodology Summary

FastAPI Router Separation

The project separates behavior by resource type: gigs, clients, users, venues, and UI. This keeps responsibilities clear and improves maintainability.

SQLModel Schema Design

The project uses SQLModel to define database tables and request/response models.

Dependency Injection

FastAPI dependencies are used for database sessions and authentication.

SessionDep=Annotated[Session, Depends(get_session)]

Authentication Coverage

The app includes token-based authentication helpers and Basic authentication flow for selected routes.

Rate Limiting

SlowAPI middleware and decorators are used to rate-limit selected endpoints.

@limiter.limit("5/minute")

Seed Data

The seed functions create fake venues, users, clients, and gigs so the app can run with sample data.


Sample Code From the Project

FastAPI Application Setup

app=FastAPI(lifespan=lifespan)
app.state.limiter=limiterapp.add_exception_handler(429, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)

Router Registration

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)

Static File Mounting

app.mount("/static", StaticFiles(directory="./static"), name="static")

SQLModel Create Pattern

db_venue=Venue.model_validate(venue)
session.add(db_venue)
session.commit()
session.refresh(db_venue)
returndb_venue

Update Pattern

venue_data=venue.model_dump(exclude_unset=True)
db_venue.sqlmodel_update(venue_data)
session.add(db_venue)
session.commit()
session.refresh(db_venue)

More Example PyTest Code

Recommended conftest.py

importpytestfromfastapi.testclientimportTestClientfromgig_tracker.appimportapp@pytest.fixturedefclient():
withTestClient(app) astest_client:
yieldtest_client

Using the Fixture

deftest_get_venues_returns_list(client):
response=client.get("/api/venues")
assertresponse.status_code==200assertisinstance(response.json(), list)

Parametrized UI Status Test

importpytest@pytest.mark.parametrize("path, expected_status", [ ("/", 200), ("/gigs", 200), ("/clients", 200), ("/venues", 200), ],)deftest_ui_pages_load(client, path, expected_status):
response=client.get(path)
assertresponse.status_code==expected_status

Validation Error Test

deftest_create_venue_requires_name(client):
response=client.post(
"/api/venues",
json={"address": "123 Missing Name Street"},
)
assertresponse.status_code==422

Tutorial and Setup Guide

1. Clone the Repository

git clone https://github.com/BrianGator/Python-PyTest-Scripting.git
cd Python-PyTest-Scripting

2. Go to the App Folder

cd gig_tracker

3. Confirm Python Version

python --version

The project is configured for Python ^3.12.

4. Install Poetry

pip install poetry

5. Install Dependencies

poetry install

6. Run the FastAPI App

poetry run fastapi dev app.py

Alternative:

poetry run uvicorn gig_tracker.app:app --reload

7. Run PyTest

After tests are added under gig_tracker/tests, run:

poetry run pytest

8. Run a Single Test File

poetry run pytest tests/test_venues_api.py

9. Run a Single Test

poetry run pytest tests/test_venues_api.py::test_get_venues_returns_list

10. Run With Verbose Output

poetry run pytest -v

11. Run Ruff Linting

poetry run ruff check .

12. Run Mypy Type Checking

poetry run mypy .

PyTest Automation Tips

Use Fixtures for Setup

@pytest.fixturedefvenue_payload():
return {
"name": "QA Venue",
"address": "123 Test Street",
}

Keep Tests Independent

Each test should create or control the data it needs. Avoid depending on another test to run first.

Test Positive and Negative Paths

For each route, test success and failure cases.

Examples:

  • GET /api/venues returns 200.
  • GET /api/venues/999999 returns 404.
  • POST /api/venues with valid data returns 201.
  • POST /api/venues with invalid data returns 422.

Use Descriptive Test Names

Good:

deftest_get_missing_venue_returns_404():

Avoid:

deftest_1():

Assert Status Code and Body

assertresponse.status_code==200body=response.json()
assertisinstance(body, list)

Avoid Hardcoded Secrets

Use environment variables for secrets and tokens. Application secret keys should not be committed in source code for production systems.

Use Parametrization

@pytest.mark.parametrize("path", ["/", "/gigs", "/clients", "/venues"])deftest_pages_load(client, path):
assertclient.get(path).status_code==200

Separate Unit, API, and UI Tests

Recommended organization:

tests/unit/
tests/api/
tests/ui/

Recommended Improvements

1. Add Committed PyTest Tests

Add actual pytest files under:

gig_tracker/tests/

2. Add Test Database Isolation

Use a separate test database so test runs do not interfere with development data.

3. Move Secrets to Environment Variables

Move application secrets into environment variables or a .env file that is not committed.

4. Add CI Workflow

Recommended path:

.github/workflows/python-tests.yml

Example workflow:

name: Python PyTeston:
push:
branches: [main]pull_request:
branches: [main]jobs:
test:
runs-on: ubuntu-lateststeps:
- name: Checkout codeuses: actions/checkout@v4
- name: Set up Pythonuses: actions/setup-python@v5with:
python-version: '3.12'
- name: Install Poetryrun: pip install poetry
- name: Install dependenciesworking-directory: gig_trackerrun: poetry install
- name: Run testsworking-directory: gig_trackerrun: poetry run pytest -v

5. Add Coverage Reporting

poetry add --group dev pytest-cov
poetry run pytest --cov=gig_tracker --cov-report=term-missing

6. Add API Contract Tests

Validate response schemas and required fields for each API endpoint.

7. Add Rate Limit Tests

The app uses SlowAPI rate limiting. Add tests to verify configured limits return 429 when exceeded.

8. Add Browser UI Tests Later

For full end-to-end browser testing, add Playwright or Selenium-based UI tests after the API and route tests are stable.


Author

Written by Brian McCarthy

Project repository:

https://github.com/BrianGator/Python-PyTest-Scripting