Dlaczego odpowiednio wdrożone historie użytkownika nie przechodzą testów jakości
Wyobraźmy sobie następującą sytuację — otrzymujemy powiadomienie, że nowa funkcjonalność jest gotowa do testów. Dobrze, sprawdźmy więc, czy wszystko działa jak należy. Kryteria akceptacji (ang. acceptance criteria): na zielono. Wdrażanie projektu graficznego: na zielono. Wszystkie pozostałe uwagi i szczegóły wdrożenia dodane podczas spotkania udoskonalającego (ang. refinement meeting): na zielono. Wynik testu: negatywny.
Robert Pajura. Software QA Enigneer w Desmart. Testuje aplikacje (i devów) do granic ich możliwości, co czasem skutkuje ich niespodziewanym zachowaniem. I to właśnie wtedy – gdy znajduje spektakularne błędy w oprogramowaniu – jest najbardziej szczęśliwy. Zawodowy pesymizm stara się łączyć z pozytywnym nastawieniem do życia. Wielki fan NBA, kultury japońskiej i wszelakich gier. Jednym z jego marzeń jest zostanie projektantem gier planszowych, jednak póki co większość wolnego czasu poświęca na bieganie, jazdę na rowerze, pływanie lub granie na wiolonczeli.
Tylko dlaczego? Dlaczego mielibyśmy odrzucić historyjkę, która spełnia wszystkie kryteria Definition of Done? Odpowiedź jest prosta — jakość tej funkcjonalności nadal jest niesatysfakcjonująca. W tej części oprogramowania wciąż coś nie działa tak, jak powinno. Innymi słowy, nadal nie spełniliśmy pewnych ukrytych kryteriów akceptacji.
Niektóre z poniższych prawdziwych przykładów usprawnią Waszą pracę podczas spotkań udoskonalających i pomogą wam przewidzieć potencjalne problemy z ukrytymi kryteriami akceptacji.
Spis treści
Przypadek 1: Nie wzięto pod uwagę wymagań niefunkcjonalnych
Opisując historyjki użytkownika i ich kryteria akceptacji, zwykle skupiamy się na perspektywie biznesowej. Próbujemy odpowiedzieć na dwa główne pytania: dlaczego ten produkt potrzebuje tej konkretnej funkcjonalności (jaką potrzebę zaspokoi ta funkcjonalność)? Oraz jak powinna ona działać, żeby oferować użytkownikom wartościowe rozwiązania? Niestety, bardzo często pytanie o to, jak coś ma działać nie zawiera w sobie aspektów technicznych dotyczących oprogramowania — takich jak wydajność, skalowalność, bezpieczeństwo i wygoda użytkowania. Czyli tak zwanych wymagań niefunkcjonalnych.
Załóżmy, że implementujemy prosty formularz — kilka pól wprowadzania tekstu, jedna czy dwie rozwijane listy i pole wyboru. Podczas testowania sprawdzimy standardowe kluczowe elementy funkcjonalności: powinniśmy być w stanie przechodzić od jednego elementu do drugiego, używając klawisza tab/shift+tab, powinniśmy móc wybierać elementy z listy, używając strzałek na klawiaturze, oraz wysłać formularz klikając enter. W innym wypadku, formularz nie będzie wygodny w użytkowaniu i wielu użytkowników czeka frustracja.
Co możemy zrobić? Projektując, analizując i rozwijając funkcjonalność, przemyślmy przynajmniej jej podstawowe niefunkcjonalne aspekty. Pamiętajmy o tym, że różni użytkownicy będą używać naszego produktu na różne sposoby — niektórzy użyją myszki lub touchpada, inni będą starali się jak najwięcej zrobić na klawiaturze. Ponieważ oba sposoby używania funkcjonalności są poprawne, nasze oprogramowanie powinno spełniać oczekiwania dotyczące standardowych zachowań użytkowników w obu przypadkach. Dodatkowo, postarajmy się jak najwięcej pracować z autentycznymi przykładowymi danymi — to najlepsze możliwe dane do testów. Jakość i ilość tych danych mogą być kluczowe dla naszego produktu.
Przypadek 2: Podobne elementy systemu działają różnie
Spójność jest niezbędna dla płynnej interakcji użytkownika z naszym oprogramowaniem. Kiedy przekonujesz się, że podobne elementy interfejsu działają tak samo (np. kiedy możemy kliknąć wszystkie elementy o określonym kolorze), używanie produktu staje się intuicyjne. Jednak jeśli możemy łatwo odnaleźć pewne niespójności w makietach interfejsu użytkownika (ang. wireframes) czy w projekcie graficznym, jeśli chodzi o dane, które chcemy przetworzyć i zaprezentować, używanie oprogramowania stanie się trudniejsze i możemy spotkać się z mylącymi rozbieżnościami.
Przykład: otwieramy portal randkowy z wyszukiwarką i kilkoma dostępnymi filtrami. Jeden z filtrów pozwala określić wiek szukanej osoby. Wybierasz wartość “26”, lista się odświeża i widzisz tylko osoby w wieku 26 lat. Otwierasz pierwszy profil z listy i w szczegółowych informacjach zauważasz, że osoba ma 25 lat. Jak to możliwe? To proste — baza danych zawiera datę urodzenia każdej osoby, ale filtr w wyszukiwarce oblicza wiek wyłącznie w oparciu o rok urodzenia, a wiek wyświetlany w szczegółowych informacjach w profilu konkretnej osoby jest obliczany na podstawie dokładnej daty urodzin — z dniem i miesiącem. Jest to drobiazg, który może zaowocować brakiem zaufania użytkowników do naszego produktu.
Co możemy zrobić? Po pierwsze, w opisanej sytuacji, oba obliczenia powinny być wykonywane przez ten sam kawałek kodu. Ogółem jednak, weźmy pod uwagę, że nasze oprogramowanie to więcej niż tylko suma wszystkich funkcjonalności. Wszystkie elementy muszą współgrać i nie naruszać spójności całego produktu. Zanim nawet zaczniemy projektować czy wdrażać funkcjonalność, zastanówmy się jak wpłynie ona na pozostałe istniejące już części naszego produktu.
Przypadek 3: „Dziwne, u mnie działa”
Klasyk! Założę się, że każdy kto pracuje w IT powiedział lub usłyszał już to stwierdzenie. Często kusi nas, żeby zakładać, że jeśli coś działa w środowisku lokalnym, powinno działać tak samo wszędzie. Często posługujemy się tym „skrótem”, by zaoszczędzić czas lub gdy jesteśmy przekonani, że napisaliśmy dobry kod. Nie mylimy się, prawda? Jednak jeśli uświadomimy sobie jaka jest liczba możliwych kombinacji sprzętu i oprogramowania, nagle zaczynamy tracić pewność siebie.
Przykład: zajmijmy się na chwilę Androidem. Kilka lat temu istniało więcej niż 24 tys. unikalnych modeli z Androidem. Obecnie, jest ich pewnie około 30 tys., lub nawet więcej. Do tego dodajmy różne wersje Androida i szybko zauważymy, że niemożliwe jest zagwarantować wsparcie dla urządzenia każdego użytkownika.
Co więcej, niektóre systemy operacyjne i przeglądarki mogą mieć dodatkowe ograniczenia, które będą wpływać na twój produkt. Przykładowo, starsze wersje przeglądarki Internet Explorer, (wiem, że to ekstremalny przykład, ale jest wiele produktów, które muszą działać bezproblemowo na starszych wersjach IE), mają limit 4095 reguł CSS na jeden arkusz. Jeśli przekroczymy ten limit, pewnie nawet nie poznalibyśmy naszego produktu.
Co możemy zrobić? Na samym początku projektu ustalmy z właścicielem produktu (ang. product owner) oraz zespołem tworzącym oprogramowanie dokładnie jakie systemy operacyjne, które przeglądarki i jakie urządzenia i wersje będą wspierane. Kiedy dysponujemy jasnymi wytycznymi, zarówno rozwój oprogramowania, jak i testowanie staną się prostsze. I zapewne rzadziej będziemy słyszeć — “dziwne, u mnie działa”.
Przypadek 4: Niezrozumienie kryteriów akceptacji
Dobra komunikacja jest niezbędna, aby stworzyć jakościowy produkt, który zadowoli zarówno udziałowców, jak i product ownera. I vice versa, słaba komunikacja może doprowadzić do nieporozumień odnośnie wymogów i spowodować dużo poprawek, zwłaszcza w przypadku współpracy na odległość.
Inną kwestią jest fakt, że wiedza na temat produktu może być przekazywana wyłącznie części zespołu, na przykład analitykom domenowym. Czasem zdaje się, że najrozsądniej będzie nie zapraszać całego zespołu na spotkanie. Jeśli mamy osiem osób w zespole, a spotkanie potrwa godzinę, zużyjemy 8 godzin roboczych, czyli ekwiwalent całego dnia pracy jednej osoby. Z punktu widzenia zarządzania, to może wyglądać na marnowanie zasobów, ale wbrew pozorom często jest wręcz przeciwnie.
Przykład: planujesz wprowadzić ofertę subskrypcji dla klientów, korzystających z twojego produktu. Powinni mieć do wyboru płatności miesięczne (płatność generowana automatycznie co miesiąc przez określony czas, ale co najmniej przez 12 miesięcy) i roczne (użytkownik opłaca z góry cały rok subskrypcji). Innymi słowy, niezależnie od tego, którą opcję wybierze użytkownik, ostatecznie będzie musiał zapłacić przynajmniej za jeden rok. Każdy rodzaj subskrypcji zawiera pewne płatne funkcjonalności, różniące się tym, ile razy w ciągu roku można z nich skorzystać.
Jeden z programistów, który nie przedyskutował tej sprawy ani z właścicielem produktu, ani z nikim innym, zacznie wdrażać funkcjonalność, a po przeczytaniu opisu, przyjmie założenie, że jeśli użytkownik wybierze plan miesięczny, to wszystkie płatne funkcjonalności i ich ilość są odświeżane co miesiąc. W przypadku planu rocznego — są ważne przez cały rok. Jest to błędna logika, ale programista nie przedyskutował z nikim szczegółów tej funkcjonalności i stało się — nieodpowiednio wdrożona funkcjonalność trafia do środowiska testowego.
Co możemy zrobić? Najlepszym sposobem na uniknięcie pomyłek jest zapraszanie całego zespołu na wszystkie ważne spotkania. Jeśli wszyscy członkowie zespołu są na miejscu, gdy podejmowane są decyzje odnośnie produktu, będą mieli informacje z pierwszej ręki i nie będziemy musieli przekazywać informacji dalej każdemu z osobna. Co więcej, podczas spotkań można znaleźć odpowiedź na dodatkowe wątpliwości i pytania, więc w miarę możliwości — pytajmy jak najwięcej! Kto pyta, nie błądzi.
Przypadek 5: Operacje na „problematycznych” danych
Jako tester, instynktownie próbuję zepsuć oprogramowanie, wprowadzając dane, które mogą spowodować niespodziewane lub niechciane efekty. Czasami wystąpi błąd aplikacji, czasem odnajdziemy dziurę w systemie bezpieczeństwa, a czasem zaobserwujemy drobne wady oprogramowania. Jednak czasem to funkcjonalność lub cały produkt są oparte na danych, które okazują się być problematyczne.
Przykład: Jednym z największych problemów w świecie IT jest arytmetyka zmiennoprzecinkowa. Odnajdziecie ją wszędzie tam, gdzie aplikacje pozwalają nam przeprowadzać jakiekolwiek transakcje. Załóżmy, że chcemy obliczyć całkowitą cenę produktu. Cena netto to 99,99, a VAT jest na poziomie 8%. Większość kalkulatorów obliczy cenę całkowitą jako: 107,9892, ale ceny podawane są tylko do drugiego miejsca po przecinku. Pytanie więc, czy ostateczna cena to 107,98 czy 107,99? Nie ma prostej odpowiedzi, bo może to zależeć między innymi od przepisów lokalnych i ograniczeń wynikających z integracji z urządzeniami innych firm.
Co możemy zrobić? W kontakcie z problematycznymi danymi (liczby zmiennoprzecinkowe, daty i czas, zamiana jednostek), musimy mieć 100% pewność jak radzić sobie ze wszystkimi możliwymi scenariuszami, zanim napiszemy choćby pierwszą linijkę kodu. Jako zespół musimy zacząć myśleć — „a co, jeśli?” — i analizować problem na tyle szczegółowo, na ile to możliwe. Niczego nie zakładajmy z góry, zdrowy rozsądek może okazać się bezużyteczny w tej sytuacji. W końcu nie chcielibyśmy powtórzyć pomyłki Mars Climate Orbitera.
Przypadek 6: Integracja z systemem zewnętrznym
Korzystanie z aplikacji zewnętrznych, specjalizujących się w konkretnych usługach, jest bardzo popularne w dzisiejszym procesie tworzenia oprogramowania. Kiedy musimy stworzyć bramkę płatniczą, wprowadzenie lub materiały tutorialowe dla nowych użytkowników, czat wewnątrz aplikacji lub rozwiązanie mailingowe — mamy wiele opcji do wyboru dostępnych w internecie. Można znaleźć zarówno płatne, jak i darmowe produkty dla niemal każdej usługi, jaką chcielibyśmy dodać do naszej aplikacji. A wspólnym mianownikiem tych różnych rozwiązań jest to, że powinny być łatwe w integracji. Wygodnie, prawda? Po co na nowo odkrywać Amerykę, skoro mamy proste i gotowe rozwiązania pod ręką? Problem zaczyna się, gdy gotowe rozwiązania okazują się „częściowo gotowymi” rozwiązaniami lub gdy konfiguracja jest znacznie bardziej problematyczna, niż się spodziewaliśmy.
Przykład: chcemy dodać wewnętrzny czat do swojego produktu. Możemy tworzyć rozmowy jeden na jeden lub grupowe pomiędzy różnymi rodzajami użytkowników. Chcemy również mieć możliwość zablokowania jakiejkolwiek rozmowy pomiędzy użytkownikami, by w razie potrzeby uniemożliwić taką konwersację.
Po sprawdzeniu dokumentacji API, stwierdzamy, że można wyłączyć czat dla konkretnego użytkownika — ta funkcjonalność jest już gotowa. Zdaje się, że już wszystko sprawdziliśmy i jesteśmy gotowi. Kilka sprintów później chcemy zintegrować czat z naszą aplikacją, ale okazuje się, że opis działania tej funkcjonalności był niedokładny — można zablokować użytkownika, ale wówczas nie będzie on miał dostępu do żadnych ze swoich rozmów na czacie. A to z kolei nie spełnia naszych wstępnych wymagań, więc musimy szukać innej zewnętrznej usługi, wysłać prośbę do jej dostawcy lub samodzielnie uporać się ze stworzeniem tej funkcjonalności. Każdy z tych scenariuszy najpewniej wpłynie znacznie na czas dostarczenia funkcji czatu.
Co możemy zrobić? Moim zdaniem, integracja z zewnętrznymi usługami jest najbardziej ryzykowna, i ciężko tutaj o dokładną ocenę ryzyka, o ile nie mamy już sporego doświadczenia z konkretną usługą. Jeśli planujemy skorzystać z aplikacji zewnętrznej w naszym produkcie, zwłaszcza jeśli związane jest to z kluczowymi częściami naszego produktu, musimy zacząć od dokładnej analizy. Powinna ona polegać nie tylko na przeczytaniu dokumentacji API, czy na kontakcie z zespołem pomocy technicznej. Niestety dokumentacja może być nieaktualna lub nawet błędna, a „pomoc techniczna” nie jest tym samym, co „specjaliści techniczni”, co oznacza, że osoby z działu marketingu mogą wprowadzić nas w błąd w kwestiach możliwości, jakie oferuje ich usługa.
Zamiast tego, spróbujmy nawiązać połączenie z API i przetestujmy go, sprawdzając podstawowe przypadki użycia, które chcemy wykorzystać ostatecznie w naszym oprogramowaniu. Upewnijmy się również, że nie ma żadnych dodatkowych wymagań formalnych (np. czasochłonnego procesu weryfikacji) i czy usługa jest dostępna w naszym kraju — jest to szczególnie ważna sprawa, jeśli chodzi o bramki płatności.
Podsumowanie
To tylko niektóre przykłady niespodzianek jakie mogą nas czekać podczas tworzenia oprogramowania. Niektóre zdają się tak oczywiste, że pomyślimy, że tylko początkujący mogą być na nie nieprzygotowani. Jednak prędzej czy później możemy znaleźć się w sytuacji, gdy stwierdzimy po prostu: „o tym nie pomyśleliśmy”. I wówczas trzeba przemodelować schemat bazy danych, zaprojektować na nowo interfejs, zmienić logikę w kodzie źródłowym lub zgłosić błąd.
Im szybciej to się stanie, tym lepiej — jeśli odnajdziemy jakiekolwiek ukryte kryteria akceptacyjne na etapie analizy i projektowania, będą one znacznie mniej kosztowne. W późniejszych etapach tworzenia produktu mogą mieć one bezpośredni wpływ na ryzyko i zarządzanie backlogiem. Może to ostatecznie zaowocować efektem domina i kolejnymi zmianami w priorytetach i zakresie pracy, obniżając wartość końcowego produktu. Wszystko to wpływa na jakość oprogramowania.
Główny wniosek jest taki, że jako programiści nie możemy pozostawać pasywni i pomijać fazy analizy. Jak wiemy z doświadczenia, główne źródło naszej wiedzy, czyli właściciel produktu lub ekspert w tej domenie — również nie wiedzą tego, czego my nie wiemy o dostarczeniu funkcjonalności zgodnej ze wszystkimi oczekiwaniami. Zwykle musimy zadawać odpowiednie pytania, by w pełni zrozumieć jakie czekają nas zadania. Bądźmy uważni i proaktywni. Bierzmy również na siebie odpowiedzialność za produkt, który współtworzymy.
Artykuł został pierwotnie opublikowany na desmart.com. Zdjęcie główne artykułu pochodzi z stocksnap.io.