Obserwując swoją pracę, zacząłem dostrzegać dość powtarzalne wzorce, jak podchodzę do danych typów zadań. Naturalnie wyklarował mi się framework pracy, którym chciałbym się z Tobą podzielić.
Artykuł podzieliłem na części opisujące dwa typowe rodzaje zadań — feature oraz bug. Do obu typów zadań podchodzę nieco inaczej.
Z artykułu najwięcej wyciągną osoby początkujące. Mam nadzieję jednak, że nawet Ci nieco bardziej doświadczeni znajdą w nim coś dla siebie.
Feature
Pracę nad jakimkolwiek zadaniem zaczynam od rekonesansu. Bazując na moich doświadczeniach, opłaca się najpierw czytać, potem pisać. Zaczynanie od pisania zwykle kończy się usuwaniem, potem czytaniem i pisaniem po raz drugi. Zwykle zaczynam od zapoznania się z opisem zadania i wymaganiami. W tym miejscu warto też, nawet pobieżnie, zapoznać się z miejscami w kodzie, które trzeba będzie zmodyfikować. Te dwa kroki często pozwolą wykryć potencjalne problemy i ograniczenia.
Mając już etap analizy wymagań i stanu obecnego za sobą zaczynam myśleć nad rozwiązaniami. Wszystkie przemyślenia i pomysły staram się jak najszybciej przelać na papier (a raczej do pliku 😉), by nie uciekło. Przy tworzeniu notatki nie dbam o jej poprawność językową, formę czy czytelność. Zapisuję wszystko, co może być istotne. Na tym etapie często wychodzą zalety i wady poszczególnych rozwiązań. Często też wtedy wyłapuję problemy dyskwalifikujące dane rozwiązanie. Dodatkową zaletą zapisywania myśli jest to, że czytając tekst, czasami łatwiej jest wyłapać, że coś nie trzyma się kupy.
Mając już gotowe „surowe” notatki wycinam z nich nieistotne informacje i zaczynam pracę nad ich formatowaniem i polerowaniem. W tym miejscu można również pokusić się o diagramy. Zwykle korzystam z Mermaid, Miro lub Excalidraw. Diagramem czasami można coś opisać znacznie lepiej niż słowem. Również analiza diagramu często będzie prostsza dla reszty zespołu niż czytanie ściany tekstu.
Mając gotowy plan, warto go udostępnić pod ticketem z zadaniem lub gdzieś, gdzie reszta zespołu będzie mogła się z nim zapoznać. Jeśli wiemy, że ktoś w zespole ma dużą wiedzę o modyfikowanym przez nas fragmencie systemu lub o planowanych rozwiązaniach, to warto skonsultować plan z tą osobą. Być może pomoże nam to wyłapać problemy i nieścisłości, których samodzielnie nie wyłapaliśmy. Konsultacja pomysłu z zespołem może też pomóc znaleźć alternatywne rozwiązania. Jednak zachęcam, by przychodzić z gotowym planem, a nie liczyć, że ktoś zrobi robotę za nas 😉
Koniec czytania, początek pisania
Mając gotowy plan, siadam do implementacji. Zwykle plan przygotowuję w postaci checklisty lub listy kroków. Dzięki temu mogę skupić się na konkretnym etapie zadania oraz śledzić postępy. W razie, gdybym nie mógł też dokończyć danego zadania, to taka forma dokumentowania pracy jest pomocna, gdyby ktoś miał przejąć pracę nad zadaniem lub pomóc w jego szybszym ukończeniu.
W przypadku pracy nad nowymi funkcjami zwykle najpierw tworzę kod źródłowy, a dopiero potem zajmuję się testami. Po implementacji zmian w kodzie źródłowym uruchamiam testy i sprawdzam, ile z nich nie przechodzi. Jest to dobry moment, by wykryć testy potencjalnie warte usprawnienia. Jeśli przez zmiany w miejscu A, sypią się testy w miejscu B lub testy, które nie sprawdzają modyfikowanego kawałka kodu, to może to być znak, że można napisać je lepiej.
Przykładowo, jeśli w systemie mamy obiekt wykorzystywany w wielu miejscach, to prawdopodobnie konieczna będzie jego częsta inicjalizacja w testach, na przykład:
const user = new User( {
firstName: 'John',
lastName: 'Doe'
} );
Jeśli w obiekcie klasy User
potrzebujemy dodać nową właściwość, to trzeba będzie zaktualizować każdy test, gdzie tworzony jest User
. Może to oznaczać konieczność dostosowania dziesiątek czy nawet setek testów. Można temu zapobiec przez stworzenie prostej funkcji.
function createUserMock( params: Partial<IUser> ): User {
return new User( {
firstName: params.firstName ?? getRandomText(),
lastName: params.lastName ?? getRandomText()
} );
}
const user = createUserMock( { firstName: 'John' } );
Obiekty klasy User
będą tworzone z losowymi wartościami, chyba że potrzebujemy konkretnej wartości. Przykładowo, jeśli mamy kilkadziesiąt testów, gdzie tworzymy taki obiekt, to sprawdzenie firstName
będzie nas pewnie interesowało maksymalnie w kilku. W pozostałych przypadkach firstName
może być czymkolwiek i nie musimy dbać o ustawianie tej wartości. W przypadku dodawania nowych parametrów dodajemy je tylko w funkcji i w testach, gdzie jest to potrzebne. Takich prostych usprawnień można wykryć znacznie więcej. Jednak to jest temat na osobny wpis.
Wróćmy jednak do tematu zepsutych testów. Zanim zacznę pisać nowe testy, to naprawiam już te istniejące. Na etapie dostosowywania testów również mogą zapalić się „lampki ostrzegawcze”. Czasami możemy dojść do wniosku, że coś zrobiliśmy źle. Wtedy jest to dobry moment, by zrobić krok wstecz. Zakładając jednak, że to nie jest nasz przypadek i mamy poprawione istniejące testy, możemy przejść do kolejnego kroku.
Po poprawieniu starych testów zabieram się za dopisanie nowych. Nowe testy zaczynam pisać od testów jednostkowych. Na początku skupiam się na testach pokrywających happy path oraz najistotniejsze edge case-y. Kolejno zaczynam pokrywać coraz mniej istotne i szczegółowe przypadki w kodzie. Coś na kształt zasady „od ogółu do szczegółu”. Staram się też nie popadać przy testowaniu w skrajność. W moim kodzie rzadko można znaleźć code coverage na poziomie 100%. Osobiście widzę mało wartości w pokrywaniu linii kodu o marginalnym znaczeniu dla poprawności działania kodu. Przykładowo, jeśli przy tworzeniu obiektu w TypeScript dana właściwość ma typ string | null
, to nie widzę potrzeby, by przy linii propA: params.propA ?? null
testować przypadek, gdy params.propA
jest undefined
. Takich rzeczy pilnuje TypeScript i test sprawdzający takie rzeczy uważam za sztukę dla sztuki.
Po przygotowaniu testów jednostkowych przygotowuję testy integracyjne. Tutaj skupiam się głównie na przygotowaniu testów pokrywających podstawowe przypadki. W teście integracyjnym interesuje mnie głównie, czy komponenty, które już przetestowałem, jednostkowo działają po połączeniu ich w całość.
Mając gotowe i przetestowane zmiany, przygotowuję pull request i changelog. W changelogu zamieszczam wszystkie informacje istotne dla osób robiących code review. Dodaję też informacje istotne z punktu widzenia historii projektu. Zaliczam do tego informacje, dlaczego coś jest zrobione i dlaczego w ten konkretny sposób. Jeśli w ramach jednego pull requesta zrobiłem coś ponad realizowane zadanie, np. usprawniłem testy czy poprawiłem drobny błąd, to również dopisuję tę informację do changeloga. Reviewer wtedy wie, że zmiana wynika z chęci poprawy kodu, a nie konieczności dodania poprawek po dodanych zmianach.
Oprócz changeloga lubię zostawiać w pull requeście komentarze do poszczególnych linii kodu. Zwykle robię to, gdy chcę, by zwrócono szczególną uwagę na dany fragment kodu lub, gdy nie jestem pewien danej zmiany. Jednak warto to robić z rozwagą, ponieważ komentarze z pull requestów w perspektywie czasu nieco ciężej znaleźć niż changelog.
Przed oznaczeniem osób do code review samodzielnie sprawdzam swój kod. Staram się wyłapać nieścisłości, błędy w kodzie, brakujące testy, literówki, fragmenty kodu do optymalizacji itp. Warto szanować czas innych osób i wysyłać do sprawdzenia kod pozbawiony takich niedoskonałości.
Bug
Ten flow jest nieco mniej skomplikowany niż w przypadku feature. Analizę buga podobnie jak w przypadku feature, zaczynam od analizy opisu ticketa. Świetnie, gdy są podane kroki do reprodukcji błędu. Jeśli nie, to szukam ich samodzielnie. Często odtworzenie błędu pozwala wyciągnąć coś więcej, niż to opisane w tickecie. Tak jak przypadku feature, dobry rekonesans to połowa sukcesu 😉
Drugim krokiem jest zlokalizowanie źródła błędu. To jest najtrudniejsza część tego flow. Nie mam tu żadnego „złotego środka” ani uniwersalnej porady. Na tym etapie ratuję się wszelkimi dostępnymi narzędziami np. dodaję dodatkowe logi, breakpointy i modyfikuję istniejące testy. Czasami posiłkuję się też debuggerami. W tym momencie nie bawię się też w pilnowanie „zgodności ze sztuką”. Nie mam problemu z użyciem @ts-ignore
(na co dzień programuję w TypeScript, instrukcja pozwala zignorować błędy typowania), łamaniem DRY, odwoływania się do prywatnych właściwości, czy komentowaniem kodu nieistotnego z punktu widzenia naprawianego błędu. Wszsytkie chwyty dozwolone.
Czasami błąd jest na tyle prosty, że widać go gołym okiem. Czasami jednak trzeba sięgnąć po drastyczne środki. Praktycznie wszystkie środki z opisanego arsenału wykorzystałem raz do znalezienia źródła wycieku pamięci. Znalezienie go zajęło mi jakiś… tydzień. Ostatecznie najbardziej pomogło mi wykorzystanie ułomności JavaScriptu i możliwości odwoływania się z zewnątrz do prywatnych właściwości obiektów. Dzięki temu udało mi się zlokalizować bardzo głęboko zagnieżdżoną tablicę, która przechowywała referencje do nieużywanych obiektów, co powodowało zapychanie się pamięci. Po zlokalizowaniu winowajcy naprawa zajęła… minutę.
Wiedząc już jak odtworzyć dany błąd i znając jego przyczynę, usuwam wszelkie zmiany dodane na potrzeby debuggowania.
W kolejnym kroku przygotowuję test, który nie powinien przejść z uwagi na omówiony błąd. Następnie implementuję poprawkę i sprawdzam, czy test przechodzi. Jest to klasyczny przykład wykorzystania Test-Driven Development, które do naprawiania bugów wręcz uwielbiam. Przed otwarciem szampana warto też uruchomić pozostałe testy, by upewnić się, że nic nie zepsuliśmy przy okazji.
Dalsze kroki są analogiczne do tych w przypadku feature. Otwieram pull request, tworzę changelog, sprawdzam kod i wysyłam do code review.
Podsumowanie
Ten artykuł to nie jest żadne rocket science. Mam nadzieję, że udało Ci się wynieść z niego coś ciekawego. Moim celem jest optymalizacja wysiłku i czasu pracy. Dodatkowo staram się, by jak najmniej mojego kodu lądowało ostatecznie w koszu lub powodowało później problemy. Myślę, że opisany workflow pracy wiele robi w tym kierunku.
Jestem bardzo ciekaw Twojego workflow pracy, szczególnie jeśli robisz coś inaczej. Jeśli masz jakieś ciekawe uwagi, spostrzeżenia lub chcesz opisać swój workflow pracy, to sekcja komentarzy poniżej jest Twoja! 🙂
Zapisz się na mailing i odbierz e-booka
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.