Zasady testowania wydajnościowego
Efektywność wydajności (lub po prostu „wydajność”) jest istotną częścią zapewniania “pozytywnego wrażenia” użytkownikom podczas korzystania z aplikacji na różnych platformach stacjonarnych i mobilnych. Testowanie wydajnościowe odgrywa kluczową rolę w ustalaniu akceptowalnych poziomów jakości dla użytkownika końcowego i często jest ściśle powiązane z innymi dziedzinami, takimi jak np. inżynieria użyteczności. Ponadto ocena przydatności funkcjonalnej, użyteczności i innych cech jakościowych w warunkach obciążenia, takich jak podczas wykonywania testu wydajnościowego, może ujawnić problemy specyficzne dla obciążenia, które mają wpływ na te właściwości.
Testowanie wydajnościowe nie ogranicza się do domeny internetowej, widocznej dla użytkownika końcowego. Ma również znaczenie dla aplikacji z innych obszarów, z różnymi architekturami systemu takimi jak: klasyczne systemy klient-serwer, systemy rozproszone i wbudowane (embedded).
Cecha wydajności jest sklasyfikowana w standardzie ISO 25010 jako niefunkcjonalna charakterystyka jakościowa z trzema podcharakterystykami jakościowymi opisanymi poniżej. Właściwa koncentracja i ustalenie priorytetów zależy od oceny ryzyka i potrzeb różnych interesariuszy. Analiza wyników testów umożliwi identyfikację innych obszarów ryzyka, którymi należy się zająć.
Zachowanie w czasie: Na ogół ocena zachowania w czasie jest najbardziej powszechnym celem testowania wydajnościowego. Ten aspekt testowania wydajnościowego bada zdolność komponentu lub systemu do reagowania na dane wejściowe użytkownika lub systemu w określonym czasie i w określonych warunkach. Pomiary zachowania w czasie mogą się różnić od całkowitego czasu potrzebnego systemowi na reakcję na dane wejściowe użytkownika w stosunku do liczby cykli CPU wymaganych przez komponent oprogramowania do wykonania określonego zadania.
Wykorzystanie zasobów: Jeśli dostępność zasobów systemowych zostanie zidentyfikowana jako ryzyko, wykorzystanie tych zasobów (np. przydzielenie ograniczonej pamięci RAM), może zostać zbadane poprzez przeprowadzenie określonych testów wydajnościowych.
Przepustowość: Jeżeli problemy związane z zachowaniem systemu przy wymaganych limitach wydajności systemu (np. liczba użytkowników lub wolumeny danych) zostaną zidentyfikowane jako ryzyko, można przeprowadzić testy wydajnościowe w celu oceny adekwatności architektury systemu.
Testowanie wydajnościowe często przybiera formę eksperymentowania, co umożliwia pomiar i analizę określonych parametrów systemu. Można je przeprowadzać iteracyjnie w celu wsparcia analizy, projektowania i wdrażania systemu, aby umożliwić podejmowanie decyzji architektonicznych i pomóc w kształtowaniu oczekiwań interesariuszy.
Poniższe zasady testowania wydajnościowego są szczególnie istotne:
- testy muszą być dostosowane do określonych oczekiwań różnych grup interesariuszy, w szczególności użytkowników, projektantów systemów i personelu operacyjnego,
- testy muszą być możliwe do odtworzenia (reprodukowalne). Należy uzyskać statystycznie identyczne wyniki (w określonej tolerancji) powtarzając testy na niezmienionym systemie,
- testy muszą dawać wyniki, które są zarówno zrozumiałe, jak i łatwe do porównania z oczekiwaniami interesariuszy,
- testy można przeprowadzać, o ile pozwalają na to zasoby, na kompletnych lub częściowych systemach lub środowiskach testowych, które są reprezentatywne dla systemu produkcyjnego,
- testy muszą mieścić się w budżecie i być i możliwe do wykonania w ramach czasowych określonych w projekcie.
W różnych publikacjach znajdziemy solidne podstawy w kwestii zasad i praktycznych aspektów testowania wydajnościowego. Wszystkie trzy z powyższych podcharakterystyk jakościowych będą miały wpływ na zdolność testowanego systemu (SUT - ang. System Under Test) do skalowania.
Typy testów wydajnościowych
Można zdefiniować różne typy testów wydajnościowych. Każdy z nich może mieć zastosowanie do danego projektu, w zależności od celów testu.
Testowanie wydajnościowe
Testowanie wydajnościowe to termin ogólny obejmujący wszelkiego rodzaju testy skoncentrowane na wydajności (responsywności) systemu lub komponentu przy różnym obciążeniu.
Testowanie obciążenia
Testowanie obciążenia koncentruje się na ocenie zdolności systemu do obsługi rosnących poziomów przewidywanych, realistycznych obciążeń, wynikających z żądań transakcji generowanych przez kontrolowaną liczbę współbieżnych użytkowników lub procesów.
Testowanie przeciążające
Testy przeciążające koncentrują się na ocenie zdolności systemu lub komponentu do obsługi szczytowych obciążeń, które są równe lub przekraczają granice przewidywanych lub określonych obciążeń. Testy przeciążające są również wykorzystywane do oceny zdolności systemu do radzenia sobie z ograniczoną dostępnością zasobów, takich jak dostępna moc obliczeniowa, dostępna przepustowość i pamięć.
Testowanie skalowalności
Testowanie skalowalności koncentruje się na ocenie zdolności systemu do spełnienia przyszłych wymagań dotyczących wydajności, które mogą wykraczać poza obecnie wymagane. Celem tych testów jest określenie zdolności systemu do rozwoju (np. przy większej liczbie użytkowników, większej ilości przechowywanych danych) bez naruszania obecnie określonych wymagań dotyczących wydajności lub awaryjności. Gdy znane są granice skalowalności, można ustawić wartości graniczne i monitorować je w środowisku produkcyjnym, aby zapewnić ostrzeganie o problemach, które mogą się pojawić. Ponadto środowisko produkcyjne można przystosowywać za pomocą odpowiedniej ilości sprzętu.
Testowanie skokowe
Testowanie skokowe skupia się na ocenie zdolności systemu do prawidłowego reagowania na nagłe przypływy obciążeń skokowych, a następnie powrotu do stanu stabilnego.
Testowanie wytrzymałościowe
Testowanie wytrzymałościowe koncentruje się na ocenie stabilności systemu w przedziale czasowym specyficznym dla kontekstu operacyjnego testowanego systemu. Ten typ testowania umożliwia sprawdzenie, czy nie występują problemy z pojemnością zasobów (np. wycieki pamięci, połączenia z bazą danych, pule wątków), które mogą ostatecznie obniżyć wydajność i/lub spowodować awarie w punktach krytycznych.
Testowanie współbieżności
Testowanie współbieżności koncentruje się ocenie wpływu sytuacji, w których określone akcje występują jednocześnie (np. gdy w tym samym czasie loguje się duża liczba użytkowników). Problemy ze współbieżnością są niezwykle trudne do znalezienia i odtworzenia, szczególnie, gdy problem występuje w środowisku, nad którym testerzy mają małą kontrolę lub też nie ma jej wcale, na przykład w środowisku produkcyjnym.
Testowanie przepustowości
Testowanie przepustowości umożliwia określenie, ilu użytkowników i/lub transakcji dany system będzie w stanie obsłużyć i nadal spełniać określone cele wydajnościowe. Cele te można również określić w odniesieniu do ilości danych wynikających z transakcji.
Rodzaje testów w procesie testowania wydajnościowego
Główne rodzaje testów wykorzystywane w procesie testowania wydajnościowego obejmują testy statyczne i testy dynamiczne.
Testowanie statyczne
Czynności testowania statycznego są często ważniejsze dla testowania wydajnościowego niż dla testowania przydatności funkcjonalnej. Dzieje się tak, ponieważ wiele krytycznych defektów wydajności jest wprowadzanych do architektury i projektu systemu. Wady te mogą być spowodowane nieporozumieniami lub brakiem wiedzy projektantów i architektów. Wady te mogą być również wprowadzone, ponieważ wymagania nie uwzględniły odpowiedniego czasu odpowiedzi, przepustowości lub celów wykorzystania zasobów, oczekiwanego obciążenia i użytkowania systemu lub ograniczeń.
Do czynności testowania statycznego dla testów wydajnościowych zaliczamy:
- przeglądy wymagań ze szczególnym uwzględnieniem aspektów wydajności i ryzyka,
- przeglądy schematów baz danych, diagramów relacji encji, metadanych, procedur składowanych i zapytań,
- przeglądy systemu i architektury sieci,
- przeglądy krytycznych segmentów kodu systemu (np. złożone algorytmy).
Testowanie dynamiczne
W miarę budowania systemu dynamiczne testy wydajnościowe powinny rozpocząć się tak szybko, jak to możliwe.
Dynamiczne testy wydajnościowe można wykorzystać:
- podczas testów jednostkowych, w tym z wykorzystaniem informacji o profilowaniu, w celu określenia potencjalnych wąskich gardeł i analizy dynamicznej w celu oceny wykorzystania zasobów,
- podczas testowania integracji komponentów, w kluczowych przypadkach użycia i przepływach pracy, zwłaszcza podczas integracji różnych funkcji przypadków użycia lub integracji ze strukturą „szkieletową” przepływu pracy,
- podczas testowania całych procesów biznesowych w ramach testowania systemowego, w różnych warunkach obciążenia,
- podczas testowania integracji systemów, szczególnie w przypadku przepływów danych i przepływów pracy między kluczowymi interfejsami między systemami. W testowaniu integracji systemów nierzadko „użytkownikiem” jest inny system lub maszyna (np. dane wejściowe z wejść czujników i innych systemów),
- podczas testowania akceptacyjnego, aby zbudować zaufanie użytkownika, klienta i operatora co do prawidłowego działania systemu i dostosować system w warunkach rzeczywistych (ale generalnie nie w celu znalezienia defektów wydajności w systemie).
Na wyższych poziomach testów, takich jak testy systemowe i testy integracji systemów, użycie realistycznych środowisk, danych i obciążeń ma kluczowe znaczenie dla dokładnych wyników. W zwinnych i innych iteracyjno-przyrostowych cyklach życia oprogramowania, w celu ograniczenia ryzyka związanego z wydajnością, zespoły powinny uwzględniać statyczne i dynamiczne testy wydajnościowe we wczesnych iteracjach, zamiast czekać na końcowe etapy projektu.
Jeśli częścią systemu jest niestandardowy lub nowy sprzęt, wczesne dynamiczne testy wydajnościowe można przeprowadzić przy użyciu symulatorów. Jednak dobrą praktyką jest jak najszybsze rozpoczęcie testów na rzeczywistym sprzęcie, ponieważ symulatory często nie wychwytują odpowiednio ograniczeń zasobów i zachowań związanych z wydajnością.
Koncepcja generowania obciążenia
Aby przeprowadzić różne rodzaje testów wydajnościowych, należy zamodelować, wygenerować i przesłać reprezentatywne obciążenia systemu do testowanego systemu. Obciążenia są porównywalne z danymi wejściowymi używanymi w przypadku testów funkcjonalnych, ale różnią się w następujący sposób:
- testy obciążenia muszą reprezentować wiele danych wejściowych użytkownika, a nie tylko pojedynczy zestaw danych,
- testy wydajnościowe mogą wymagać specjalnego sprzętu i narzędzi do generowania obciążenia,
- generowanie testów wydajnościowych jest uzależnione od braku jakichkolwiek defektów funkcjonalnych w testowanym systemie, które mogą mieć wpływ na wykonanie testu.
Wydajne i niezawodne generowanie określonego obciążenia jest kluczowym czynnikiem sukcesu podczas przeprowadzania testów wydajnościowych. Istnieją różne opcje generowania obciążenia.
Generowanie obciążenia za pośrednictwem interfejsu użytkownika
Może to być odpowiednie podejście, jeśli reprezentowana ma być tylko niewielka liczba użytkowników i jeśli dostępna jest wymagana liczba oprogramowania klienckiego, z którego można wprowadzić wymagane dane wejściowe. Podejście to można również zastosować w połączeniu z narzędziami do wykonywania testów funkcjonalnych, ale może szybko stać się niepraktyczne wraz ze wzrostem liczby symulowanych użytkowników. Stabilność interfejsu użytkownika (UI) również stanowi krytyczną zależność. Częste zmiany mogą wpływać na powtarzalność testów wydajnościowych i mogą znacząco wpływać na koszty utrzymania. Testowanie za pośrednictwem interfejsu użytkownika może być najbardziej reprezentatywnym podejściem do testów kompleksowych (end-to-end).
Generowanie obciążenia za pomocą testowania w tłumie (ang. crowd testing)
Takie podejście zależy od dostępności dużej liczby testerów, którzy będą reprezentować rzeczywistych użytkowników. W testowaniu w tłumie testerzy są zorganizowani w taki sposób, aby można było wygenerować pożądane obciążenie. Może to być odpowiednia metoda testowania aplikacji, do których jest dostęp z dowolnego miejsca na świecie (np. aplikacje webowe) i może wymagać od użytkowników generowania obciążenia z wielu różnych typów urządzeń i w różnych konfiguracjach. Takie podejście może zaangażować do testów bardzo dużą liczbę użytkowników, jednak generowane obciążenie może nie być tak powtarzalne i precyzyjne jak w innych metodach, a jego zorganizowanie jest bardziej skomplikowane.
Generowanie obciążenia za pomocą interfejsu programistycznego aplikacji (API)
To podejście jest podobne do użycia interfejsu użytkownika do wprowadzania danych, ale wykorzystuje interfejs API aplikacji zamiast interfejsu użytkownika do symulacji interakcji użytkownika z testowanym systemem. Podejście to jest zatem mniej wrażliwe na zmiany (np. opóźnienia) w interfejsie użytkownika i umożliwia przetwarzanie transakcji w taki sam sposób, jak gdyby były wprowadzane bezpośrednio przez użytkownika za pośrednictwem interfejsu użytkownika. Można tworzyć dedykowane skrypty, które wielokrotnie wywołują określone procedury API i umożliwiają symulację większej liczby użytkowników w porównaniu z wykorzystaniem danych wejściowych interfejsu użytkownika.
Generowanie obciążenia przy użyciu przechwyconych protokołów komunikacyjnych
Podejście to obejmuje przechwytywanie interakcji użytkownika z testowanym systemem na poziomie protokołu komunikacyjnego, a następnie odtwarzanie tych skryptów w celu symulacji potencjalnie bardzo dużej liczby użytkowników w powtarzalny i niezawodny sposób.
Typowe rodzaje awarii w testach wydajnościowych i ich przyczyny
Chociaż z pewnością istnieje wiele różnych rodzajów awarii związanych z wydajnością, które można wykryć podczas testów dynamicznych, poniżej przedstawiono kilka przykładów typowych awarii (w tym awarii systemu) wraz z opisem ich typowych przyczyn.
Zbyt wolne odpowiedzi przy każdym poziomie obciążenia
W niektórych przypadkach czas odpowiedzi jest niedopuszczalny, niezależnie od poziomu obciążenia. Może to być spowodowane podstawowymi problemami z wydajnością, w tym między innymi błędnym projektem lub implementacją bazy danych, opóźnieniem sieci i innymi obciążeniami w tle. Takie problemy można zidentyfikować podczas testów funkcjonalnych i użyteczności, a nie tylko testów wydajnościowych, więc analitycy testów (testerzy) powinni zwracać na nie uwagę i zgłaszać je.
Zbyt wolne odpowiedzi przy poziomie obciążenia wzrastającym od średniego do dużego
W niektórych przypadkach czasy odpowiedzi pogarszają się w stopniu niedopuszczalnym, przy obciążeniu wzrastającym od poziomu średniego do dużego, nawet jeśli takie obciążenia zawierają się w całkowicie normalnych, oczekiwanych, dozwolonych zakresach. Podstawowe defekty obejmują przesycenie jednego lub więcej zasobów oraz inne obciążenia w tle.
Pogarszające się czasy odpowiedzi
W niektórych przypadkach czasy odpowiedzi pogarszają się stopniowo lub poważnie w czasie. Podstawowymi przyczynami są wycieki pamięci, fragmentacja dysku, zwiększające się z czasem obciążenie sieci, powiększanie repozytorium plików i nieoczekiwany wzrost bazy danych.
Nieadekwatna lub nierzetelna obsługa błędów przy dużym lub nadmiernym obciążeniu
W niektórych przypadkach czas odpowiedzi jest akceptowalny, ale obsługa błędów pogarsza się przy wysokim i poza wyspecyfikowaną granicą obciążeniu. Podstawowe defekty obejmują niewystarczające pule zasobów, niedostatecznych rozmiarów (za krótkie, za małe) kolejki i stosy oraz zbyt niskie ustawienia limitu czasu.
Konkretne przykłady ogólnych typów awarii wymienionych powyżej obejmują:
- aplikacja webowa, która dostarcza informacji o usługach firmy, nie odpowiada na żądania użytkowników w ciągu siedmiu sekund (ogólna zasada branżowa). Nie można osiągnąć efektywnego działania systemu w określonych warunkach obciążenia,
- system ulega awarii lub nie jest w stanie odpowiedzieć na dane wejściowe użytkownika, gdy jest poddawany nagłej dużej liczbie żądań użytkowników (np. sprzedaż biletów na duże wydarzenie sportowe). Zdolność systemu do obsługi takiej liczby użytkowników jest niewystarczająca,
- reakcja systemu ulega znacznemu pogorszeniu, gdy użytkownicy przesyłają żądania dotyczące dużych ilości danych (np. duży i ważny raport jest publikowany na stronie internetowej do pobrania). Zdolność systemu do obsługi generowanych ilości danych jest niewystarczająca,
- przetwarzanie wsadowe nie może się zakończyć, zanim będzie potrzebne przetwarzanie online. Czas realizacji procesów wsadowych jest niewystarczający w dozwolonym okresie,
- systemowi czasu rzeczywistego zabraknie pamięci RAM, gdy równoległe procesy generują duże zapotrzebowanie na pamięć dynamiczną, której nie można zwolnić na czas. Pamięć RAM nie jest odpowiednio zwymiarowana lub żądania dotyczące pamięci RAM nie mają odpowiedniego priorytetu,
- składnik A systemu czasu rzeczywistego, który dostarcza dane wejściowe do składnika B systemu czasu rzeczywistego, nie jest w stanie obliczyć aktualizacji z wymaganą szybkością. Cały system nie reaguje na czas i może zawieść. Moduły kodu w komponencie A muszą zostać ocenione i zmodyfikowane („profilowanie wydajności”), aby zapewnić osiągnięcie wymaganych szybkości aktualizacji.
Artykuł bazuje na sylabusie „Sylabus ISTQB® Performance Tester”. Do największych zmian należą:
- usunięcie referencji wewnętrznych i zewnętrznych,
- dodanie leadu,
- drobne poprawki redakcyjne
- zmodyfikowanie niektórych określeń.