Belajar Flask - Testing (pytest)
Episode 21 of 27

Belajar Flask - Testing (pytest)

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Mengapa Testing

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).

Setup pytest dan Fixtures

Install dengan pip install pytest pytest-cov, lalu pola fixture inti — satu fixture menyediakan aplikasi + database:

Pythontests/conftest.py - fixtures
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.

Test Pertama: View Sederhana

Pythontests/test_views.py
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 == 404

client.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.

Test API: JSON dan Validasi

Untuk API, periksa status, bentuk JSON, dan validasi input:

Pythontests/test_api.py - CRUD
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 == 404

client.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.

Fixture Auth: Login sebagai User

Untuk endpoint terproteksi (@login_required), buat fixture yang membuat user dan login:

Pythontests/conftest.py - fixture auth
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:

PythonTest endpoint terproteksi
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 == 200

Test 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 Jangkauan Test

Coverage mengukur persentase baris kode yang dijalankan test:

Jalankan test dengan coverage
pytest --cov=app --cov-report=term-missing

Outputnya menampilkan persentase per modul dan baris yang belum teruji. Pedoman:

  • 100% jarang realistis dan bukan tujuan mutlak — fokus pada jalur kritis: auth, validasi, error handling, alur pembayaran.
  • Kode dengan coverage 0% (misal except branch) sering menyembunyikan bug — test mereka.
  • Coverage yang rendah bukan alasan untuk menulis test yang dangkal; ukur kualitas, bukan hanya angka.
PythonUji branch error 500
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.

Pola Testing yang Baik

Beberapa kebiasaan yang membuat test bertahan lama:

  • Satu perilaku per test: judul test menjelaskan perilaku (test_create_user_invalid), bukan urutan eksekusi.
  • Jangan bergantung pada urutan test: setiap test mandiri (database di-reset per test).
  • Uji API dari sisi client: client.post(json=...) mencerminkan cara konsumen memakai API, bukan detail internal.
  • Jalankan di CI: test menjadi gate di pipeline (episode 23) — kode yang gagal test tidak boleh deploy.

Common Pitfalls Testing

  • Test bergantung pada data global: database state bocor antar test — reset per test.
  • Memakai server sungguhan: client test jauh lebih cepat dan deterministik — tidak perlu menjalankan server.
  • CSRF mengganggu test: pastikan WTF_CSRF_ENABLED = False di TestingConfig, atau sertakan token di setiap request POST.
  • Test menutup kesalahan nyata: jangan try/except yang menelan error di test — biarkan gagal dan pelajari.

Penutup

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.
  • Fixture app + client dengan database in-memory membuat test cepat dan mandiri.
  • Uji status code, body JSON, dan jalur error — bukan hanya sukses.
  • Coverage membantu menemukan kode yang belum teruji; 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!

Belajar Flask - Testing (pytest) | Belajar Flask