Een belangrijk deel van het werk van een ontwikkelaar is het fixen van bugs. Daarnaast is het één van mijn favoriete professionele bezigheden. Ik zie elke bug als een puzzel. Echter, je hoort en leest er bijna niks over. Met dit artikel wil ik daar verandering in brengen. Ik zal hier uit de doeken doen wat ik de afgelopen jaren heb geleerd over het fixen van bugs. Na het lezen van het artikel verwacht ik dat jij het in de toekomst beter kan.
Allereerst een stukje context. Je bent onderdeel van een team dat software schrijft. Uiteraard draait deze software niet in isolatie en communiceert het met andere systemen. Tot slot beoefent het team devops, dus je bent verantwoordelijk voor de ontwikkeling en het beheer van de software.
Het artikel zal beschrijven welke fases een bug doormaakt en in elke fase kort uitleggen hoe je de fase herkent en wat je vervolgens kan doen. Tot slot komt er nog een paragraaf over hoe je in de toekomst nog sneller bugs kan vinden.
{ Detectiefase }
Het leven van een bug begint op het moment dat de bug ontstaat. Echter, je weet nog niet dat de bug bestaat, want anders had je hem wel direct gefixed of helemaal nooit laten ontstaan. De eerste keer dat je je bewust wordt van een bug is vaak de melding van een incident. Uiteraard kun je zelf ook een bug vinden, maar dit artikel gaat er vanuit dat iemand anders de bug vindt.
Zo start de detectiefase. Je wordt je bewust van het feit dat er iets niet helemaal in de haak is. Er is nog veel onduidelijk. Het is nog niet zeker dat het een bug is, en het is ook onduidelijk of het probleem zich in je eigen software of in één van de aanpalende systemen bevindt.
Uit het bestaan van een incident kun je afleiden dat er een gebruiker is met een onverwachte ervaring. Het eerste dat je nu wil bereiken is het reproduceren van het probleem. Als je geluk hebt dan heeft de gebruiker een voldoende informatie om het probleem te reproduceren. Als dit niet het geval is, zul je erom moeten vragen. Zonder de mogelijkheid tot reproductie wordt het erg moeilijk om te bewijzen dat er een bug is en dat deze daadwerkelijk is opgelost. In de praktijk worden niet-reproduceerbare bugs niet opgelost.
{ Reproductiefase }
Het is nu tijd om zelf de bug te reproduceren. De hoofdreden hiervoor is snelheid. In het hele traject zul je de bug nog vaak moeten reproduceren. Als je hiervoor elke keer iemand anders nodig hebt, dan zal het traject vele malen langer duren dan wanneer je zelf in staat bent om de bug te reproduceren.
Het einddoel van deze fase is het reproduceren van de bug op je lokale systeem. Soms is het probleem te ingewikkeld om dit in één keer voor elkaar te krijgen. Een goede tussenstap is dan het zelf reproduceren op de omgeving waar de bug is gevonden. Soms is het te moeilijk om het lokaal te reproduceren. In dat geval is reproductie op een andere omgeving het best haalbare.
Als je in staat bent om de bug zelf te reproduceren, dan is het zaak om dit zo efficiënt mogelijk te doen. In deze fase is het waarschijnlijk moeilijk om een unittest te maken die het probleem reproduceert. Echter, het is vaak wel mogelijk om een end-to-end test of zelfs een integratietest te maken die het probleem reproduceert. Hoe sneller je het probleem kan reproduceren, hoe sneller de volgende fases verlopen. Het is belangrijk om hier niet op te bezuinigen. Een investering om snel te kunnen reproduceren, zal zich later dubbel en dwars terugverdienen.
{ Onderzoeksfase }
Nu je in staat bent om de bug snel te reproduceren, is de vervolgstap om te onderzoeken wat de bug veroorzaakt. Er is een aantal technieken die je hierbij kan gebruiken. Het eerste dat je probeert, is de locatie van het probleem vinden. Als er een stacktrace is dan is dit relatief simpel. Met een beetje geluk geeft de stacktrace de exacte locatie in code waar het probleem optreedt. Soms heb je alleen de locatie waar de exceptie is afgevangen, maar vaak kan je van daaruit wel beredeneren waar de fout exact vandaan komt.
Als er geen stacktrace is, dan is het lastiger om te achterhalen waar het probleem optreedt, maar het is zeker niet onmogelijk. De volgende plek om te kijken is de logfile. Bekijk welke logging er is rondom het optreden van de bug. Is er op dat moment geen enkele logging? Verhoog dan het log level en probeer het nog eens. De meeste applicaties loggen normaal gesproken op het niveau info. Voor meer informatie zet je het niveau op debug of nog hoger. Nog steeds geen interessante logging? Dan is het tijd om om zelf logregels aan de code toe te voegen. Als je echt geen idee hebt waar het probleem optreedt, dan begin je met het toevoegen van logregels op plekken waar je denkt dat het misgaat. Vervolgens gebruik je deze nieuwe logregels om het probleem te lokaliseren. Je blijft net zo lang logregels toevoegen, totdat je precies weet waar het misgaat. Pas met het loggen wel op dat je geen gegevens logt die privacygevoelig zijn. Mocht deze toch willen loggen, encrypt de logging dan dusdanig dat alleen jij de logging kan lezen.
Als je eenmaal weet waar het misgaat, dan is de vervolgstap om te kijken waarom het misgaat. Mocht je een stacktrace hebben, is het nu tijd om deze een rustig te lezen. Het is mij meerdere keren overkomen dat ik veel tijd had bespaard als ik rustig de foutmelding had gelezen. Een stacktrace bevat vaak een melding met daarin informatie wat er misgaat of wat er werd verwacht. Een ontwikkelaar heeft de tijd genomen om een extra bericht aan de stacktrace toe te voegen. Er is dus een behoorlijke kans dat deze informatie je gaat helpen om het probleem te begrijpen.
Als er een foutmelding is, is het altijd handig om deze te googelen. Een grote kans dat je dan op Stack Overflow uitkomt en dat is precies waar je wil zijn. In de beste gevallen levert dit de oplossing en in mindere gevallen geeft het nieuwe inzichten of richtingen om verder te onderzoeken.
Mocht Google de oplossing niet hebben, is de volgende stap debuggen. Op dit moment in het traject weet je precies op welke regel code het probleem zich voordoet. Dus je zet een breakpoint op deze regel en je start de applicatie in debug modus. Als je vervolgens de bug reproduceert, zal de debugger stoppen op de regel met het breakpoint en kun je uitgebreid onderzoeken waarom de bug optreedt. Je kijkt dan voornamelijk of de variabelen gevuld zijn met de verwachte waarden. Het komt voor dat het probleem zich niet bevindt in eigen code, maar in een library. Ook dit is geen probleem, want elke moderne ontwikkelomgeving kan prima overweg met breakpoints in libraries. Je hebt nu voldoende informatie verzameld om door te gaan naar de volgende fase.
Als je na bovenstaande tips nog steeds geen idee hebben wat het probleem veroorzaakt, is mijn laatste redmiddel de rubber ducky techniek. Je gaat het probleem uitleggen aan een uh… rubberen badeendje. Het idee hierachter is dat het uitleggen van het probleem je nieuwe inzichten gaat geven. Deze techniek heeft mij meerdere keren geholpen om een probleem beter te begrijpen. In mijn geval moet de rubber ducky een echt persoon zijn, want anders werkt de techniek voor mij niet. Dit heeft als bijkomend voordeel dat de rubber ducky na het uitleggen jou kan helpen het probleem te doorgronden.
{ Oplossingsfase }
Je weet nu precies wat het probleem veroorzaakt en het is tijd om een oplossing te maken. Als het probleem zich voordoet in een library en het daar ook moet worden opgelost, is daar nog een kader over. Deze paragraaf gaat ervan uit dat het probleem wordt opgelost in eigen code.
De eerste stap is een unittest schrijven die de juiste werking van het systeem test. Deze unittest zal nu nog falen, want de bug is nog niet gefixed. Echter, na het schrijven van de test, ga je de bug fixen en behoort de unittest groen te worden. Er zijn twee belangrijke redenen om eerst een unittest te schrijven. Allereerst kan je deze unittest gebruiken om te bewijzen dat het probleem is opgelost. Daarnaast zal deze voorkomen dat hetzelfde probleem nog een keer optreedt in de toekomst.
Als de unittest slaagt, is het tijd om handmatig te controleren of het probleem echt is opgelost. Het komt voor dat er meerdere problemen onder één bug schuilgaan. Het fixen van één probleem is dan niet voldoende. Het handmatig controleren doe je eerst op je eigen machine. Als het probleem lokaal verdwenen is, breng je een nieuwe release naar productie om daar vervolgens ook nog eens te testen. Dit lijkt overkill, maar het komt voor dat de gekozen oplossing alleen lokaal werkt. Het is dus belangrijk om ook altijd op productie te kijken of het probleem ook echt weg is.
{ Afrondingsfase }
In de laatste fase zijn er nog een paar dingen die je zou kunnen doen. Allereerst het afsluiten van het incident. Dit laat de gebruiker weten dat het probleem is opgelost. Vervolgens is het handig om je teamgenoten op de hoogte te brengen. Zeker in het geval van complexe bugs schrijf je een post-mortem of presenteer je hoe de bug is gevonden en hoe deze is verholpen. Dit zorgt ervoor dat hetzelfde type bug in de toekomst sneller wordt opgelost. Tot slot vier je dat de bug is opgelost!
Sneller bugs fixen
Je hebt gelezen hoe je bugs kan fixen. Echter, de vervolgstap is dit in de toekomst nog sneller te kunnen. Er is een aantal zaken belangrijk. De meeste zijn hierboven al aan bod gekomen, maar ik herhaal hier de belangrijkste punten.
Zorg dat je logging op orde is. Zonder goede logging is het moeilijk om de locatie van de bug te vinden. Elke keer dat je logregels moet toevoegen, moet je ook releasen. Dit is tegenwoordig een stuk goedkoper dan vroeger, maar het kost je nog steeds tijd. Dus goede logging kost tijd op de korte termijn, maar bespaart veel tijd op de lange termijn.
Zorg dat je handig bent of wordt met een debugger. Verdiep je in de mogelijkheden van dit geweldige hulpmiddel. Een paar losse tips. Wist je bijvoorbeeld dat een debugger overweg kan met elk Java proces dat er open voor staat? Het Java proces hoeft niet eens op je eigen computer te draaien. Dit wordt remote debugging genoemd. Wist je dat je ook conditionele breakpoints aan kan maken? Soms wil je alleen stoppen als een bepaalde conditie waar is. Verder heeft IntelliJ een stream debugger en deze kan (heel verrassend) streams debuggen. Deze zijn van nature lastig te onderzoeken, maar met deze tool wordt het een stuk makkelijker. Ik hoop in de toekomst nog een keer een artikel te schrijven dat specifiek ingaat op dit gereedschap.
De laatste tip om sneller bugs te fixen, is het hebben van een grote variatie in je unittests. Dit verhoogt de kans dat je een bestaande unittest kan kopiëren en aanpassen om de gevonden bug af te dekken. Dit is natuurlijk veel sneller dan een compleet nieuwe unittest schrijven. Happy hunting!
Bug in een library
Het komt voor dat de bug niet in je eigen code zit, maar in een gebruikte library. Door gebruik te maken van de debugger weet je precies waar de bug zich bevindt. De eerste stap is dan een issue aanmaken bij het project dat de library beheert. De overtreffende stap is dan een pull request maken die de bug oplost. Dit laatste kan redelijk wat tijd en energie kosten, maar hoe gaaf is het als je kan zeggen dat er code van jou in een bekende library zit?!
Bio:
Bouke Nijhuis is managing consultant bij CINQ ICT.