Skip to content

Testing

Overview

The School Clubs Management System uses pytest for automated testing. Tests are located in deliverable-2/tests/ and cover models, routes, authentication, authorization, admin functionality, and end-to-end workflows.

Prerequisites

Install testing dependencies:

pip install -r deliverable-2/requirements.txt

Testing packages included:

  • pytest - Testing framework
  • pytest-flask - Flask-specific fixtures and utilities
  • pytest-cov - Code coverage reporting

Test Structure

Test Files

test_models.py

Tests database models, relationships, and constraints:

  • User, Club, Membership, Application, Post models
  • Relationship integrity (one-to-many, many-to-many)
  • Database constraints and validation
  • Model methods and properties
  • Follow relationships between users and clubs

Example tests:

  • test_create_user - User creation and basic attributes
  • test_user_club_membership_relationship - Bidirectional relationships
  • test_application_status_constraint - Status field validation
  • test_follow_relationship - User following clubs
  • test_event_post_fields - Event-specific post attributes

test_routes.py

Tests HTTP endpoints and routing logic:

  • Public vs. authenticated routes
  • Login-required decorators
  • Club browsing and filtering
  • Follow/unfollow functionality
  • Post creation and management
  • Application submission

Example tests:

  • test_landing_page_accessible_without_login - Public access
  • test_browse_redirects_when_not_logged_in - Auth protection
  • test_browse_clubs_authenticated - Authenticated browsing
  • test_follow_and_unfollow_club_flow - Complete follow workflow

test_authentication.py

Comprehensive authentication and authorization tests organized in classes:

TestUserRegistration:

  • New user creation via signup
  • Password hashing verification
  • Duplicate email rejection
  • Missing field validation
  • Email format validation
  • Default user type assignment

TestUserLogin:

  • Correct credential authentication
  • Invalid password handling
  • Non-existent user handling
  • Session management

Example tests:

  • test_signup_hashes_password - Security verification
  • test_signup_duplicate_email - Constraint enforcement
  • test_login_with_correct_credentials - Successful authentication

test_admin.py

Tests admin-specific permissions and functionality:

TestAdminPermissions:

  • Posting to any club
  • Editing any user's posts
  • Deleting posts
  • Verifying/unverifying clubs
  • Managing user accounts

TestClubVerification:

  • Admin verification workflow
  • Verification status changes
  • Non-admin restriction

Example tests:

  • test_admin_can_post_to_any_club - Elevated permissions
  • test_admin_can_edit_any_post - Content moderation
  • test_student_cannot_verify_club - Permission boundaries

test_integration.py

Multi-component integration tests for complex workflows:

TestCompleteUserJourney:

  • Signup → Login → Browse → Follow → Apply → Accept → Post
  • Data consistency across operations
  • State transitions

TestClubManagement:

  • Club creation by presidents
  • Member management
  • Post moderation
  • Event creation and scheduling

Example tests:

  • test_new_student_complete_journey - Full user lifecycle
  • Multi-step workflows with realistic data

test_e2e.py

End-to-end user scenarios:

  • Complete signup and login flows
  • Club membership application and acceptance
  • President approval workflows
  • Student browsing and interaction

Example tests:

  • test_signup_login_and_browse_flow - New user onboarding
  • test_full_join_and_accept_flow - Complete membership cycle

Fixtures (conftest.py)

Common test fixtures defined in conftest.py:

app

Creates a fresh Flask application instance for each test:

@pytest.fixture
def app():
    """New Flask app for each test"""
    db_filedscr, db_path = tempfile.mkstemp()

    test_config = {
        "TESTING": True,
        "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}",
        "WTF_CSRF_ENABLED": False,
    }
    app = create_app(test_config)
    with app.app_context(): 
        db.create_all()
        yield app 
        db.session.remove() 
        db.drop_all()
    os.close(db_filedscr)
    os.unlink(db_path)
  • Uses temporary SQLite database
  • Creates tables before test
  • Cleans up after test
  • Disables CSRF for testing

client

Test client for making HTTP requests:

@pytest.fixture
def client(app):
    return app.test_client()

Enables testing routes without running a server.

test_user

Pre-created test user with known credentials:

@pytest.fixture 
def test_user(app):
    user = User(
        first_name="Test",
        last_name="User",
        email="test_user@student.lu",
        password_hash=generate_password_hash("password123"),
        user_type="student",
    )
    db.session.add(user)
    db.session.commit()
    return user

Running Tests

All Tests

pytest deliverable-2/tests/

Specific Test File

pytest deliverable-2/tests/test_models.py

Specific Test Function

pytest deliverable-2/tests/test_models.py::test_create_user

Test Class

pytest deliverable-2/tests/test_authentication.py::TestUserRegistration

With Coverage

pytest --cov=school_clubs deliverable-2/tests/

Coverage with HTML Report

pytest --cov=school_clubs --cov-report=html deliverable-2/tests/

View report: open htmlcov/index.html

Verbose Output

pytest -v deliverable-2/tests/

Show Print Statements

pytest -s deliverable-2/tests/

Stop at First Failure

pytest -x deliverable-2/tests/

Run Last Failed Tests

pytest --lf deliverable-2/tests/

Run Tests Matching Pattern

pytest -k "authentication" deliverable-2/tests/

Test Coverage Areas

Tests comprehensively cover:

Authentication & Authorization

  • ✅ User registration and signup
  • ✅ Password hashing and security
  • ✅ Login/logout functionality
  • ✅ Session management
  • ✅ Role-based permissions (student, president, admin)
  • ✅ Login-required route protection

Database Models

  • ✅ User model and attributes
  • ✅ Club model and verification
  • ✅ Membership relationships
  • ✅ Application status workflow
  • ✅ Post and event models
  • ✅ Follow relationships
  • ✅ Database constraints and validation

Club Management

  • ✅ Club creation and editing
  • ✅ Club browsing and filtering
  • ✅ Club verification by admins
  • ✅ President assignment
  • ✅ Member management

Membership Workflow

  • ✅ Application submission
  • ✅ Application approval/rejection
  • ✅ President application review
  • ✅ Membership status tracking
  • ✅ Following clubs

Posts & Events

  • ✅ Post creation (announcements, events)
  • ✅ Event date and location
  • ✅ Post editing and deletion
  • ✅ Pinned posts
  • ✅ Post visibility and permissions

Admin Functions

  • ✅ Admin panel access control
  • ✅ Content moderation
  • ✅ User management
  • ✅ Club verification
  • ✅ System-wide permissions

Writing Tests

Basic Test Structure

def test_example(client, app):
    # 1. Setup - Create test data
    with app.app_context():
        user = User(email="test@student.lu", ...)
        db.session.add(user)
        db.session.commit()

    # 2. Execute - Perform action
    response = client.get("/some-route")

    # 3. Assert - Verify results
    assert response.status_code == 200
    assert b"expected text" in response.data

Testing Database Models

def test_user_creation(app):
    """Test creating a user and verifying attributes"""
    with app.app_context():
        user = User(
            first_name="Alice",
            last_name="Tester",
            email="alice@student.lu",
            password_hash="hashed_password",
            user_type="student",
        )
        db.session.add(user)
        db.session.commit()

        # Verify
        assert user.user_id is not None
        assert user.email == "alice@student.lu"
        assert user.user_type == "student"

Testing Relationships

def test_membership_relationship(app):
    """Test bidirectional relationships"""
    with app.app_context():
        user = User(...)
        club = Club(...)
        db.session.add_all([user, club])
        db.session.commit()

        membership = Membership(
            student_id=user.user_id,
            club_id=club.club_id
        )
        db.session.add(membership)
        db.session.commit()

        # Test both directions
        assert user.memberships[0].club is club
        assert club.memberships[0].student is user

Testing Routes with Authentication

def test_protected_route(client, app):
    """Test route requires authentication"""
    # Create user
    with app.app_context():
        user = User(
            email="auth@student.lu",
            password_hash=generate_password_hash("pass123"),
            ...
        )
        db.session.add(user)
        db.session.commit()

    # Login
    response = client.post(
        "/login_modal",
        data={"email": "auth@student.lu", "password": "pass123"},
        follow_redirects=True
    )
    assert response.status_code == 200

    # Access protected route
    response = client.get("/browse")
    assert response.status_code == 200
    assert b"clubs" in response.data.lower()

Helper Functions

Create reusable helper functions for common operations:

def login_helper(client, app, email, password):
    """Helper to create and login a user"""
    with app.app_context():
        user = User(
            first_name="Test",
            last_name="User",
            email=email,
            password_hash=generate_password_hash(password),
            user_type="student",
        )
        db.session.add(user)
        db.session.commit()
        user_id = user.user_id

    response = client.post(
        "/login_modal",
        data={"email": email, "password": password},
        follow_redirects=True,
    )
    assert response.status_code == 200
    return user_id

Testing Form Submissions

def test_club_application(client, app):
    """Test applying to a club"""
    user_id = login_helper(client, app, "user@student.lu", "pass")

    with app.app_context():
        club = Club(club_name="Test Club", ...)
        db.session.add(club)
        db.session.commit()
        club_id = club.club_id

    # Submit application
    response = client.post(
        f"/club/{club_id}/apply",
        follow_redirects=True
    )
    assert response.status_code == 200

    # Verify application created
    with app.app_context():
        app = Application.query.filter_by(
            student_id=user_id,
            club_id=club_id
        ).first()
        assert app is not None
        assert app.status == "pending"

Testing Error Conditions

def test_duplicate_email_rejected(client, app):
    """Test that duplicate emails are rejected"""
    email = "duplicate@student.lu"

    # First signup succeeds
    response = client.post("/sign_up", data={
        "first_name": "First",
        "last_name": "User",
        "email": email,
        "password": "pass123"
    })

    # Second signup fails
    response = client.post("/sign_up", data={
        "first_name": "Second",
        "last_name": "User",
        "email": email,
        "password": "pass456"
    })

    # Verify only one user exists
    with app.app_context():
        users = User.query.filter_by(email=email).all()
        assert len(users) == 1

Testing Database Constraints

def test_invalid_application_status(app):
    """Test that invalid status values are rejected"""
    with app.app_context():
        user = User(...)
        club = Club(...)
        db.session.add_all([user, club])
        db.session.commit()

        # Try invalid status
        app = Application(
            student_id=user.user_id,
            club_id=club.club_id,
            status="invalid_status"
        )
        db.session.add(app)

        # Should raise IntegrityError
        with pytest.raises(IntegrityError):
            db.session.commit()

        db.session.rollback()

Best Practices

Test Organization

  • Group related tests in classes
  • Use descriptive test names that explain what is being tested
  • Keep tests focused on a single concept
  • Order tests from simple to complex

Test Independence

  • Each test should be independent
  • Don't rely on test execution order
  • Use fixtures for setup, not other tests
  • Clean up after each test (handled by fixtures)

Fixtures

  • Use fixtures for common setup
  • Keep fixtures focused and single-purpose
  • Document fixture behavior
  • Use fixture scope appropriately (function, class, module, session)

Assertions

  • Use clear, specific assertions
  • Assert the most important condition first
  • Include helpful assertion messages
  • Test both positive and negative cases

Database Testing

  • Always use app.app_context() when accessing db
  • Commit changes explicitly with db.session.commit()
  • Test constraints raise expected errors
  • Verify relationships in both directions

Authentication Testing

  • Use helper functions for login
  • Test both authenticated and unauthenticated access
  • Verify session state
  • Test role-based permissions

Coverage Goals

  • Aim for high coverage (>80%)
  • Focus on critical paths first
  • Test edge cases and error conditions
  • Don't just chase 100% - test meaningfully

Test Data

  • Use realistic but minimal test data
  • Use factories or builders for complex objects
  • Keep test data maintainable
  • Avoid hard-coded IDs

Naming Conventions

  • Test files: test_*.py
  • Test functions: test_*
  • Test classes: Test*
  • Use descriptive names: test_user_cannot_delete_other_users_posts

Common Patterns

Testing Redirects

def test_login_redirects_to_home(client, app):
    # Don't follow redirects
    response = client.post("/login_modal", data={...})
    assert response.status_code == 302
    assert response.location == "/home"

Testing Flash Messages

def test_error_message_shown(client, app):
    response = client.post("/login_modal", data={
        "email": "wrong@email.com",
        "password": "wrong"
    }, follow_redirects=True)
    assert b"Invalid credentials" in response.data

Testing Permissions

def test_student_cannot_access_admin_panel(client, app):
    login_helper(client, app, "student@student.lu", "pass")
    response = client.get("/admin/")
    assert response.status_code in (302, 403, 401)

Testing Follow Relationships

def test_follow_unfollow_cycle(client, app):
    user_id = login_helper(client, app, "user@student.lu", "pass")

    with app.app_context():
        club = Club(...)
        db.session.add(club)
        db.session.commit()
        club_id = club.club_id

    # Follow
    client.post(f"/clubs/{club_id}/follow")
    with app.app_context():
        user = User.query.get(user_id)
        club = Club.query.get(club_id)
        assert club in user.followed_clubs

    # Unfollow
    client.post(f"/clubs/{club_id}/unfollow")
    with app.app_context():
        user = User.query.get(user_id)
        club = Club.query.get(club_id)
        assert club not in user.followed_clubs

Debugging Tests

Run with Print Output

pytest -s deliverable-2/tests/test_models.py

Use pytest.set_trace() for Debugging

def test_complex_workflow(client, app):
    # ... setup ...
    pytest.set_trace()  # Debugger breakpoint
    response = client.get("/route")

Check Test Database State

def test_example(app):
    with app.app_context():
        # Print database contents
        users = User.query.all()
        print(f"Users in DB: {[u.email for u in users]}")

Verbose Failure Output

pytest -vv deliverable-2/tests/

Continuous Integration

Tests should be run automatically in CI/CD pipelines:

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
      - run: pip install -r deliverable-2/requirements.txt
      - run: pytest deliverable-2/tests/ --cov=school_clubs

Troubleshooting

Tests Fail with "no app context"

Always wrap database operations in with app.app_context():

Tests Pollute Each Other

Check that fixtures properly clean up. Verify temp database is deleted.

Import Errors

Ensure sys.path includes project root in conftest.py

CSRF Errors

Disable CSRF in test config: "WTF_CSRF_ENABLED": False

Session Issues

Use follow_redirects=True when testing login flows