Testing, Security, and Deployment
Ensure quality, secure your app, containerize, automate, and deploy to cloud platforms with confidence.
Content
Integration tests with Flask
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Integration Tests with Flask — CS50 Practical Guide
"Unit tests check the bones. Integration tests make sure the skeleton can dance without collapsing." — your slightly stressed but proud TA
You've already written unit tests and learned how to test React components. You also learned how React talks to Flask APIs. Now we climb one rung higher: integration tests for your Flask app — the tests that verify multiple pieces (routing, DB, auth, and input/output) work together. These tests are the safety net before the frontend and backend handshake turns into an awkward meetup.
Why integration tests matter (and where unit tests stop)
- Unit tests: isolate functions, models, small pieces (we covered these earlier under Unit tests and coverage).
- Integration tests: exercise multiple layers together — HTTP endpoints, database, authentication, input validation, and sometimes external APIs.
Real-life example: your React component calls POST /api/todos. A unit test can't catch that your endpoint accidentally requires a missing header or that a database relationship is misconfigured. Integration tests catch these.
The typical setup pattern (app factory, test DB, test client)
If you followed CS50 patterns, your Flask app uses an app factory and Flask-SQLAlchemy. Integration tests leverage that.
Minimal app factory
# myapp/__init__.py
from flask import Flask
from .models import db
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY='dev',
SQLALCHEMY_DATABASE_URI='sqlite:///prod.db',
)
if test_config:
app.config.update(test_config)
db.init_app(app)
# register blueprints, etc.
return app
Test configuration
- Use an in-memory SQLite DB (sqlite:///:memory:) or a separate test DB.
- Turn on TESTING and disable CSRF for form posts (or handle CSRF tokens explicitly).
Pytest fixtures for integration tests (recommended)
Create fixtures that:
- Create an app with test config
- Set up DB schema and tear it down
- Provide a test client
# tests/conftest.py
import pytest
from myapp import create_app
from myapp.models import db as _db
@pytest.fixture(scope='function')
def app():
app = create_app({
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
'WTF_CSRF_ENABLED': False,
})
with app.app_context():
_db.create_all()
yield app
_db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
Table: Common fixtures
| Fixture | Purpose |
|---|---|
| app | creates app + fresh DB |
| client | Flask test client for HTTP calls |
| runner | run CLI commands (migrations, seeds) |
Example: testing a protected API route (auth + DB)
Imagine an endpoint POST /api/todo that requires login and saves a row.
# tests/test_todo_api.py
from myapp.models import User, Todo, db
def create_user(username='tester', password='pwd'):
u = User(username=username)
u.set_password(password)
db.session.add(u)
db.session.commit()
return u
def login(client, username, password):
return client.post('/login', data={'username': username, 'password': password}, follow_redirects=True)
def test_create_todo_requires_login(client, app):
resp = client.post('/api/todo', json={'title': 'Buy milk'})
assert resp.status_code == 401 # unauthorized
def test_create_todo_happy_path(client, app):
with app.app_context():
create_user()
login(client, 'tester', 'pwd')
resp = client.post('/api/todo', json={'title': 'Buy milk'})
assert resp.status_code == 201
data = resp.get_json()
assert data['title'] == 'Buy milk'
# assert DB row exists
with app.app_context():
todo = Todo.query.filter_by(title='Buy milk').first()
assert todo is not None
Notes:
- Use follow_redirects for session-based login flows so the cookie is set on the client.
- For JSON endpoints, use client.post(..., json=payload) — Flask test client sets content-type for you.
Speed tips and DB transactions
- For many tests, recreate the DB per test is safe but slow.
- Advanced: use a single DB for the session and wrap each test in a rollback/savepoint (db.session.begin_nested + teardown) for speed — helpful for larger suites.
Testing integrations with front-end (React) in the loop
You already connect React to Flask APIs. Integration tests can remain backend-focused, since React component tests already cover UI behavior. But: for end-to-end confidence, add a small e2e test (Playwright / Selenium) that loads your built React app and exercises a flow: signup, create item, view it in UI.
Quick strategy:
- Backend integration tests: verify API contracts (status codes, JSON shape).
- Frontend unit/component tests: ensure UI reacts to API outputs.
- Optional E2E: smoke test the whole stack on CI after deploy to staging.
Mocking external services
If your Flask app calls external APIs (Stripe, an OAuth provider), mock those calls in integration tests using:
- monkeypatch to replace the function that performs the HTTP call
- responses or requests-mock to fake requests
Example with monkeypatch:
def fake_fetch_profile(token):
return {'id': '123', 'email': 'bot@example.com'}
def test_oauth_login(monkeypatch, client):
monkeypatch.setattr('myapp.auth.fetch_profile', fake_fetch_profile)
resp = client.get('/oauth/callback?code=abc', follow_redirects=True)
assert resp.status_code == 200
Run tests in CI and gate deployments
Always run tests in CI (GitHub Actions, GitLab CI) and fail the pipeline if tests fail. Example job snippet for GitHub Actions:
name: Python tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- run: pytest --maxfail=1 -q
Tie this check to your deployment pipeline so broken integrations never reach production.
Quick checklist: what to test in integration tests
- Routes return correct status codes and JSON / HTML
- DB writes/reads persist expected state
- Authentication and authorization gates work
- Form and JSON validation errors handled correctly
- External APIs are mocked and error paths are tested
- CSRF behavior (enabled or intentionally disabled) is considered
Key takeaways
- Integration tests verify pieces play well together. They catch issues unit tests miss: mismatched expectations between API and DB, auth problems, and incorrect status codes.
- Use the app factory + test config + fixtures pattern to keep tests isolated and reproducible.
- Mock external services; test the happy path and error paths.
- Run tests in CI and gate deployments — your future self will thank you (and your users).
"A test suite is like an immune system for your code base: it doesn’t stop bugs from existing, but it helps you detect them before they get toxic." — wise, slightly caffeinated TA
Happy testing. Go write a test that fails first — then make it glorious.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!