본문으로 건너뛰기
김신건의 로그

[Flask] Testing

· 수정 · 📖 약 1분 · 517자/단어 #python #flask #testing #pytest
Flask Testing, Flask test_client, Flask pytest, test_request_context, Flask 테스트

정의

Flask Testing 은 앱 인스턴스를 직접 감싸 HTTP 요청을 시뮬레이션하는 workflow 입니다. app.test_client() (Werkzeug) 로 라우팅/뷰/미들웨어까지 실제 실행하고 응답을 검증합니다. HTTP 서버 없이 프로세스 내부에서 수행되어 매우 빠릅니다.

기본 사용

def test_index():
    app = create_app("testing")
    client = app.test_client()

    resp = client.get("/")
    assert resp.status_code == 200
    assert resp.get_json() == {"hello": "world"}

pytest 통합

# conftest.py
import pytest
from myapp import create_app
from myapp.extensions import db

@pytest.fixture
def app():
    app = create_app("testing")
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

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

@pytest.fixture
def runner(app):
    return app.test_cli_runner()
# tests/test_users.py
def test_create_user(client):
    resp = client.post("/users/", json={"email": "a@b.com", "password": "secret123"})
    assert resp.status_code == 201
    data = resp.get_json()
    assert data["email"] == "a@b.com"

def test_get_user(client):
    resp = client.get("/users/1")
    assert resp.status_code == 200

HTTP 요청 API

# GET with query
client.get("/items/", query_string={"q": "search"})

# POST JSON
client.post("/items/", json={"name": "foo"})

# POST form
client.post("/login", data={"email": "a@b.com", "password": "secret"})

# POST multipart (파일)
from io import BytesIO

client.post(
    "/upload",
    data={"file": (BytesIO(b"file content"), "test.txt")},
    content_type="multipart/form-data",
)

# Headers
client.get("/protected", headers={"Authorization": "Bearer token"})

# Cookies
client.set_cookie("localhost", "session", "abc")
resp = client.get("/me")

Response 검증

def test_response(client):
    resp = client.get("/api")

    assert resp.status_code == 200
    assert resp.mimetype == "application/json"

    # JSON body
    data = resp.get_json()
    assert data["key"] == "value"

    # Raw body
    assert b"success" in resp.data
    text = resp.get_data(as_text=True)

    # Headers
    assert resp.headers["X-Custom"] == "value"

    # Cookies
    assert "session" in resp.headers.get("Set-Cookie", "")

Session 조작

def test_session(client):
    with client.session_transaction() as sess:
        sess["user_id"] = 1

    resp = client.get("/me")
    assert resp.status_code == 200

session_transaction context 안에서 세션을 조작하고, 이후 요청에 세션이 반영됨.

test_request_context

Request context 를 수동으로 열기 (뷰 함수 밖에서 request, url_for 등 접근):

def test_url_for(app):
    with app.test_request_context("/hello?q=world"):
        assert request.path == "/hello"
        assert request.args["q"] == "world"
        assert url_for("index") == "/"

test_cli_runner (CLI 명령 테스트)

def test_seed_cli(runner):
    result = runner.invoke(args=["seed-db", "--count", "10"])
    assert result.exit_code == 0
    assert "Seeded 10" in result.output

DB 격리 패턴

패턴 1: fresh schema per test

@pytest.fixture
def app():
    app = create_app("testing")
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

빠르지만 테스트 간 오염 위험 (fixture 로 매번 clear 필요).

패턴 2: Transaction rollback per test

@pytest.fixture
def app():
    app = create_app("testing")
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def db_session(app):
    connection = db.engine.connect()
    transaction = connection.begin()
    session = db.session
    session.bind = connection

    yield session

    transaction.rollback()
    connection.close()

각 테스트 후 rollback -> 완벽 격리.

패턴 3: SQLite in-memory

class TestingConfig(Config):
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"

가장 빠름. 하지만 PostgreSQL-specific 기능 (JSONB, LATERAL, …) 은 테스트 불가.

패턴 4: testcontainers

from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def postgres():
    with PostgresContainer("postgres:16") as p:
        yield p.get_connection_url()

@pytest.fixture(scope="session")
def app(postgres):
    os.environ["DATABASE_URL"] = postgres
    app = create_app("testing")
    ...

프로덕션 DB 와 동일한 환경. Docker 필요.

Authentication 테스트

Flask-Login 사용 시:

@pytest.fixture
def logged_in_user(client, app):
    with app.app_context():
        user = User(email="test@example.com", password_hash=hash("secret"))
        db.session.add(user)
        db.session.commit()
        user_id = user.id

    with client.session_transaction() as sess:
        sess["_user_id"] = str(user_id)
        sess["_fresh"] = True

    return user_id

def test_dashboard(client, logged_in_user):
    resp = client.get("/dashboard")
    assert resp.status_code == 200

Mocking

unittest.mock 활용:

from unittest.mock import patch

def test_external_call(client):
    with patch("myapp.services.external_api.post") as mock_post:
        mock_post.return_value.json.return_value = {"status": "ok"}
        resp = client.post("/webhook", json={"event": "..."})
        assert resp.status_code == 200
        mock_post.assert_called_once()

Flask-Caching 무효화

class TestingConfig(Config):
    CACHE_TYPE = "NullCache"    # 캐시 안 함

또는 fixture 로 clear:

@pytest.fixture(autouse=True)
def clear_cache(app):
    from myapp.extensions import cache
    cache.clear()

Async view 테스트 (Flask 2.0+)

async def test_async_view(client):
    resp = await client.get("/async")   # httpx-like
    assert resp.status_code == 200

Flask 는 async view 지원이 제한적. pytest-asyncio 조합.

Coverage

pip install pytest-cov
pytest --cov=myapp --cov-report=term-missing --cov-report=html

pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.coverage.run]
branch = true
source = ["myapp"]
omit = ["*/migrations/*", "wsgi.py"]

[tool.coverage.report]
fail_under = 80
show_missing = true

E2E 테스트 (실제 서버)

Selenium/Playwright 로 브라우저 자동화:

import pytest
from playwright.sync_api import Page
from myapp import create_app
from threading import Thread

@pytest.fixture(scope="session")
def live_server():
    app = create_app("testing")
    server = make_server("127.0.0.1", 5000, app)
    thread = Thread(target=server.serve_forever)
    thread.start()
    yield "http://127.0.0.1:5000"
    server.shutdown()
    thread.join()

def test_home_page(page: Page, live_server):
    page.goto(f"{live_server}/")
    assert page.title() == "My App"

함정

WARNING

App context / request context 혼동. test_request_context 는 request 시뮬레이션, app_context 는 앱 활성화. DB 조작은 앱 컨텍스트 필요.

CAUTION

db.session.remove() 안 하면 다음 테스트로 세션 leak. teardown fixture 에서 반드시.

WARNING

CSRF 비활성화. 테스트 시 WTF_CSRF_ENABLED = False. 활성이면 매 폼 테스트마다 토큰 발급 필요.

IMPORTANT

Fixture scope. DB migrations 는 session scope, transaction rollback 은 function scope. 잘못 조합하면 매우 느림.

CAUTION

환경 변수 leak. 테스트에서 os.environ 을 조작하면 다른 테스트 영향. monkeypatch.setenv 사용.

관련 위키

이 글의 용어 (7개)
[FastAPI] Testingfastapi
정의 FastAPI Testing 은 endpoint 를 실제 서버 없이 직접 호출해 응답을 검증하는 workflow 입니다. Starlette 의 (내부적으로 sync) 또는 …
[Flask] Application Factory Patternflask
정의 Application Factory 는 Flask 앱 인스턴스를 함수 안에서 생성 하는 패턴입니다. 모듈 스코프에 를 두는 대신 함수를 정의해 반환합니다. 이 패턴은 사실상…
[Flask] Blueprintsflask
정의 Blueprint 는 Flask 앱을 여러 모듈로 나누는 표준 도구입니다. 라우트, 정적파일, 템플릿, before/after hook, error handler 를 그룹화…
[Flask] Extensions (SQLAlchemy, Login, Migrate, WTF, ...)flask
정의 Flask Extension 은 코어에 없는 기능 (DB, 인증, 마이그레이션, 캐시 등) 을 표준 인터페이스로 통합하는 서드파티 패키지입니다. 패턴이 관용이며, appli…
[Flask] Sessions & Cookiesflask
정의 Flask Session 은 요청 간 상태를 유지하는 방법입니다. 기본 구현은 서명된 쿠키 (secure cookie) 로 클라이언트에 저장 (server-less). ex…
[Python] Flaskflask
정의 Flask 는 Armin Ronacher 가 2010년 발표한 WSGI 기반 Python 마이크로프레임워크 입니다. Pallets Projects 가 유지관리하고, 2026…
[Python] pytest: fixture, parametrize, mockpython
정의 는 Python 표준 테스트 프레임워크의 사실상 표준(stdlib는 이지만 pytest가 더 강력). 단순한 함수 로 시작해서 fixture, parametrize, plu…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기