Skip to content

BrianGator/Python-PyTest-Scripting

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Python PyTest Scripting

Python PyTest FastAPI SQLModel Portfolio

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 / 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

Tools and Frameworks

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

Project File Links

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

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

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 / Model Layer

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 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.
@asynccontextmanager
async def lifespan(app: FastAPI):
    create_db_and_tables()
    seed_db()
    yield
    drop_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

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)

Gig API Routes

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

Client API Routes

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.


Venue API Routes

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

User API Routes

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

UI Routes

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}
    )

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 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 PyTest Test Suite

Test 1: Root Page Loads

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.


Test 2: Invalid Login Returns 401

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.


Test 3: Password Hashing Can Be Verified

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.


Test 4: Access Token Can Be Created and Verified

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.


Test 5: Get Venues Returns a List

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.


Test 6: Create Venue Returns 201

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 body

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

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.


Test 8: Create Gig Returns 201

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.


Test 9: UI Gig Filter Returns HTML

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.


Test 10: Search Route Returns HTML

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.


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 = limiter
app.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)
return db_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

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

Using the Fixture

def test_get_venues_returns_list(client):
    response = client.get("/api/venues")

    assert response.status_code == 200
    assert isinstance(response.json(), list)

Parametrized UI Status Test

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

Validation Error Test

def test_create_venue_requires_name(client):
    response = client.post(
        "/api/venues",
        json={"address": "123 Missing Name Street"},
    )

    assert response.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.fixture
def venue_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:

def test_get_missing_venue_returns_404():

Avoid:

def test_1():

Assert Status Code and Body

assert response.status_code == 200
body = response.json()
assert isinstance(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"])
def test_pages_load(client, path):
    assert client.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 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

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

About

Python-PyTest-Scripting

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors