6.5 Debugger eszközök

← 6. Debugging gondolkodásmód

6.5 Debugger eszközök alapjai – breakpoint, watch, step

Áttekintő

A debugger az egyik legerősebb eszköz egy fejlesztő kezében – mégis rengeteg junior soha nem tanulja meg rendesen használni. Ehelyett maradnak a print/console.log/var_dump módszernél, ami bizonyos esetekben teljesen rendben van, más esetekben viszont egy helyes debugger-használattal perc alatt megtalálnánk a hibát, amire így órákat töltünk.

Ebben az alfejezetben nem fogunk konkrét IDE-t tanítani – a debugger-gondolkodásmód ugyanaz VS Code-ban, PyCharm-ban, Chrome DevTools-ban és mindenhol máshol is. Ha egyszer megérted a koncepciókat, bármely eszközben otthon leszel.

Amit megtanulsz:

  • Mi a debugger valójában és miben más, mint a print-alapú hibakeresés
  • Breakpoint elhelyezése és mire jó
  • Step over, step into, step out – mikor melyiket használd
  • Watch változók és call stack figyelése
  • Mikor érdemes debuggert, mikor loggot használni

Miért számít ez az állásinterjún?
Ha az interjúztatód megkérdezi, hogyan közelítenél egy ismeretlen hibához, és azt mondod „print-elném az értékeket" – ez belefér. Ha viszont azt mondod „breakpointot raknék az érintett területre és megnézném a call stack-et" – ez azt jelzi, hogy profi eszközöket használsz.

Miért számít production környezetben?
Production-ban nem tudsz print-elni. Loggolhatsz, igen – de egy fejlesztői gépen, ahol reprodukálni tudod a hibát, a debugger sokszor 10x gyorsabb a megoldáshoz.


Részletes leírás

Mi a debugger és miért más, mint a print?

A print-alapú hibakeresés így néz ki:

def szamol(a, b):
    print(f"a = {a}")  # ezt tettem be
    eredmeny = a * b
    print(f"eredmeny = {eredmeny}")  # ezt is
    return eredmeny

Ez működik. De van néhány probléma:

  • Módosítod a kódot – majd el kell takarítani
  • Csak azt látod, amit előre elhelyeztél – ha rossz helyre tetted, kezdheted elölről
  • Nem látod a programállapot összképét – csak a kiírt értékeket
  • Nem léphetsz „visszafelé" – nem tudod megnézni, mi volt 3 utasítással korábban

A debugger ezzel szemben:

  • Megállítja a program futását egy adott ponton
  • Megmutatja az összes változó értékét abban a pillanatban
  • Lehetővé teszi a lépésenkénti végrehajtást – utasításról utasításra haladva
  • Megmutatja a call stack-et – ki hívta a jelenlegi függvényt, az ki hívta, és így tovább
  • Nem módosítja a kódot – nincs mit utána eltakarítani

Breakpoint – az „állj meg itt" jelzés

A breakpoint egy jelölés, amit a kódban egy adott sorra helyezel. Amikor a program futása eléri ezt a sort, megáll – és te átveszed az irányítást.

Képzeld el, mintha egy könyvet olvasnál és beragasztanál egy lapjelzőt: „ha ide érsz, állj meg és nézd meg alaposan mi van körülötted."

Hogyan kell elhelyezni? Az összes modern IDE-ben a sor számára kattintva (általában a szám melletti margóra) megjelenik egy piros pont – ez a breakpoint. Más eszközökben egy debugger; kulcsszót (JavaScript) vagy breakpoint() függvényhívást (Python 3.7+) illeszthetsz a kódba:

function szamolAr(termek, mennyiseg) {
    const alapAr = termek.ar * mennyiseg;
    debugger; // itt megáll a böngésző DevTools-ban
    const kedvezmeny = alapAr > 10000 ? alapAr * 0.1 : 0;
    return alapAr - kedvezmeny;
}
def szamol_ar(termek, mennyiseg):
    alap_ar = termek["ar"] * mennyiseg
    breakpoint()  # itt megáll a Python debugger
    kedvezmeny = alap_ar * 0.1 if alap_ar > 10000 else 0
    return alap_ar - kedvezmeny

Feltételes breakpoint: A legtöbb debugger lehetővé teszi, hogy a breakpoint csak akkor aktiválódjon, ha egy feltétel teljesül. Például: „állj meg ennél a sornál, de csak ha mennyiseg > 100." Ez rendkívül hasznos, ha egy ciklus 10.000-szer fut le, de csak a 9.873. iterációban törik el valami.


Step over, step into, step out – a három alapmozdulat

Amikor a debugger megáll egy breakpointnál, te irányítod, mi történjen ezután. Három alapvető lépési mód létezik:

Step over (lépj át)

Az aktuális sort végrehajtja, és a következő sorra lép – de ha az aktuális sor függvényhívást tartalmaz, nem megy bele a függvénybe. Kívülről nézi az egészet.

→ eredmeny = szamol(a, b)   ← [STEP OVER] → végrehajta szamol()-t, de nem megy bele
  return eredmeny

Mikor használd: Ha egy függvényt már ismersz és nem akarod végigkövetni, csak az utána következő állapotot akarod látni.

Step into (lépj bele)

Ha az aktuális sor függvényhívást tartalmaz, bemegy a függvénybe és ott folytatja a lépésenkénti végrehajtást.

→ eredmeny = szamol(a, b)   ← [STEP INTO] → bemegy a szamol() függvénybe
    def szamol(a, b):
    →   return a * b         ← itt folytatja

Mikor használd: Ha gyanítod, hogy a hiba a hívott függvényen belül van.

Step out (lépj ki)

Ha egy függvényen belül vagy, befejezi az egész függvényt és visszatér a hívó kódhoz. Hasznos, ha véletlenül beléptél egy függvénybe, amit nem akartál.

    def szamol(a, b):         ← már itt vagy, de rájöttél nem ide kellett
    →   return a * b
[STEP OUT] → visszaugrik a szamol() hívójához
→ eredmeny = szamol(a, b)   ← itt folytatja

Összefoglalás egy mondatban: Step over = ugyanazon a szinten haladok, step into = lemegyek mélyebbre, step out = feljövök egy szinttel.


Watch – változók figyelése

A watch (vagy watch expression) lehetővé teszi, hogy megfigyeld egy változó vagy kifejezés értékét a debugger futása közben. Amikor az érték megváltozik, azonnal látod.

Nem csak változókat figyelhetsz – tetszőleges kifejezéseket is:

  • user.email – egy objektum adott mezőjét
  • lista[3] – egy lista adott elemét
  • len(lista) > 5 – egy feltételt (igaz-e éppen?)
  • a + b – egy számítás eredményét

Ez különösen hasznos ciklusokon belül, ahol ugyanaz a változó iterációnként más értéket vesz fel.


Call stack – ki hívott kit?

A call stack (hívási verem) megmutatja a függvényhívások sorát az aktuális pontig. Alulról felfelé olvasva látod, hogyan jutottál el a program belépési pontjától az aktuális pillanatig.

Például ha a debugger megáll egy hibánál:

→ szamol_kedvezmeny()      ← jelenleg itt vagy
  szamol_vegosszeget()     ← ezt hívta
  feldolgoz_rendelest()    ← ezt hívta
  main()                   ← és ez volt az induló pont

Ez azért értékes, mert megmutatja a kontextust: nem csak azt, hogy hol van a hiba, hanem azt is, hogyan kerültél oda. Ha egy szamol_kedvezmeny() függvény rossz értékeket kap, a call stack-ből kiderül, melyik caller adta át a rossz adatot.


Debugger vs. logging – mikor melyik?

HelyzetDebuggerLogging
Fejlesztői gépen, reprodukálható hiba✅ IdeálisLassabb
Production hiba, amit nem tudsz reprodukálni❌ Nem elérhető✅ Egyetlen lehetőség
Komplex adatstruktúra vizsgálata✅ Mindent látszNehézkes
Ciklus futás közbeni állapotok✅ Feltételes breakpointRengeteg sor kell
Aszinkron / párhuzamos kód⚠️ Trükkös✅ Jobban nyomon követhető
Csapattal megosztandó megfigyelés❌ Csak neked látható✅ Mindenki olvassa

A valóságban a kettő kiegészíti egymást: debugger fejlesztés közben, logging production-ban.


Életszerű példák

1. példa: A kedvezmény nem stimmel

Egy e-kereskedelmi rendszerben a rendelések végösszege néha rosszul jön ki. A hiba csak bizonyos feltételek mellett jelenik meg.

Print-alapú megközelítés:

def szamol_vegosszeg(tetelek, felhasznalo):
    alap = sum(t["ar"] * t["mennyiseg"] for t in tetelek)
    print(f"alap: {alap}")
    kedvezmeny = szamol_kedvezmeny(felhasznalo, alap)
    print(f"kedvezmeny: {kedvezmeny}")
    ado = szamol_ado(alap - kedvezmeny)
    print(f"ado: {ado}")
    return alap - kedvezmeny + ado

Futtatod, kiírja az értékeket, de még mindig nem érted, miért rossz a szamol_kedvezmeny() kimenete.

Debugger-alapú megközelítés:

  1. Breakpointot raksz a kedvezmeny = szamol_kedvezmeny(...) sorra
  2. Lefuttatod a hibás forgatókönyvet
  3. A debugger megáll – látod az összes változót: alap = 15000, felhasznalo = {"prémium": True, "régi_ügyfél": False}
  4. Step into → bemész a szamol_kedvezmeny()-be
  5. Ott rájössz, hogy a feltétel felhasznalo["premium"] (kisbetűs) helyett felhasznalo["prémium"]-ot kellene keresni – a kulcs ékezettel van

A bug kulcselérési hiba volt. Print-el is megtaláltad volna, de a debuggerrel 3 lépés helyett egyből a problémánál vagy.


2. példa: A ciklus valahol elromlik

function feldolgozMegrendelesek(megrendelesek) {
    let osszeg = 0;
    for (let i = 0; i < megrendelesek.length; i++) {
        osszeg += szamolTetel(megrendelesek[i]);
    }
    return osszeg;
}

Az összeg nem stimmel, de csak akkor, ha vannak visszáruzott tételek.

Debugger megközelítés:

  1. Feltételes breakpointot raksz a szamolTetel() hívásra: csak akkor álljon meg, ha megrendelesek[i].tipus === "visszaru"
  2. Így a debugger kihagyja a normál tételeket és egyből a problémás esetekre ugrik
  3. A watch-ba teszed: megrendelesek[i], osszeg
  4. Látod, hogy a visszáruzott tételeknél a szamolTetel() pozitív értéket ad vissza ahelyett, hogy levonná az összeget

3. példa: Melyik függvény adja át a rossz adatot?

A PHP-s rendszerben egy save_user() hívás néha null-t kap email mezőként, bár az adatok látszólag helyesek.

Call stack használat:

  1. Breakpoint kerül a save_user()-be, ahol ellenőrzöd az email értékét
  2. Amikor megáll, a call stack megmutatja:

``` save_user() ← itt vagy register_user() handle_form_submit() ```

  1. Step out → felmész register_user()-be, megnézed mit ad tovább
  2. Ott látod: $email = $data['eamil'] – elírás a kulcsban (eamil helyett email)

A hiba nem a save_user()-ben volt – a call stack nélkül valószínűleg percekig a rossz függvényt vizsgáltad volna.


Tesztfeladatok

1. Melyik debugger-lépés való mire?

Rendeld hozzá a helyes lépést minden szituációhoz:

SzituációHelyes lépés
Benne vagy egy függvényben, amit véletlenül léptél be___
Egy függvényhívás előtt állsz és gyanítod, hogy a hiba abban van___
Egy függvényhívás előtt állsz, de az a függvény tuti jó___

Lehetséges válaszok: Step over, Step into, Step out


2. Mi az a breakpoint?

Válaszd ki a legpontosabb definíciót:

A) Egy speciális szintaxishiba, amit szándékosan beleírunk a kódba
B) Egy jelölés a kódban, amelynél a debugger megállítja a program futását
C) Egy log üzenet, ami a konzolra ír amikor eléri
D) Egy feltétel, ami kivételt dob futás közben


3. Mikor van előnye a debuggernek a print-tel szemben?

Jelöld meg az összes helyes állítást:

A) Ha egyszerre több tucat változó értékét akarod egyszerre látni
B) Ha production-ban kell nyomon követni a hibát
C) Ha komplex objektum belső struktúráját akarod vizsgálni
D) Ha a hiba csak akkor jelenik meg, ha a ciklus 5000-szer fut le
E) Ha a csapatoddal meg akarod osztani a megfigyeléseidet


4. Call stack értelmezése

A debugger megáll, és a call stack így néz ki (felülről lefelé):

validate_email()        ← jelenleg itt
create_account()
register_new_user()
api_handler()

Melyik állítás igaz?

A) A validate_email() hívta a create_account()-ot
B) Az api_handler() hívta a register_new_user()-t
C) A create_account() fog következni a validate_email() után
D) A stack arra utal, hogy az api_handler() az alkalmazás belépési pontja


5. Watch kifejezés

Egy ciklusban figyeled a lista változót. Melyik watch kifejezés jelzi előre, ha a lista kiürül a következő iterációban?

A) lista
B) len(lista) == 0
C) lista[0]
D) len(lista) == 1

(Tipp: azt akarod látni, mielőtt kiürül – tehát mikor van már csak 1 elem)


6. Igaz vagy hamis?

Döntsd el minden állításról:

ÁllításIgaz / Hamis
A debugger csak akkor hasznos, ha az IDE-d támogatja___
A feltételes breakpoint lehetővé teszi, hogy csak bizonyos esetekben álljon meg a program___
A call stack mindig csak egy függvényt mutat___
Watch kifejezésként csak változóneveket adhatsz meg, kifejezéseket nem___
Production-ban a debugger jobb eszköz, mint a logging___

7. Szimulált debugging feladat

Az alábbi pszeudokód egy rendelés feldolgozó rendszer részlete. Olvasd el és válaszolj a kérdésekre:

FUNCTION feldolgoz_rendeles(rendeles_id):
    rendeles = betolt_rendeles(rendeles_id)       // sor 2
    felhasznalo = betolt_felhasznalo(rendeles.user_id)  // sor 3
    ha felhasznalo.egyenleg < rendeles.vegosszeg:  // sor 4
        dobj_hibat("Nincs elég egyenleg")          // sor 5
    vonjuk_le(felhasznalo, rendeles.vegosszeg)     // sor 6
    ments_rendeles(rendeles)                        // sor 7

FUNCTION vonjuk_le(felhasznalo, osszeg):
    felhasznalo.egyenleg = felhasznalo.egyenleg - osszeg  // sor 10
    ments_felhasznalo(felhasznalo)                         // sor 11

Kérdések:

a) Hova raknál breakpointot, ha azt akarod megvizsgálni, hogy a rendeles.vegosszeg helyes értéket tartalmaz-e?

b) Ha a debugger megáll a 6. sornál és step into-t nyomsz, hova ugrik?

c) Ha a 10. sornál vagy és step out-ot nyomsz, hova kerülsz?

d) Mit érdemes watch-ba tenni, ha azt akarod figyelni, hogy az egyenleg levonás után negatív lesz-e?


8. Mikor melyik eszköz?

Szituációhoz rendeld hozzá a helyes eszközt: Debugger / Logging / Mindkettő jó / Egyik sem ideális

SzituációEszköz
Fejlesztői gépen reprodukálható bug vizsgálata___
Production éjszaka kiesett, reggel kell kivizsgálni___
Egy komplex adatstruktúra tartalmát akarod pillanatfelvételként látni___
Nyomon akarod követni, hány kérés érkezett egy API endpointra___
Pair programming közben a kollégáddal együtt keresed a hibát___

Megoldások:

1. Step out | Step into | Step over

2. B

3. A, C, D (B helytelen – production-ban debugger általában nem elérhető; E helytelen – a debugger nem osztható meg, csak logging)

4. B és D (A fordított – validate_email()-t hívta create_account(); C sem igaz – step out esetén create_account()-ba megy vissza)

5. D – amikor len(lista) == 1, a következő iterációban üres lesz

6. Hamis | Igaz | Hamis | Hamis | Hamis

7.
a) 2. vagy 4. sor – a 4. soron már láthatod a rendeles.vegosszeg értékét
b) A vonjuk_le() függvény belsejébe, a 10. sorra
c) Vissza a feldolgoz_rendeles() 7. sorára (a vonjuk_le() hívója utáni következő sorra)
d) felhasznalo.egyenleg - osszeg vagy felhasznalo.egyenleg

8. Debugger | Logging | Debugger | Logging | Mindkettő jó

Scroll to Top