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:
Testing packages included:
pytest- Testing frameworkpytest-flask- Flask-specific fixtures and utilitiespytest-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 attributestest_user_club_membership_relationship- Bidirectional relationshipstest_application_status_constraint- Status field validationtest_follow_relationship- User following clubstest_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 accesstest_browse_redirects_when_not_logged_in- Auth protectiontest_browse_clubs_authenticated- Authenticated browsingtest_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 verificationtest_signup_duplicate_email- Constraint enforcementtest_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 permissionstest_admin_can_edit_any_post- Content moderationtest_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 onboardingtest_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:
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¶
Specific Test File¶
Specific Test Function¶
Test Class¶
With Coverage¶
Coverage with HTML Report¶
View report: open htmlcov/index.html
Verbose Output¶
Show Print Statements¶
Stop at First Failure¶
Run Last Failed Tests¶
Run Tests Matching Pattern¶
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¶
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¶
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