Menguji aplikasi Flask dengan pytest: Flask test client untuk simulasi request, fixtures untuk setup database dan auth, penutup coverage, serta pola pengujian view dan REST API yang andal dan cepat.

Semua fitur yang kita bangun dari episode 3 sampai 20 — routing, database, auth, upload — akan roboh tanpa jaring pengaman. Episode 21 membangun jaring itu: pytest bersama Flask test client. Testing bukan sekadar "membuktikan kode jalan", melainkan memberikan kepercayaan untuk mengubah kode — saat refactor, upgrade library, atau menambah fitur, test memberi tahu secepat mungkin apa yang rusak. Kita akan membangun test untuk view dan REST API, dengan fixtures yang rapi dan coverage yang terukur.
Tanpa test, perubahan kecil (menambah kolom, mengubah format response) bisa merusak fitur yang tidak terduga — dan baru ketahuan di produksi. Test otomatis mengubah ini: setiap perubahan dijalankan terhadap perilaku yang sudah dikunci, dan kegagalan muncul dalam hitungan detik, bukan setelah user melapor. Tiga istilah dasar yang akan dipakai: unit test (satu komponen kecil), integration test (interaksi view → DB → response), dan test client (klien HTTP palsu bawaan Flask yang memanggil aplikasi tanpa server sungguhan).
Install dengan pip install pytest pytest-cov, lalu pola fixture inti — satu fixture menyediakan aplikasi + database:
import pytest
from app import create_app
from app.config import TestingConfig
from app.extensions import db
@pytest.fixture()
def app():
app = create_app(TestingConfig)
with app.app_context():
db.create_all()
yield app
with app.app_context():
db.drop_all()
@pytest.fixture()
def client(app):
return app.test_client()TestingConfig memakai database in-memory (episode 8) sehingga setiap test dimulai dari database bersih — create_all() di awal, drop_all() di akhir. client membungkus app sehingga test bisa melakukan request.
def test_home_returns_200(client):
resp = client.get("/")
assert resp.status_code == 200
assert b"Selamat datang" in resp.data
def test_unknown_page_returns_404(client):
resp = client.get("/tidak-ada")
assert resp.status_code == 404client.get("/") menjalankan seluruh pipeline — routing, view, template — tanpa server. resp.status_code memeriksa kode status; resp.data berisi body response. Dua test ini sudah melindungi fitur dasar.
Untuk API, periksa status, bentuk JSON, dan validasi input:
def test_create_user(client):
resp = client.post("/api/users", json={"username": "budi", "email": "budi@x.id"})
assert resp.status_code == 201
data = resp.get_json()
assert data["data"]["username"] == "budi"
assert "id" in data["data"]
def test_create_user_invalid(client):
resp = client.post("/api/users", json={})
assert resp.status_code == 400
def test_get_user_not_found(client):
resp = client.get("/api/users/999")
assert resp.status_code == 404client.post(..., json=...) secara otomatis mengirim body JSON dengan header Content-Type: application/json. resp.get_json() mem-parsing response JSON. Tiga test ini menutup kasus sukses, validasi gagal, dan not found.
Untuk endpoint terproteksi (@login_required), buat fixture yang membuat user dan login:
from app.models.user import User
@pytest.fixture()
def auth_headers(app, client):
with app.app_context():
user = User(username="admin", email="admin@x.id")
user.set_password("password123")
db.session.add(user)
db.session.commit()
user_id = user.id
# session-based login via test client
client.post("/auth/login", data={
"username": "admin",
"password": "password123",
})
return {"user_id": user_id}Untuk JWT (episode 18), test bisa langsung memanggil endpoint /auth/login dan mengambil token dari response. Fixture ini dipakai sebagai parameter test:
def test_dashboard_requires_login(client):
resp = client.get("/dashboard")
assert resp.status_code == 302 # redirect ke login
def test_dashboard_after_login(client, auth_headers):
resp = client.get("/dashboard")
assert resp.status_code == 200Test pertama mengunci perilaku keamanan: tanpa login, halaman terlindungi. Test kedua memastikan setelah login, akses diberikan.
Note
Pastikan mode TESTING=True diaktifkan (sudah di TestingConfig). Tanpa itu, exception di view akan "menetes" sebagai error Python (test gagal dengan traceback), bukan sebagai response 500 — kedua perilaku bisa berguna, tetapi konsistensi lebih penting. Dengan TESTING=True, error juga tidak memicu rate limit atau CSRF yang menyulitkan test.
Coverage mengukur persentase baris kode yang dijalankan test:
pytest --cov=app --cov-report=term-missingOutputnya menampilkan persentase per modul dan baris yang belum teruji. Pedoman:
except branch) sering menyembunyikan bug — test mereka.def test_internal_error(client, monkeypatch):
def boom():
raise RuntimeError("ledakan")
monkeypatch.setattr("app.routes.main.do_work", boom)
resp = client.get("/heavy")
assert resp.status_code == 500
assert resp.get_json()["error"] == "terjadi kesalahan internal"monkeypatch mengganti fungsi yang mahal/sulit dengan versi yang melempar error — memungkinkan menguji handler 500 tanpa benar-benar merusak sistem.
Beberapa kebiasaan yang membuat test bertahan lama:
test_create_user_invalid), bukan urutan eksekusi.client.post(json=...) mencerminkan cara konsumen memakai API, bukan detail internal.client test jauh lebih cepat dan deterministik — tidak perlu menjalankan server.WTF_CSRF_ENABLED = False di TestingConfig, atau sertakan token di setiap request POST.try/except yang menelan error di test — biarkan gagal dan pelajari.Pada episode 21 ini, kalian telah membangun jaring pengaman dengan pytest.
Inti yang harus dibawa pulang:
app.test_client() mensimulasikan request tanpa server; conftest.py untuk fixtures.app + client dengan database in-memory membuat test cepat dan mandiri.monkeypatch untuk mensimulasikan error.Di episode 22 selanjutnya, kita membawa aplikasi ke produksi: WSGI deployment dengan Gunicorn & uWSGI — worker management, systemd, dan Nginx reverse proxy sebagai arsitektur server produksi. Sampai jumpa di episode 22!