137 7 19MB
Polish Pages [1545] Year 2015
Spis treści
Spis ilustracji Spis tabel Spis listingów Znaki handlowe Wstęp CZĘŚĆ I. PODSTAWY TESTOWANIA 1. Wprowadzenie do testowania 1.1. Dlaczego testowanie jest niezbędne 1.2. Definicja testowania 1.3. Słynne przykłady awarii oprogramowania 1.4. Rys historyczny 1.5. Ogólne zasady testowania 1.6. Jak dużo testować i kiedy skończyć? 1.7. Psychologia testowania 1.8. Kodeks etyczny testera 1.9. Role związane z testowaniem
2. Podstawowe definicje
3. Proces testowy 3.1. Podstawowy proces testowy 3.1.1. Planowanie 3.1.2. Monitorowanie i nadzór 3.1.3. Analiza testów 3.1.4. Projektowanie testów 3.1.5. Implementacja testów 3.1.6. Wykonanie testów 3.1.7. Ocena spełnienia kryteriów wyjścia oraz raportowanie 3.1.8. Czynności zamykające testowanie 3.2. Proces testowy wg ISO/IEC/IEEE 29119
4. Testowanie w cyklu życia oprogramowania 4.1. Modele wytwarzania oprogramowania 4.1.1. Model kaskadowy 4.1.2. Model V 4.1.3. Model W 4.1.4. Rational Unified Process (RUP) 4.1.5. Rapid Application Development (RAD) 4.1.6. Model spiralny Boehma 4.1.7. Metodyki zwinne 4.1.8. Metodologia Cleanroom 4.2. Weryfikacja i walidacja 4.3. Poziomy testów 4.3.1. Testy jednostkowe 4.3.2. Testy integracyjne 4.3.3. Testy systemowe 4.3.4. Testy akceptacyjne 4.3.5. Pozostałe poziomy testów 4.4. Typy testów 4.4.1. Testy funkcjonalne 4.4.2. Testy niefunkcjonalne 4.4.3. Testy strukturalne 4.4.4. Testy związane ze zmianami 4.5. Poziomy a typy testów
CZĘŚĆ II. TECHNIKI PROJEKTOWANIA TESTÓW 5. Testowanie oparte na modelu 5.1. Cechy dobrego modelu 5.2. Taksonomia modeli 5.3. Przykład wykorzystania modelu 5.4. Modele działania oprogramowania 5.4.1. Graf przepływu sterowania 5.4.2. Ograniczenia w stosowaniu grafu przepływu sterowania 5.4.3. Graf przepływu danych 5.4.4. Ścieżki, ścieżki testowe i ścieżki nieosiągalne
6. Techniki testowania statycznego 6.1. Przeglądy 6.1.1. Proces dla testowania statycznego 6.1.2. Metody sprawdzania oraz możliwe wyniki przeglądu 6.1.3. Role 6.1.4. Aspekt psychologiczny przeglądów 6.1.5. Typy przeglądów 6.1.6. Biznesowa wartość przeglądów 6.1.7. Wdrażanie przeglądów 6.1.8. Kryteria sukcesu przeglądów 6.2. Analiza statyczna 6.2.1. Analiza przepływu sterowania 6.2.2. Poprawność sekwencji operacji 6.2.3. Analiza przepływu danych 6.2.4. Narzędzia do parsowania kodu 6.2.5. Testowanie zgodności ze standardami oprogramowania 6.2.6. Metryki złożoności kodu 6.2.7. Formalne dowodzenie poprawności 6.2.8. Symboliczne wykonywanie kodu 6.2.9. Analiza statyczna strony internetowej 6.2.10. Grafy wywołań
7. Analiza dynamiczna 7.1. Wykrywanie wycieków pamięci
7.2. Wykrywanie dzikich i wiszących wskaźników 7.3. Błędy API 7.4. Analiza wydajności (profiling)
8. Techniki oparte na specyfikacji (czarnoskrzynkowe) 8.1. Podział na klasy równoważności 8.1.1. Opis metody 8.1.2. Formalna definicja podziału 8.1.3. Poprawne i niepoprawne klasy równoważności 8.1.4. Procedura tworzenia przypadków testowych 8.1.5. Przykład 8.1.6. Przykład śledzenia artefaktów procesu testowego 8.2. Analiza wartości brzegowych 8.2.1. Opis metody 8.2.2. Metody dwóch oraz trzech wartości granicznych 8.2.3. Które wartości rozważać jako brzegowe? 8.2.4. Przypadek zmiennych ciągłych 8.2.5. Przykład 8.3. Tablice decyzyjne 8.3.1. Opis metody 8.3.2. Wartości nieistotne i minimalizacja tablicy decyzyjnej 8.3.3. Przykład 8.4. Grafy przyczynowo-skutkowe 8.4.1. Opis metody 8.4.2. Przekształcanie między grafami P-S i tablicami decyzyjnymi 8.4.3. Metoda śledzenia wstecznego – redukcja liczby testów 8.4.4. Przykład 8.5. Testowanie przejść między stanami 8.5.1. Opis metody 8.5.2. Tabelaryczne reprezentacje przejść 8.5.3. Kryteria pokrycia dla maszyny stanowej 8.5.4. Diagram maszyny stanowej w UML 8.5.5. Przykład 8.6. Kategoria-podział (Category-Partition) 8.6.1. Opis metody 8.6.2. Przykład
8.7. Drzewa klasyfikacji 8.7.1. Opis metody 8.7.2. Budowa drzewa klasyfikacji 8.7.3. Asortyment produktów programowych i model cech 8.7.4. Przykład 8.8. Metody kombinacyjne 8.8.1. Opis metody 8.8.2. Each Choice 8.8.3. Base Choice 8.8.4. Multiple Base Choice 8.8.5. Pair-wise testing 8.8.6. Pokrycie n-tupletów (n-wise}) 8.8.7. Pełne pokrycie kombinatoryczne 8.8.8. Subsumpcja kryteriów kombinacyjnych 8.9. Testowanie dziedziny 8.9.1. Opis metody 8.9.2. Hiperpłaszczyzny i podprzestrzenie 8.9.3. Wyznaczanie hiperpłaszczyzny przez punkty 8.9.4. Punkty IN, OUT, ON i OFF 8.9.5. Strategia IN-OUT dla testowania dziedziny 8.9.6. Strategia N-ON × M-OFF dla testowania wartości brzegowych 8.9.7. Ograniczenia nieliniowe 8.9.8. Przykład 8.10. Testowanie oparte na przypadkach użycia 8.10.1. Opis metody 8.10.2. Przykład 8.11. Testowanie oparte na scenariuszach 8.11.1. Opis metody 8.11.2. Przykład 8.12. Testowanie oparte na historyjkach użytkownika 8.12.1. Opis metody 8.12.2. Przykład 8.13. Testowanie losowe 8.13.1. Opis metody 8.13.2. Wady i zalety testowania losowego
8.13.3. Automatyzacja i problem wyroczni 8.13.4. Adaptive Random Testing (ART) 8.13.5. Losowanie danych wejściowych 8.13.6. Przykład 8.14. Testowanie oparte na składni 8.14.1. Opis metody 8.14.2. Notacja Backusa–Naura (BNF) 8.14.3. Tworzenie przypadków testowych 8.14.4. Przykład 8.15. Testowanie CRUD 8.15.1. Opis metody 8.15.2. Rozszerzenie metody 8.15.3. Przykład 8.16. Wybór i łączenie technik ze sobą
9. Techniki oparte na strukturze (białoskrzynkowe) 9.1. Testowanie instrukcji 9.1.1. Opis metody 9.1.2. Przykład 9.2. Testowanie gałęzi 9.2.1. Opis metody 9.2.2. Przykład 9.3. Testowanie decyzji 9.3.1. Opis metody 9.3.2. Testowanie decyzji a testowanie gałęzi 9.3.3. Przykład 9.4. Testowanie warunków 9.4.1. Opis metody 9.4.2. Przykład 9.5. Testowanie warunków/decyzji 9.5.1. Opis metody 9.5.2. Przykład 9.6. Testowanie wielokrotnych warunków 9.6.1. Opis metody 9.6.2. Zwarcie (semantyka short-circuit) 9.6.3. Nieosiągalne kombinacje warunków
9.6.4. Przykład 9.7. Testowanie warunków znaczących (MC/DC) 9.7.1. Opis metody 9.7.2. Dwa warianty kryterium MC/DC 9.7.3. Skorelowane a ścisłe pokrycie warunków znaczących 9.7.4. Wyznaczanie wartości warunków pobocznych dla warunku znaczącego 9.7.5. Przykład 9.8. Pokrycie MUMCUT oraz kryteria z nim związane 9.8.1. Opis metody 9.8.2. Kryteria MUTP, MNFP, CUTPNFP i MUMCUT 9.8.3. Przykład 9.8.4. Zdolność metody MUMCUT do wykrywania określonych typów błędów 9.9. Testowanie pętli 9.9.1. Opis metody 9.9.2. Pętle zagnieżdżone 9.9.3. Testowanie wzorców pętli 9.9.4. Przykład 9.10. Liniowa sekwencja kodu i skok (LSKiS) 9.10.1. Opis metody 9.10.2. Przykład 9.10.3. LSKiS a DD-ścieżki i bloki podstawowe 9.11. Testowanie ścieżek pierwszych 9.11.1. Opis metody 9.11.2. Algorytm wyznaczania ścieżek pierwszych 9.11.3. Przykład 9.12. Analiza ścieżek 9.12.1. Wstęp 9.12.2. Testowanie wszystkich ścieżek 9.12.3. Ścieżki liniowo niezależne i testowanie ścieżek bazowych 9.12.4. Wyznaczanie ścieżek bazowych 9.12.5. Przykład 9.12.6. Inne kryteria pokrycia związane z testowaniem ścieżek 9.13. Testowanie przepływu danych
9.13.1. Wstęp 9.13.2. Pokrycie wszystkich definicji (all-defs) 9.13.3. Pokrycie wszystkich użyć (all-uses) 9.13.4. Pokrycie wszystkich du-ścieżek (all-du-paths) 9.13.5. Uwagi o kryteriach pokrycia przepływu danych 9.13.6. Przykład 9.14. Objazdy i ścieżki poboczne (detours, sidetrips) 9.15. Testowanie mutacyjne 9.15.1. Wstęp 9.15.2. Rodzaje mutantów 9.15.3. Proces testowania mutacyjnego 9.15.4. Operatory mutacyjne 9.16. Subsumpcja kryteriów
10. Techniki oparte na defektach i na doświadczeniu 10.1. Wstrzykiwanie błędów 10.2. Taksonomie 10.3. Zgadywanie błędów 10.3.1. Opis metody 10.3.2. Przykład 10.4. Testowanie oparte na liście kontrolnej 10.5. Testowanie eksploracyjne 10.5.1. Opis metody 10.5.2. Przykład sesji testowania eksploracyjnego 10.6. Ataki usterkowe 10.7. Testowanie ad hoc
11. Wybór odpowiednich technik 12. Priorytetyzacja przypadków testowych 12.1. Wprowadzenie 12.2. Ocena priorytetyzacji – miara APFD 12.3. Techniki priorytetyzacji
CZĘŚĆ III. TESTOWANIE CHARAKTERYSTYK JAKOŚCIOWYCH 13. Model jakości według ISO 9126
14. Modele jakości według ISO 25010 15. Testowanie jakości użytkowej 15.1. Testowanie efektywności (effectiveness) 15.2. Testowanie wydajności (efficiency) 15.3. Testowanie satysfakcji (satisfaction) 15.3.1. Przydatność (usefulness) 15.3.2. Zaufanie (trust) 15.3.3. Przyjemność (pleasure) 15.3.4. Komfort (comfort) 15.4. Testowanie wolności od ryzyka (freedom from risk) 15.4.1. Ryzyko ekonomiczne (economic risk) 15.4.2. Ryzyko dotyczące zdrowia i bezpieczeństwa (health and safety risk) 15.4.3. Ryzyko związane ze środowiskiem (environmental risk) 15.5. Testowanie kontekstu użycia (context coverage) 15.5.1. Zupełność kontekstu (context completeness) 15.5.2. Elastyczność (flexibility)
16. Testowanie jakości produktu 16.1. Testowanie funkcjonalnej przydatności (functional suitability) 16.1.1. Zupełność funkcjonalności (functional completeness) 16.1.2. Poprawność funkcjonalności (functional correctness) 16.1.3. Stosowność funkcjonalności (functional appropriateness) 16.2. Testowanie wydajności w działaniu (performance efficiency) 16.2.1. Zachowanie w czasie (time behaviour) 16.2.2. Zużycie zasobów (resource utilization) 16.2.3. Pojemność (capacity) 16.2.4. Techniki testowania wydajności 16.3. Testowanie zgodności (compatibility) 16.3.1. Współistnienie (co-existence) 16.3.2. Współdziałanie (interoperability) 16.3.3. Przykład 16.4. Testowanie użyteczności (usability) 16.4.1. Zrozumiałość (appropriateness recognizability) 16.4.2. Łatwość nauki (learnability)
16.4.3. Łatwość użycia (operability) 16.4.4. Ochrona przed błędami użytkownika (user error protection) 16.4.5. Estetyka interfejsu użytkownika (user interface aesthetics) 16.4.6. Dostęp (accessibility) 16.5. Heurystyki dotyczące użyteczności 16.5.1. Heurystyki Nielsena 16.5.2. Laboratorium badania użyteczności (usability lab) 16.6. Testowanie niezawodności (reliability) 16.6.1. Dojrzałość (maturity) 16.6.2. Odporność na błędy (fault tolerance, robustness) 16.6.3. Odtwarzalność (recoverability) 16.6.4. Dostępność (availability) 16.7. Testowanie zabezpieczeń (security) 16.7.1. Poufność (confidentiality) 16.7.2. Integralność (integrity) 16.7.3. Niezaprzeczalność (non-repudiation) 16.7.4. Odpowiedzialność (accountability) 16.7.5. Uwierzytelnianie (authenticity) 16.8. Testowanie pielęgnowalności (maintainability) 16.8.1. Modularność (modularity) 16.8.2. Powtórne użycie (reusability) 16.8.3. Analizowalność (analyzability) 16.8.4. Modyfikowalność (modifiability) 16.8.5. Testowalność (testability) 16.9. Testowanie przenaszalności (portability) 16.9.1. Adaptowalność (adaptability) 16.9.2. Instalowalność (installability) 16.9.3. Zastępowalność (replaceability)
17. Testowanie jakości danych 17.1. Model jakości danych 17.2. Charakterystyki inherentne 17.2.1. Dokładność (accuracy) 17.2.2. Zupełność (completeness) 17.2.3. Spójność (consistency) 17.2.4. Wiarygodność (credibility)
17.2.5. Aktualność (currentness) 17.3. Charakterystyki inherentne i zależne od systemu 17.3.1. Dostępność (accessibility) 17.3.2. Zgodność (compliance) 17.3.3. Poufność (confidentiality) 17.3.4. Wydajność (efficiency) 17.3.5. Precyzja (precision) 17.3.6. Identyfikowalność (traceability) 17.3.7. Zrozumiałość (understandability) 17.4. Charakterystyki zależne od systemu 17.4.1. Dostępność (availability) 17.4.2. Przenaszalność (portability) 17.4.3. Odtwarzalność (recoverability)
CZĘŚĆ IV. ZARZĄDZANIE TESTOWANIEM 18. Zarządzanie testowaniem w kontekście 18.1. Kontekst ograniczeń projektowych 18.2. Kontekst interesariuszy procesu testowego 18.3. Kontekst produkcji oprogramowania 18.4. Kontekst cyklu życia oprogramowania 18.5. Kontekst testów 18.6. Kontekst czynnika ludzkiego
19. Testowanie oparte na ryzyku 19.1. Czym jest ryzyko? 19.2. Zalety testowania opartego na ryzyku 19.3. Rodzaje ryzyka 19.4. Zarządzanie ryzykiem w cyklu życia 19.5. Identyfikacja ryzyka 19.5.1. Analiza interesariuszy (stakeholder analysis) 19.5.2. Technika „władza versus zainteresowanie” 19.5.3. Techniki identyfikacji ryzyk 19.5.4. Przykłady ryzyk produktowych 19.6. Analiza ryzyka 19.6.1. Klasyfikacja ryzyk
19.6.2. Czynniki wpływające na prawdopodobieństwo i wpływ ryzyka 19.6.3. Ilościowa i jakościowa ocena ryzyka produktowego 19.6.4. Osiąganie konsensusu w procesie decyzyjnym 19.6.5. Priorytetyzacja ryzyk 19.7. Łagodzenie ryzyka 19.7.1. Sposoby łagodzenia ryzyka 19.7.2. Łagodzenie ryzyka przez testowanie 19.7.3. Estymacja kosztów łagodzenia ryzyka 19.8. Monitorowanie ryzyka 19.8.1. Macierz identyfikowalności ryzyk 19.8.2. Aktualizacja ryzyk oraz ich parametrów 19.8.3. Raportowanie 19.9. Techniki analizy ryzyka 19.9.1. PRAM (Pragmatic Risk Analysis and Management) 19.9.2. SST (Systematic Software Testing) 19.9.3. Przykład: zastosowanie SST do systemu ELROJ 19.9.4. PRisMa (Product Risk Management) 19.9.5. Przykład: zastosowanie PRisMa do systemu ELROJ 19.9.6. Analiza zagrożeń (hazard analysis) 19.9.7. Koszt ekspozycji ryzyka (cost of exposure) 19.9.8. FMEA (Failure Mode and Effect Analysis) 19.9.9. Przykład: zastosowanie FMEA do systemu ELROJ 19.9.10. QFD (Quality Function Deployment) 19.9.11. Przykład: zastosowanie QFD do systemu ELROJ 19.9.12. FTA (Fault Tree Analysis) 19.9.13. Przykład: zastosowanie FTA do systemu ELROJ 19.10. TMap (Test Management Approach) 19.11. TestGoal – testowanie oparte na wynikach
20. Pozostałe strategie testowania 20.1. Testowanie oparte na wymaganiach 20.1.1. Testowanie wymagań 20.1.2. Projektowanie testów opartych na wymaganiach 20.1.3. Przykład: obliczanie punktacji w grze w kręgle 20.2. Podejście oparte na modelu (profile operacyjne) 20.3. Podejście metodyczne (listy kontrolne)
20.4. Podejście oparte na standardzie 20.5. Inne podejścia 20.5.1. Podejście reaktywne 20.5.2. Podejście good enough 20.5.3. Podejście konsultacyjne
21. Dokumentacja w zarządzaniu testowaniem 21.1. Dokumenty organizacyjnego procesu testowego 21.1.1. Polityka testów 21.1.2. Przykład polityki testów 21.1.3. Organizacyjna strategia testowania 21.1.4. Przykład organizacyjnej strategii testowania 21.2. Dokumenty procesu zarządzania testowaniem 21.2.1. Plan testów (test plan) 21.2.2. Przykład planu testów 21.2.3. Jednopoziomowy plan testów (level test plan) 21.2.4. Przykład jednopoziomowego planu testów 21.2.5. Raport o stanie testów (test status report) 21.2.6. Przykład raportu o stanie testów 21.2.7. Raport końcowy z testowania (test completion report) 21.2.8. Przykład raportu końcowego z testowania 21.3. Dokumenty dynamicznych procesów testowych 21.3.1. Specyfikacja testów (test design specification) 21.3.2. Przykład specyfikacji testów 21.3.3. Specyfikacja przypadku testowego (test case specification) 21.3.4. Przykład specyfikacji przypadku testowego 21.3.5. Specyfikacja procedury testowej (test procedure specification) 21.3.6. Przykład specyfikacji procedury testowej 21.3.7. Wymagania co do danych testowych (test data requirements) 21.3.8. Przykład wymagania co do danych testowych 21.3.9. Wymagania co do środowiska testowego (test environment requirements) 21.3.10. Przykład wymagań co do środowiska testowego 21.3.11. Raport o gotowości danych testowych (test data readiness report) 21.3.12. Raport o gotowości środowiska testowego (test environment readiness report)
21.3.13. Otrzymane wyniki (actual results) 21.3.14. Wynik testu (test result) 21.3.15. Dziennik wykonania testów (test execution log) 21.3.16. Raport o incydencie (test incident report) 21.3.17. Raport z sesji testowania eksploracyjnego (exploratory testing session report) 21.3.18. Przykład raportu z sesji testowania eksploracyjnego
22. Szacowanie testów 22.1. Czynniki wpływające na szacowanie 22.2. Techniki szacowania 22.2.1. Intuicja, zgadywanie, doświadczenie 22.2.2. Estymacja przez analogię 22.2.3. Struktura podziału prac (Work Breakdown Structure, WBS) 22.2.4. Estymacja grupowa 22.2.5. Dane przemysłowe 22.2.6. Analiza punktów testowych (Test Point Analysis) 22.2.7. Modele matematyczne (parametryczne) 22.3. Negocjacje i redukcja zakresu testów
23. Nadzór i kontrola postępu testów 23.1. Wprowadzenie 23.2. Przykłady metryk 23.2.1. Metryki ryzyka produktowego 23.2.2. Metryki defektów 23.2.3. Metryki przypadków testowych 23.2.4. Metryki pokrycia 23.2.5. Metryki pewności 23.3. Zarządzanie testowaniem opartym na sesji
24. Biznesowa wartość testowania 24.1. Wprowadzenie 24.2. Koszt jakości 24.3. Wartość ekonomiczna oprogramowania i problemy z nią związane 24.4. Dług technologiczny w kontekście testowania
25. Testowanie rozproszone, zakontraktowane i zewnętrzne
25.1. Zespoły w ramach organizacji 25.2. Zespoły w ramach innych oddziałów organizacji 25.3. Dostawcy sprzętu i oprogramowania 25.4. Dostawcy usług testowych 25.5. TaaS (Testing as a Service)
26. Zarządzanie wdrażaniem standardów przemysłowych 27. Zarządzanie incydentami 27.1. Cykl życia defektu 27.2. Atrybuty defektu i ODC 27.2.1. Atrybuty defektów 27.2.2. ODC (Orthogonal Defect Classification) 27.2.3. Przykładowe analizy przy użyciu ODC 27.3. Metryki zarządzania incydentami 27.4. Zawartość raportu o incydencie 27.5. Komunikowanie incydentów
CZĘŚĆ V. LUDZIE I NARZĘDZIA 28. Ludzie i ich kompetencje – tworzenie zespołu 28.1. Budowanie zespołu testowego 28.1.1. Opis stanowiska 28.1.2. Analiza CV 28.1.3. Rozmowa kwalifikacyjna 28.1.4. Asymilacja nowego członka zespołu 28.1.5. Zakończenie zatrudnienia 28.2. Rozwój zespołu testowego 28.2.1. Indywidualny rozwój poszczególnych członków zespołu 28.2.2. Wyznaczanie celów 28.2.3. Dynamika zespołu testowego 28.2.4. Określanie ról i odpowiedzialności 28.2.5. Umiejętności zespołu jako całości: gap analysis 28.2.6. Indywidualne osobowości i role w zespole 28.2.7. Rozwój umiejętności i szkolenia 28.2.8. Mentoring
28.2.9. Okresowa ocena członków zespołu 28.3. Przywództwo 28.3.1. Zarządzanie a przywództwo 28.3.2. Model zmiany według Satir 28.3.3. Cechy charakteru lidera i zasady przywództwa 28.3.4. Informowanie i komunikacja 28.3.5. Lojalność, zaufanie i odpowiedzialność 28.3.6. Budowanie zespołu 28.4. Poziomy niezależności zespołu testowego 28.5. Komunikacja
29. Techniki pracy grupowej 29.1. Proces podejmowania decyzji 29.2. Techniki wspomagania kreatywności 29.2.1. Burza mózgów 29.2.2. NGT (Nominal Group Technique) 29.2.3. Metoda analogii 29.2.4. JAD (Joint Application Development) 29.3. Techniki porządkowania i priorytetyzacji 29.3.1. Diagram podobieństwa (affinity diagram) 29.3.2. Macierz i graf priorytetyzacji 29.3.3. Mapa myśli 29.4. Techniki szacowania i oceny 29.4.1. Wielokrotne głosowanie 29.4.2. Metoda delficka i Wideband Delphi 29.4.3. Poker planistyczny (planning poker) 29.4.4. Model Saaty’ego (Analytic Hierarchy Process) 29.5. Definiowanie problemu, przyczyny źródłowej lub możliwości 29.5.1. Analiza pola sił (force field analysis) 29.5.2. Immersja 29.5.3. Diagram rybiej ości (diagram Ishikawy)
30. Testowanie wspierane narzędziami 30.1. Podstawowe zagadnienia związane z użyciem narzędzi 30.1.1. Wybór odpowiedniego narzędzia 30.1.2. Koszty wdrożenia narzędzia
30.1.3. Ryzyka związane z wdrożeniem narzędzia 30.1.4. Korzyści z wdrożenia narzędzia 30.1.5. Strategie wdrażania automatyzacji 30.1.6. Integracja i wymiana informacji między narzędziami 30.1.7. Klasyfikacja narzędzi testowych 30.2. Automatyzacja testów 30.2.1. Czynniki sukcesu udanej automatyzacji 30.2.2. Metryki dla automatyzacji testów 30.3. Generyczna architektura automatyzacji testów 30.3.1. Warstwa generowania testów 30.3.2. Warstwa definiowania testów 30.3.3. Warstwa wykonania testów 30.3.4. Warstwa adaptacji testów 30.3.5. Zarządzanie konfiguracją 30.4. Automatyczna generacja danych testowych 30.5. Metody i techniki automatyzacji testów 30.5.1. Kroki procesu projektowania architektury 30.5.2. Podejścia do automatyzacji przypadków testowych 30.5.3. Podejście nagraj i odtwórz 30.5.4. Skrypty linearne 30.5.5. Skrypty zorganizowane 30.5.6. Testowanie oparte na danych 30.5.7. Testowanie oparte na słowach kluczowych 30.5.8. Testowanie oparte na procesie 30.5.9. Testowanie oparte na modelu 30.5.10. Języki i notacje dla definicji testów 30.6. Katalog narzędzi testowych 30.6.1. Narzędzia do zarządzania testami 30.6.2. Narzędzia do wykonywania/organizacji testów 30.6.3. Narzędzia do testowania mutacyjnego, posiewu usterek i wstrzykiwania błędów 30.6.4. Narzędzia do testów bezpieczeństwa 30.6.5. Symulatory i emulatory 30.6.6. Narzędzia do analizy statycznej i dynamicznej 30.6.7. Narzędzia do testów wydajnościowych
30.6.8. Narzędzia typu capture and replay 30.6.9. Narzędzia do testów jednostkowych 30.6.10. Testowanie w metodyce BDD 30.6.11. Narzędzia do testowania opartego na modelu 30.6.12. Narzędzia do śledzenia defektów, zarządzania incydentami i raportowania 30.7. Wdrażanie narzędzi w organizacji 30.7.1. Cykl życia narzędzia 30.7.2. Użycie narzędzi open source 30.7.3. Przykładowe problemy związane z użyciem narzędzi
CZĘŚĆ VI. UDOSKONALANIE PROCESU TESTOWEGO 31. Kontekst udoskonalania procesu 31.1. Po co udoskonalać? 31.2. Co można udoskonalić? 31.3. Spojrzenia na jakość 31.3.1. Jakość oparta na produkcie 31.3.2. Jakość oparta na użytkowniku 31.3.3. Jakość oparta na wytwarzaniu 31.3.4. Jakość oparta na wartości 31.3.5. Jakość transcendentna 31.4. Generyczny proces udoskonalania 31.4.1. Cykl Deminga–Shewarta (PDCA) 31.4.2. IDEAL 31.4.3. Podstawowe zasady doskonałości (Fundamental Concepts of Excellence) 31.5. Przegląd podejść do udoskonalania 31.5.1. Podejścia oparte na modelu 31.5.2. Podejścia analityczne 31.5.3. Podejścia hybrydowe 31.5.4. Inne podejścia do udoskonalania procesu testowego
32. Udoskonalanie oparte na modelu 32.1. Wprowadzenie 32.2. Udoskonalanie procesów organizacyjnych i biznesowych
32.2.1. TQM (Total Quality Management) 32.2.2. ISO 9000 32.2.3. EFQM Excellence Model 32.2.4. Six Sigma 32.2.5. Lean 32.3. Udoskonalanie procesu produkcji oprogramowania 32.3.1. CMMI 32.3.2. ISO/IEC 15504 32.3.3. ITIL (Information Technology Infrastructure Library) 32.3.4. TSP (Team Software Process) 32.3.5. BOOTSTRAP 32.4. Udoskonalanie procesu testowego – modele referencyjne procesu 32.4.1. TPI Next (Test Process Improvement) 32.4.2. TMMi (Test Maturity Model integration) 32.4.3. Porównanie TPI Next i TMMi 32.5. Udoskonalanie procesu testowego – modele referencyjne zawartości 32.5.1. STEP (Systematic Test and Evaluation Process) 32.5.2. CTP (Critical Testing Processes)
33. Podejście analityczne 33.1. Wprowadzenie 33.2. Analiza przyczynowa 33.2.1. Opis metody 33.2.2. Wybór elementów do analizy przyczynowej 33.2.3. Zebranie i zorganizowanie informacji 33.2.4. Identyfikacja przyczyny źródłowej przez analizę zebranych informacji 33.2.5. Wyciągnięcie wniosków 33.3. Podejście GQM (Goal–Question–Metric) 33.3.1. Opis metody 33.3.2. Fazy GQM 33.3.3. Dwa paradygmaty metody GQM 33.3.4. Wzorzec definiowania celu 33.3.5. Siedem pytań 33.3.6. Przykład 33.4. Miary, metryki i wskaźniki
34. Wybór metody usprawniania 35. Proces udoskonalania 35.1. Wprowadzenie 35.2. Rozpoczęcie procesu doskonalenia 35.2.1. Określenie powodu doskonalenia (stymulacja do zmiany) 35.2.2. Ustanowienie celów dla doskonalenia testowania 35.2.3. Określenie kontekstu 35.2.4. Pozyskanie wsparcia 35.2.5. Stworzenie infrastruktury dla procesu udoskonalania 35.3. Diagnozowanie aktualnej sytuacji 35.3.1. Scharakteryzowanie obecnego oraz pożądanego stanu procesu 35.3.2. Rekomendacje akcji naprawczych 35.4. Ustanowienie planu doskonalenia procesu testowego 35.4.1. Ustanowienie priorytetów dla wdrażania planu doskonalenia 35.4.2. Opracowanie podejścia do wdrożenia 35.4.3. Zaplanowanie działań związanych z wdrożeniem 35.5. Działanie w celu wdrożenia udoskonaleń 35.5.1. Stworzenie rozwiązania 35.5.2. Rozwiązania pilotażowe/testowe 35.5.3. Doprecyzowanie rozwiązania 35.5.4. Zaimplementowanie rozwiązania 35.6. Wyciąganie wniosków z projektu doskonalenia testów 35.6.1. Analiza i weryfikacja przeprowadzonych działań 35.6.2. Propozycje przyszłych rozwiązań
36. Organizacja, role i umiejętności 36.1. Organizacja 36.1.1. Zakres działań GPT 36.1.2. Organizacja Grupy procesu testowego 36.1.3. Właściwości Grupy procesu testowego 36.2. Role i umiejętności 36.2.1. Doskonalący proces testowy 36.2.2. Główny oceniający 36.2.3. Oceniający 36.2.4. Umiejętności doskonalącego proces testowy
37. Czynniki sukcesu CZĘŚĆ VII. JAKOŚĆ OPROGRAMOWANIA 38. Czym jest jakość oprogramowania? 38.1. Testowanie oprogramowania a jakość oprogramowania 38.2. Model Kano 38.3. Dojrzałość procesu i standardy jakości 38.3.1. SPR 38.3.2. Ocena Malcolma Baldridge’a 38.4. Co mierzyć, jak mierzyć i po co mierzyć?
39. Podstawy teorii pomiarów 39.1. Metryka, miara, wskaźnik, pomiar 39.2. Skale pomiarowe 39.2.1. Skala nominalna 39.2.2. Skala porządkowa 39.2.3. Skala interwałowa 39.2.4. Skala stosunkowa 39.2.5. Podsumowanie rodzajów skal pomiarowych 39.3. Typy metryk 39.3.1. Metryka bezpośrednia (podstawowa) 39.3.2. Suma/różnica 39.3.3. Stosunek 39.3.4. Proporcja 39.3.5. Odsetek 39.3.6. Miary iloczynowe 39.3.7. Tempo 39.4. Spójność i odpowiedniość pomiaru 39.5. Błędy pomiarowe 39.6. Podstawowe zasady analizy danych 39.6.1. Miary tendencji centralnej 39.6.2. Miary rozproszenia 39.6.3. Korelacja i regresja liniowa 39.6.4. Przyczynowość
40. Narzędzia kontroli jakości 40.1. Klasyfikacja narzędzi 40.2. Rodzaje narzędzi oraz obszary ich zastosowań 40.3. Statystyczna kontrola procesu 40.3.1. Wykres przebiegu 40.3.2. Karty kontrolne 40.4. Wykres czasu cyklu 40.5. Narzędzia analizy i zapobiegania przyczynom źródłowym 40.5.1. 5 pytań „dlaczego?” i diagram „why–why” 40.5.2. Macierz „jest–nie jest” 40.5.3. Kaizen 40.5.4. Poka yoke
41. Metryki wielkości oprogramowania 41.1. Metryki wolumenowe 41.1.1. LOC 41.1.2. Współczynnik produktywności języka 41.1.3. Pomiar specyfikacji i projektu 41.2. Metryki funkcjonalności 41.2.1. Punkty funkcyjne 41.2.2. Punkty obiektowe i rozszerzone punkty obiektowe 41.2.3. Punkty cech 41.2.4. Punkty przypadków użycia
42. Metryki charakterystyk jakościowych 42.1. Metryki dla funkcjonalności 42.2. Metryki dla niezawodności 42.3. Metryki dla użyteczności 42.4. Metryki dla wydajności 42.5. Metryki dla pielęgnowalności 42.6. Metryki dla przenaszalności
43. Metryki złożoności oprogramowania 43.1. Metryki Halsteada 43.2. Złożoność cyklomatyczna McCabe’a 43.2.1. Gęstość złożoności cyklomatycznej
43.2.2. ECC (Essential Cyclomatic Complexity) 43.3. Konstrukcje składniowe 43.4. Metryki struktur 43.5. Metryki złożoności systemu 43.5.1. Indeks utrzymywalności 43.5.2. Metryka złożoności systemu Agrestiego–Carda–Glassa 43.6. Metryki obiektowe 43.6.1. Metryki Lorenza 43.6.2. Metryki CK 43.7. Metryki złożoności dokumentacji 43.8. Metryki złożoności algorytmicznej
44. Metryki i modele wysiłku 44.1. Modele oparte na zgadywaniu i intuicji 44.2. Modele oparte na dekompozycji 44.3. Modele oparte na wiedzy eksperckiej 44.4. Modele oparte na benchmarkach 44.5. Modele oparte na porównaniu 44.5.1. Porównanie proste (naiwne) 44.5.2. Porównanie z uwzględnieniem różnic 44.6. Modele parametryczne 44.6.1. Tworzenie własnego modelu 44.6.2. Model COCOMO II 44.7. Łączenie modeli i uwagi na temat estymacji
45. Metryki i modele dla defektów 45.1. Natura defektów 45.2. Metryki defektów 45.3. Modele statyczne defektów 45.3.1. Model wprowadzania/usuwania defektów 45.3.2. Model fazowy 45.3.3. Model dwufazowy 45.3.4. Efektywność usuwania defektów i powstrzymanie fazowe 45.3.5. Modele zmian w kodzie 45.4. Modele dynamiczne defektów 45.4.1. Model Rayleigha
45.4.2. Model wykładniczy i S-kształtny 45.4.3. Model COQUALMO 45.5. Analiza mutacyjna 45.6. Metryki dynamicznej stylometrii
46. Metryki i modele przyrostu niezawodności 46.1. Wprowadzenie 46.2. Matematyczne podstawy teorii niezawodności 46.2.1. Funkcja niezawodności i funkcja awarii 46.2.2. Metryka MTTF (średniego czasu do awarii) 46.2.3. Rozkłady prawdopodobieństwa modelujące występowanie awarii 46.2.4. Rozkład wykładniczy i jego związek z metryką MTTF 46.2.5. Funkcja częstości awarii oraz ryzyko (hazard rate) 46.2.6. Prawdopodobieństwo awarii do czasu t 46.3. Modele przyrostu niezawodności 46.3.1. Model Jelinskiego–Morandy 46.3.2. Model niedoskonałego debugowania Goela–Okumoto 46.3.3. Niejednorodny model procesu Poissona Goela–Okumoto 46.3.4. Logarytmiczny model Poissona czasu wykonywania Musy– Okumoto 46.3.5. Model S-kształtny 46.3.6. Inne modele niezawodności
47. Metryki i modele dostępności 47.1. Dostępność 47.1.1. Dostępność ciągła a dostępność wysoka 47.1.2. Metody zwiększające dostępność systemu 47.1.3. Ilościowa miara dostępności i jej obliczanie 47.2. Odmładzanie oprogramowania 47.2.1. Powody degradacji i sposoby odmładzania oprogramowania 47.2.2. Wpływ odmładzania na dostępność systemu
48. Metryki dla procesu testowego 49. Metryki zadowolenia klienta 49.1. Proces pomiaru zadowolenia klienta 49.1.1. Wybór metody i sposobu przeprowadzenia badania
49.1.2. Opracowanie ankiety/kwestionariusza lub innego narzędzia badań 49.1.3. Wybór metody próbkowania 49.1.4. Wybór rozmiaru próby 49.1.5. Zebranie, opracowanie i analiza danych 49.2. Dział wsparcia klienta
50. Sposób prezentowania danych 50.1. Prezentowanie danych graficznych 50.2. Prezentowanie metryk
Dodatek A Dodatek B Dodatek C Dodatek D Bibliografia O autorze
Spis ilustracji
1.1. U-kształtna zależność gęstości defektów od rozmiaru modułu 1.2. Dwa przykłady skumulowanej liczby zgłoszonych i naprawionych defektów 3.1. Podstawowy proces testowy (wg ISTQB) 3.2. Przykład obustronnego śledzenia między artefaktami projektu 3.3. Przypadek testowy wysokiego poziomu 3.4. Przypadek testowy niskiego poziomu 3.5. Histogram odsetka wymagań pokrytych testami 3.6. Wielowarstwowy model procesu testowego wg ISO/IEC/IEEE 29119 3.7. Proces projektowania i implementacji testów wg ISO/IEC/IEEE 29119 4.1. Model kaskadowy (wodospadowy) 4.2. Model V 4.3. Model W 4.4. Model RUP 4.5. Model szybkiego prototypowania RAD 4.6. Model spiralny Boehma 4.7. Model Scrum 4.8. TDD 4.9. Metodologia Cleanroom 4.10. Hierarchia czynności weryfikacji i walidacji wg ISO/IEC/IEEE 29119 4.11. Projekt architektury przykładowego programu 5.1. Taksonomia modeli 5.2. Generacja kodu Graya o długości 3 z kodu o długości 2 5.3. Model 8-bitowego konwertera Gray2Order 5.4. Podstawowe elementy grafu przepływu sterowania 5.5. Transformacja konstrukcji algorytmicznych na grafy przepływu sterowania
5.6. Podział kodu na bloki podstawowe i odpowiadający mu graf przepływu sterowania 5.7. Problematyczne programy dla modelowania przepływu sterowania 5.8. Graf przepływu danych dla funkcji compute_prefix 5.9. Kod oraz odpowiadający mu CFG z nieosiągalną ścieżką 6.1. Porównanie miejsc powstawania i wykrywania defektu bez stosowania przeglądów (u góry) i z przeglądami (na dolej) 6.2. Podstawowy model inspekcji Fagana 6.3. Porównanie nakładu pracy w projekcie z inspekcjami i bez nich 6.4. Graf przepływu sterowania
dla
programu wykorzystującego
kolejkę
z ograniczoną pojemnością 6.5. Graf przepływu sterowania i jego złożoność cyklomatyczna 6.6. Przykład złego grafu przepływu sterowania oraz jego korekta 6.7. Wykonanie symboliczne kodu zamieniającego wartości zmiennych 6.8. Struktura strony www sklepu internetowego 6.9. Przykład niezrównoważonej i zrównoważonej struktury strony internetowej 6.10. Podstawowe typy wywołań między modułami 6.11. Graf wywołań dla programu grep.c 8.1. Podział dziedziny na klasy równoważności 8.2. Niepoprawny podział dziedziny 8.3. Obustronne śledzenie między artefaktami procesu testowego 8.4. Klasy równoważności i wartości brzegowe dla programu ZniżkaMPK 8.5. Metody dwóch oraz trzech wartości granicznych 8.6. Drzewo możliwych wartości wszystkich warunków 8.7. Ilustracja minimalizacji tablicy decyzyjnej 8.8. Opcje programu Word 8.9. Elementy składowe grafów przyczynowo-skutkowych 8.10. Wykorzystanie operatorów O i M dla reprezentacji przyczyn i skutków o więcej niż dwóch możliwych wartościach 8.11. Fragment grafu P-S odpowiadający jednej regule tablicy decyzyjnej 8.12. Graf P-S stworzony na podstawie tablicy decyzyjnej 8.13. Graficzna ilustracja reguł śledzenia wstecznego 8.14. Graf P-S do którego zastosowano śledzenie wsteczne 8.15. Ogólny model stanów oraz przejść między nimi. Dwie konwencje zapisu 8.16. Maszyna stanowa dla automatu biletowego
8.17. Graficzna reprezentacja różnych typów stanów 8.18. Maszyna stanowa z warunkami na niektórych przejściach 8.19. Przykładowa maszyna stanowa 8.20. Graf de Brujina dla ścieżek długości 2 8.21. Maszyna stanowa dla procesu opisującego pracę redakcji czasopisma 8.22. System komputerowego rozpoznawania obiektów 8.23. Typy wierzchołków w drzewie klasyfikacji 8.24. Drzewo klasyfikacji dla systemu SKRO wraz z macierzą testową 8.25. Notacja dla modelu cech 8.26. Model cech dla asortymentu produktu programowego smartfonu 8.27.
Drzewo
klasyfikacji
wraz
z
tabelą
kombinacji
dla
kryterium
minimalistycznego 8.28. Etapy ręcznego tworzenia przypadków dla kryterium pokrycia par 8.29. 2-(4, 4, 1)-tablica ortogonalna i jej przekształcenie na zbiór przypadków testowych 8.30. Hierarchia subsumpcji dla kombinacyjnych kryteriów pokrycia 8.31. Hiperpłaszczyzny i podprzestrzenie w przestrzeni dwu- i trójwymiarowej 8.32. Obszar z brzegiem „otwartym” 8.33. Przykłady punktów IN, OUT, ON i OFF dla testowanego obszaru 8.34. Zdolność punktów ON-OFF do wykrywania błędów dziedziny 8.35. Redukcja liczby punktów ON i OFF dla kombinacji ograniczeń 8.36. Graficzna reprezentacja warunków dla kategoryzacji urządzeń ciśnieniowych 8.37. Warunki testowe na połączeniach linii 8.38. Model przepływu zdarzeń dla oprogramowania bankomatu 8.39. Typowe wzorce obszarów wejścia powodujących awarie 8.40. Ilustracja technik adaptacyjnego testowania losowego 8.41. Macierz CRUD 9.1. Kod i CFG dla programu SortBąbelkowe 9.2. Krawędzie CFG jako elementy pokrycia w testowaniu gałęzi 9.3. Graf przepływu sterowania dla programu Bisekcja 9.4. CFG i jego drzewo zagnieżdżeń pętli 9.5. Symboliczna reprezentacja LSKiS 9.6. DD-ścieżki a bloki podstawowe 9.7. Przykładowy CFG 9.8. CFG oraz ścieżki pierwsze dla funkcji CzyPierwsza
9.9. CFG z wykładniczą liczbą ścieżek testowych względem decyzji 9.10. Przykładowy CFG i jego zbiór ścieżek bazowych 9.11. CFG dla programu TypTrójkąta oraz dwa zbiory ścieżek bazowych 9.12. Przykładowy graf przepływu danych 9.13. Objazdy i ścieżki poboczne 9.14. Proces testowania mutacyjnego 9.15. Hierarchia subsumpcji dla białoskrzynkowych kryteriów pokrycia 10.1. Proces wstrzykiwania usterek i ulepszania zestawu testów (za [131]) 10.2. Karta testu przed rozpoczęciem sesji testowania eksploracyjnego 10.3. Stworzenie etykiety w MS Word 10.4. Stworzenie podpisu tablicy w MS Word 10.5. Stworzenie w tekście odsyłacza do tablicy w MS Word 10.6. Karta testu po zakończeniu sesji testowania eksploracyjnego 10.7. Model usterki powodowanej interakcją programu i środowiska 11.1. Heurystyczny model strategii testowej 12.1. Geometryczna interpretacja miary APFD 13.1. Model jakości według ISO 9126 14.1. Model jakości McCalla 14.2. Model jakości użytkowej według ISO 25010 14.3. Model jakości produktu według ISO 25010 14.4. Model jakości danych według ISO 25012 16.1. Ekran programu ELROJ – przykład niepoprawnej funkcjonalności 16.2. Przykłady testowania obciążeniowego 16.3. Przykład złej skalowalności systemu 16.4.
Przykładowy
wynik
analizy
WAMMI
(źródło:
www.wammi.com/whatis.html) 16.5. Informowanie użytkownika o stanie systemu 16.6. Brak zgodności między systemem a rzeczywistością 16.7. Kontrola użytkownika nad systemem 16.8. Spójność interfejsów w obrębie rodziny produktów MS Office 16.9. Zapobieganie błędom: okno kalendarza zamiast pola na ręczne wpisanie daty 16.10. Wybór lokalizacji zamiast ręcznego wpisywania ścieżki 16.11. Zaawansowane opcje niewidoczne dla początkującego użytkownika 16.12. Prosty i estetyczny interfejs elektronicznego kalkulatora 16.13. Przykłady złych komunikatów o błędach
16.14.
Laboratorium
badania
użyteczności
(źródło:
www.designperspectives.com/usability.html) 16.15. Mapa ciepła (heat map) dla śledzenia wzroku 16.16. Relacje między metrykami MTTR, MTTF i MTBF 16.17. Średni czas między awariami 16.18. Zależności między modułami i odpowiadająca im macierz struktury projektu 17.1. Przykład zgodności danych z zewnętrznymi regulacjami 17.2. Przykład poufności danych (hasło do poczty) 17.3. Przykład identyfikowalności danych 18.1. Klasyczny trójkąt ograniczeń projektowych 18.2. Skomplikowana struktura zależności między rolami w projekcie 19.1. Prawdopodobieństwo ryzyka a jego wpływ 19.2. Zależność między wielkością zużycia zasobów a osiąganym poziomem jakości 19.3. Model zarządzania ryzykiem 19.4. Macierz „władza versus zainteresowanie” 19.5. Czynniki wpływające na prawdopodobieństwo i wpływ ryzyka 19.6. Przykłady macierzy ryzyka produktowego 19.7. Poziom ryzyka według jego klasyfikacji 19.8. Przykład symulatora dla łagodzenia ryzyka 19.9. Macierz identyfikowalności ryzyka-testy 19.10. Przykład wysokopoziomowego raportu o ryzykach 19.11. Systemy o wysokiej i niskiej wierności 19.12. Czynności w analizie ryzyka według metody SST 19.13. Dwie postaci macierzy ryzyka produktowego w metodzie PRisMa 19.14. Proces PRisMa 19.15. Macierz ryzyka PRisMa i reguła koła 19.16. Techniki projektowania testów a kwadranty macierzy PRisMa 19.17. Macierz ryzyka produktowego dla systemu ELROJ 19.18. Związek między zagrożeniem, ryzykiem a awarią 19.19. Macierz QFD (tzw. dom jakości) 19.20. Macierz QFD dla systemu ELROJ 19.21. Notacja drzew awarii 19.22. Przykładowe drzewo awarii
19.23. Drzewo awarii dla systemu ELROJ 19.24. Model TMap 19.25. TestGoal – dziesięć zasad testowania sterowanego wynikami 20.1. Podział wymagań 20.2. Łańcuch Markowa jako model profilu operacyjnego 20.3. Wartość testowania w podejściu good enough 21.1. Hierarchia dokumentacji testowej wg ISO/IEE 29119-3 21.2. Przykładowy dziennik wykonania testów 22.1. Struktura podziału prac dla liczby testów systemu ELROJ 22.2. Schemat APT 23.1. Wymiary postępu testowania 23.2.
Macierz
identyfikowalności
testy
–
ryzyka
oraz
historia
testów
przykładowego projektu 23.3. Raport z pokrycia ryzyk 23.4. Raport o defektach 23.5. Raport o defektach w podziale na ich umiejscowienie 23.6. Dwa raporty o testach i defektach dla modułu B 23.7. Raport o przypadkach testowych 23.8. Raport o testach uwzględniający wysiłek i podział na cechy jakościowe 23.9. Raport z pokrycia 24.1. Model kosztu jakości 24.2. Klasyczne i współczesne spojrzenie na ekonomiczny model kosztu jakości 25.1. Testowanie rozproszone, zakontraktowane i zewnętrzne 25.2. Klasyfikacja usług TaaS 27.1. Cykl życia incydentu 27.2. ODC: rozkład defektów dla atrybutu „wpływ” 27.3. Raport z narzędzia do zarządzania incydentami 27.4. Raporty na podstawie danych z dziennika incydentów 28.1. Przykładowe ogłoszenie o pracę dla testera 28.2. Hierarchia potrzeb – piramida Maslowa 28.3. Model Tuckmana dynamiki zespołu 28.4. Matryca RACI 28.5. Inwentarz umiejętności i gap-analysis 28.6. Wskaźnik osobowości Myers–Briggs 28.7. Model analizy transakcyjnej Wagnera
28.8. Style uczenia się według Honeya i Mumforda 28.9. Model zmiany według V. Satir 28.10. Cechy charakteru lidera 28.11. Komunikacja kierownika testów z innymi podmiotami 29.1. Generyczny proces podejmowania decyzji 29.2. Diagram podobieństwa – przykład 29.3. Graf priorytetyzacji – przykład 29.4. Mapa myśli – przykład 29.5. Wielokrotne głosowanie – przykład 29.6. Metoda delficka – przykład 29.7. Proces metody Wideband Delphi 29.8. Poker planistyczny w formie aplikacji na smartfona 29.9. AHP – struktura przykładowego problemu decyzyjnego 29.10. Analiza pola sił dla problemu zmiany w organizacji 29.11. Diagram rybiej ości (Ishikawy) – przykład 30.1. Automatyzacja vs testowanie ręczne – koszty 30.2. Koszty związane z wdrożeniem narzędzia 30.3. Ryzyka związane z wykorzystaniem narzędzi w testowaniu 30.4. Model generycznej architektury automatyzacji testów 30.5. Proces projektowania architektury automatyzacji testów 30.6. Różne sposoby reprezentacji elementów GUI: a) check-box; b) radio button; c) lista rozwijana 30.7. Formularz ze strony www obliczającej indeks BMI 30.8. Widok okna Selenium po nagraniu przypadku testowego 30.9. Selenium – odtworzenie nagranych akcji przypadku testowego 30.10. Oczekiwana informacja na ekranie systemu ELROJ 30.11. Skrypt oparty na słowach kluczowych z wykorzystaniem programu Excel 30.12. Języki i notacje stosowane w automatyzacji testów 30.13. Diagramy UML 2.5 30.14. Cykl życia narzędzia 31.1. Kontekst udoskonalania procesu testowego 31.2. Przykładowe obszary udoskonalania testowania 31.3. Cykl Deminga i jego zastosowanie w kontekście udoskonalania 31.4. Model IDEAL 31.5. Przegląd metod udoskonalania
31.6. Model People CMM 32.1. Model doskonałości EFQM (Excellence Model) 32.2. Six Sigma – rozkład normalny i zmienność procesu 32.3. CMMI – model z reprezentacją etapową 32.4. Obszary procesowe CMMI: weryfikacja i walidacja 32.5. Model TPI Next 32.6. TPI Next – macierz dojrzałości testów 32.7. TPI Next – macierz dojrzałości dla przykładowego procesu 32.8. TPI Next – bazowy układ klastrów 32.9. TPI Next – przykładowa sekwencja doskonalenia oparta na klastrach 32.10. TMMi – poziomy dojrzałości i odpowiadające im obszary procesów 32.11. TMMi – struktura modelu 32.12. STEP – elementy modelu 32.13. STEP – struktura aktywności w podziale na poziomy testów 32.14. CTP – model procesu 33.1. Analiza Pareto 33.2. Przestrzenna i czasowa identyfikacja grup defektów 33.3. Rozkład normalny 33.4. Wykres X–Y dla identyfikacji defektów do analizy przyczynowej 33.5. Techniki zbierania i organizacji informacji 33.6. Rozszerzenie diagramu Ishikawy w celu przeprowadzenia analizy 33.7. GQM – schemat modelu 33.8. Paradygmat GQM – koncepcja pomiaru 33.9. Paradygmat QIP – koncepcja udoskonalania 33.10. GQM – wzorzec definiowania celu 35.1. Korporacyjna tablica rozdzielcza – przykład 35.2. Zrównoważona karta wyników – przykład 36.1. Struktura Grupy procesu testowego dla dużej organizacji 36.2. Umiejętności doskonalącego proces testowy 38.1. Model Kano 39.1. Hierarchia abstrakcji pojęć w teorii pomiaru 39.2. Przykład zastosowania skali nominalnej 39.3. Graficzna reprezentacja spójności i odpowiedniości pomiaru 39.4. Rozkład dobrze opisywany przez modę 39.5. Różne stopnie korelacji
39.6. Model liniowy zależności defektów od punktów funkcyjnych 39.7. Podatność korelacji na wartości odstające 39.8. Kwartet Anscombe’a 40.1. Wykres przebiegu dla dostępności systemu 40.2. S-kształtny (skumulowany) wykres przebiegu 40.3. Karta kontrolna i przykłady anomalii 40.4. Karta kontrolna 40.5. Wykres czasu cyklu 40.6. Diagram why–why 40.7. Macierz jest–nie jest 40.8. Kaizen 41.1. Zależność gęstości defektów od rozmiaru programu 41.2. Procedura obliczania punktów funkcyjnych 41.3. Punkty funkcyjne – pięć typów funkcji 43.1. Ustrukturalizowane i nieustrukturalizowane elementy CFG 43.2. Redukcja CFG w celu obliczenia ECC 43.3. Przykładowy system 43.4. Przykładowa hierarchia klas 44.1. Model szacowania przez porównanie 44.2. Porównanie modeli parametrycznych dla wysiłku 44.3. Dane o PF i wysiłku oraz krzywa do nich dopasowana 45.1. Wykorzystanie modeli defektów 45.2. Dynamika i występowanie defektów – przykłady 45.3. Gęstość defektów w kolejnych wersjach oprogramowania 45.4. Model wprowadzania/usuwania defektów 45.5. Model wprowadzania i usuwania defektów w pojedynczej fazie 45.6. Macierz defektów i obliczanie metryk PCE, DRE i Dunna 45.7. Model Rayleigha dla różnych wartości współczynnika skali 45.8. Rozkład Weibulla dla różnych współczynników kształtu 45.9. Graficzna reprezentacja zaobserwowanych danych 45.10. Predykcja na podstawie modelu Rayleigha 45.11. Rozkład wykładniczy – gęstość i dystrybuanta 45.12. COQUALMO – podmodele wprowadzania i usuwania defektów 45.13. Model dynamicznej stylometrii – predykcja gęstości defektów 46.1. Indeks niezawodności
46.2. Dystrybuanta rozkładu wykładniczego 46.3. Model Jelinskiego–Morandy – chwilowa częstość awarii 47.1. Strategie odmładzania i reagowania a dostępność systemu 49.1. Metody pomiaru zadowolenia klienta 50.1. Marsz wojsk napoleońskich na Moskwę – przykład idealnego wykresu 50.2. Przykłady błędnie prezentowanych danych 50.3. Przykład niepożądanej i pożądanej redundancji 50.4. Wykres z dodanymi informacjami kontekstowymi 50.5. Przykład drążenia danych A.1. Przykładowy ekran obsługiwany przez program ELROJ A.2. Schemat logiczny systemu ELROJ C.1. Granica funkcji C.2. Geometryczna interpretacja pochodnej funkcji w punkcie C.3. Geometryczna interpretacja całki oznaczonej C.4. Ilustracja prawdopodobieńs twa warunkowego
Spis tabel
1.1. Najważniejsze odkrycia w historii testowania oprogramowania 1.2. Koszt usuwania defektu w podziale na fazy cyklu życia 3.1. Typowe poziomy testów i odpowiadająca im podstawa testów 3.2. Mapowanie wymagań na elementy projektowe systemu ELROJ 3.3. Możliwe relacje między rzeczywistym a uzyskanym wynikiem testu 5.1. Przypadki testowe dla konwertera Gray2Order 5.2. Modele działania oprogramowania 6.1. Efektywność
usuwania
defektów
dla
wybranych statycznych metod
testowania 6.2. Prosta analiza kosztów i zysków z przeprowadzenia przeglądu 6.3. Możliwe typy następstw operacji na zmiennych 6.4. Możliwe następstwa operacji dla zmiennej i w programie compute_prefix 8.1. Podsumowanie metody podziału na klasy równoważności 8.2. Zestaw warunków testowych dla programu TypTrójkąta na podstawie charakterystyki „typ trójkąta” 8.3. Zestaw warunków testowych dla programu TypTrójkąta na podstawie charakterystyk Ch2, Ch3 i Ch4 8.4. Przypadki testowe pokrywające klasy równoważności dla programu TypTrójkąta 8.5. Macierz identyfikowalności dla elementów pokrycia i przypadków testowych 8.6. Podsumowanie metody analizy wartości brzegowych 8.7. Porównanie efektywności
różnych podejść do
identyfikacji
wartości
granicznych 8.8. Przypadki testowe w analizie wartości brzegowych dla programu CarPanel 8.9. Ogólna postać tablicy decyzyjnej
8.10. Podsumowanie metody tablicy decyzyjnej 8.11. Tablica decyzyjna dla programu Egzaminator 8.12. Zminimalizowana tablica decyzyjna dla programu Egzaminator 8.13. Jeszcze bardziej zminimalizowana tablica decyzyjna programu Egzaminator 8.14. Tablica decyzyjna dla programu Bankier 8.15. Zminimalizowana tablica decyzyjna dla programu Bankier 8.16. Przypadki testowe dla programu Bankier 8.17. Przykładowa tablica decyzyjna z wymaganiami nieosiągalnymi 8.18. Przykładowa
tablica
decyzyjna
z wymaganiami
nieosiągalnymi
po
zminimalizowaniu 8.19. Podsumowanie metody grafów przyczynowo-skutkowych 8.20. Wynikowa tablica decyzyjna po zastosowaniu metody śledzenia wstecznego dla skutku S1 8.21. Tabelaryczna postać maszyny stanowej automatu biletowego 8.22. Pełna tabela przejść maszyny dla automatu biletowego 8.23. Krawędzie wychodzące z poszczególnych stanów 8.24. Obliczanie ścieżek o długości 2 8.25. Podsumowanie metody grafów przyczynowo-skutkowych 8.26. Przypadki testowe dla kryterium 0-przełączeń dla programu Czasopismo 8.27. Przypadki testowe dla kryterium 1-przełączeń dla programu Czasopismo 8.28. Podsumowanie metody kategoria-podział 8.29. Parametry i kategorie metody getDisplayString 8.30. Parametry, kategorie i wybory dla metody getDisplayString 8.31. Podsumowanie metody drzew decyzyjnych 8.32. Przypadki testowe dla programu SKRO 8.33. Przypadki testowe spełniające kryterium Each Choice 8.34. Elementy pokrycia dla kryterium Base Choice 8.35. Elementy pokrycia dla kryterium Multiple Base Choice 8.36. Podsumowanie metod kombinacyjnych 8.37. Podsumowanie metod analizy dziedziny 8.38. Kategoryzacja urządzeń ciśnieniowych w zależności od dopuszczalnego ciśnienia i pojemności 8.39. Przypadki testowe dla programu Kategoryzacja 8.40. Przypadek użycia dla szybkiej wypłaty z bankomatu
8.41.
Przypadek
testowy
dla
scenariusza
głównego
przypadku
użycia
SzybkaWypłata 8.42. Przypadek testowy dla scenariusza alternatywnego przypadku użycia SzybkaWypłata 8.43. Podsumowanie metody testowania opartego na przypadkach użycia 8.44. Podsumowanie metody testowania opartego na scenariuszach 8.45. Przypadek testowy dla scenariusza „udana wypłata” 8.46. Przypadek testowy dla scenariusza „nierozpoznana karta” 8.47. Przypadek testowy dla scenariusza „trzykrotny błędny PIN” 8.48. Przypadek testowy dla scenariusza „niewystarczająca ilość środków na koncie” 8.49. Podsumowanie metody testowania opartego na historyjkach użytkownika 8.50. Przypadki testowe dla historyjki użytkownika 8.51. Podsumowanie metody testowania losowego 8.52. Testowanie losowe – przypadki testowe dla modułu Przekształcenie 8.53. Podsumowanie metody testowania opartego na składni 8.54. Przypadki testowe dla techniki opartej na składni 8.55. Przypadki testowe wyprowadzone z macierzy CRUD 8.56. Różne realizacje możliwych operacji CRUD 8.57. Macierz CRUD dla systemu ELROJ 8.58. Przekształcona macierz CRUD dla systemu ELROJ 9.1. Podsumowanie metody testowania instrukcji 9.2. Przypadki testowe dla pokrycia instrukcji 9.3. Podsumowanie metody testowania gałęzi 9.4. Przypadki testowe spełniające kryterium pokrycia gałęzi – wariant I 9.5. Przypadki testowe spełniające kryterium pokrycia gałęzi – wariant II 9.6. Podsumowanie metody testowania decyzji 9.7. Przypadki testowe dla pokrycia decyzji 9.8. Przypadki testowe dla decyzji złożonej z 3 warunków 9.9. Podsumowanie metody testowania warunków 9.10. Przypadki testowe dla testowania warunków 9.11. Podsumowanie metody testowania warunków/decyzji 9.12. Przypadki testowe dla pokrycia warunków/decyzji 9.13. Podsumowanie metody testowania warunków wielokrotnych 9.14. Przypadki testowe nieuwzględniające zwarcia
9.15. Testy dla pokrycia wielokrotnych warunków z short-circuit 9.16. Przypadki testowe dla kryterium pokrycia wielokrotnych warunków 9.17. Wartościowanie warunków
dla
skorelowanego
pokrycia
warunków
znaczących 9.18. Skorelowane pokrycie warunków znaczących – po usunięciu duplikatów 9.19. Wartościowania warunków dla ścisłego pokrycia warunków znaczących 9.20. Ścisłe pokrycie warunków znaczących – przypadki po usunięciu duplikatów 9.21. Tablica prawdy dla predykatu D = p ∧ (q ∨ r) 9.22. Podsumowanie metody testowania warunków znaczących 9.23. Tablica prawdy dla operatora XOR 9.24. Tablica prawdy dla predykatu funkcji leapYear 9.25. Przypadki testowe spełniające kryterium MC/DC dla funkcji leapYear 9.26. Podsumowanie metody testowania pętli 9.27. Pokrycie ścieżek iterowanych w metodzie wzorców pętli 9.28. Przypadki testowe dla pokrycia pętli w programie SortBąbelkowe 9.29. Podsumowanie metody testowania LSKiS 9.30. Możliwe LSKiS dla programu Potęgowanie 9.31. Przypadki testowe pokrywające LSKiS 9.32. Podsumowanie metody testowania ścieżek pierwszych 9.33. Przypadki testowe dla pokrycia ścieżek pierwszych programu CzyPierwsza 9.34. Podsumowanie metody testowania ścieżek 9.35. Testy dla TypTrójkąta spełniające kryteria pokrycia ścieżek i ścieżek bazowych 9.36. Podsumowanie metody testowania przepływu danych 9.37. Miejsca i typy wystąpienia zmiennych w procedurze RównanieKwadratowe 9.38. Przypadki testowe dla pokrycia wszystkich definicji 9.39. Przypadki testowe dla pokrycia wszystkich użyć 9.40. Podsumowanie metody testowania mutacyjnego 10.1. Tasonomia defektów oparta na przyczynie źródłowej (root-cause) 10.2. Lista kontrolna dla sprawdzenia użyteczności strony www 12.1. Przypadki testowe i usterki przez nie wykrywane 16.1. Ochrona przed błędami użytkownika – przykłady dobrych i złych rozwiązań 17.1. Baza danych adresów urzędów wojewódzkich 19.1. Ilościowe szacowanie ryzyka 19.2. Przykłady ryzyk produktowych i projektowych
19.3. Lista ryzyk z określonym poziomem i kategorią 19.4. Priorytetyzacja ryzyk 19.5. Poziomy krytyczności wg normy DO-178C 19.6. Poziomy nienaruszalności bezpieczeństwa wg IEC 61508 19.7. Obliczanie poziomu ryzyka w metodzie SST 19.8. Lista ryzyk dla programu ELROJ 19.9. Wagi czynników w PRisMa 19.10. Szacowanie ryzyka przez kierownika projektu 19.11. Szacowanie ryzyka przez analityka biznesowego 19.12. Szacowanie ryzyka przez architekta systemu 19.13. Uśrednione oceny 19.14. Ważona ocena ryzyk produktowych 19.15. Metody łagodzenia ryzyka dla systemu ELROJ 19.16. Wpływ redukcji ryzyka 19.17. FMEA dla programu ELROJ 20.1. Postać wymagań i proponowane techniki ich testowania 20.2. Profil operacyjny dla wyboru funkcji bankomatu 22.1. Przykładowe szacowanie kosztów testów z korektą 22.2. Średnie przemysłowe parametrów dotyczących testowania 27.1. ODC: przykład dwuwymiarowej analizy 29.1. Macierz priorytetyzacji – przykład 30.1. Zestaw słów kluczowych dla programu ELROJ 30.2. Frameworki wspierające testowanie jednostkowe 30.3. Przykładowe problemy z używaniem narzędzi 32.1. Doskonalenie oparte na modelu – korzyści i ryzyka 32.2. Obszary procesowe TMMi oraz ich cele i praktyki specyficzne 32.3. Porównanie TPI Next i TMMi 32.4. STEP – struktura poziomu testów 32.5. CTP – przykładowe metryki 33.1. Przykłady kategoryzacji defektów dla ich wyboru w analizie przyczynowej 35.1. Porównanie podejść do wdrożenia doskonalenia procesu testowego 36.1. Typy zdolności emocjonalnych według Mayera i Saloveya 39.1. Skale pomiarowe – podsumowanie 39.2. Dane o punktach funkcyjnych i liczbie defektów dla modelu regresji 40.1. Narzędzia kontroli jakości
40.2. Parametry kart kontrolnych 40.3. Pomiary dla karty kontrolnej 41.1. Tablica produktywności dla wybranych języków programowania 41.2. Wagi dla poszczególnych złożoności typów funkcji 41.3. Obliczanie złożoności typu funkcji danych 41.4. Obliczanie złożoności typu transakcji dla plików wewnętrznych 41.5. Obliczanie złożoności typu transakcji dla plików/zapytań zewnętrznych 41.6. Punkty funkcyjne dla systemu ELROJ 43.1. Metryki Halsteada 43.2. Złożoność cyklomatyczna a ryzyko 43.3. Złożoność cyklomatyczna a prawdopodobieństwo złej poprawki 43.4. Metryki Lorenza 43.5. Indeks Flescha – interpretacja wyników testu 44.1. Pracochłonność projektu testowego według podziału prac i czynności 44.2. Pracochłonność projektu testowego według podziału systemu 44.3. Rekomendowane wartości współczynnika D 44.4. Przykładowe dane historyczne z poprzednich projektów 44.5. Czynniki modyfikujące dla metody szacowania przez porównanie 44.6. Szacowanie wysiłku i kosztu przez porównanie 44.7. Dane historyczne o wysiłku i punktach funkcyjnych 44.8. COCOMO II – wagi czynników skali w zależności od ich wpływu 45.1. Wykorzystanie modelu fazowego 45.2. Modele zmian w kodzie – porównanie 47.1. Przykładowe dostępności systemu i odpowiadające im czasy niedostępności 47.2. Związek między gęstością defektów i wartością MTTF 49.1. Zebrane od użytkowników dane z ankiet A.1. Wymagania funkcjonalne dla programu ELROJ C.1. Tablica
prawdy
dla
operatorów
i równoważności C.2. Tablica prawdy dla operatora negacji
alternatywy,
koniunkcji,
implikacji
Spis listingów
1.1. Procedura przetwarzania komunikatów w oprogramowaniu AT&T 1.2. Dwa sposoby pobrania ciągu znaków ze strumienia wejściowego 1.3. Przykładowy program z pętlą 2.1. Algorytm sortowania bąbelkowego 2.2. Przykładowy program ilustrujący stopień pokrycia 3.1. Implementacja wysokopoziomowego przypadku testowego 3.2. Implementacja niskopoziomowego przypadku testowego 4.1. Historyjka użytkownika w podejściu BDD 5.1. Kod w C obliczający tablicę prefiksów dla algorytmu Knutha–Morrisa–Pratta 5.2. Fragment kodu z dynamicznym wiązaniem 6.1. Przykład programu z martwym kodem 6.2. Fragment kodu z dwoma definicjami zmiennej x 6.3. Przykład źle napisanego programu w C 6.4. Program obliczający silnię 6.5. Program obliczający silnię z wstawionymi asercjami 6.6. Program zamieniający miejscami wartości dwóch zmiennych 6.7. Kod źródłowy programu grep.c 7.1. Pseudokod programu Collatz 7.2. Zinstrumentowany program Collatz 7.3. Kod z wiszącymi i dzikimi wskaźnikami 7.4. Przeprowadzenie analizy wydajności 7.5. Przykładowy płaski profil programu generowany przez gprof 7.6. Graf wywołań wygenerowany przez gprof 8.1. Program obliczający końcową ocenę z przedmiotu 8.2. Algorytm redukcji liczby testów dla grafu P-S
8.3.
Konfiguracja
środowiska
testowego
dla
wykonania
testu
metody
getDisplayString 8.4. Procedura InParameterOrder dla pokrycia par 9.1. Fragment programu z martwym kodem 9.2. Prosty program 9.3. Program Bisekcja 9.4. Funkcja sprawdzająca przestępność roku 9.5. Funkcja RównanieKwadratowe 9.6. Powtórzony kod programu SortBąbelkowe 9.7. Szybkie potęgowanie 9.8. Wyznaczanie ścieżek pierwszych 9.9. Funkcja CzyPierwsza 9.10. Zdolność metody ścieżek bazowych do wykrywania defektów 9.11. Kod programu TypTrójkąta 9.12. Procedura RównanieKwadratowe napisana w Adzie 9.13. Program i jego pięć przykładowych mutantów 16.1. Fragment kodu html obowiązkowego w responsive web design 17.1. Skrypt PL/SQL z poleceniem ROLLBACK 19.1. Symulacja Monte Carlo szacująca prawdopodobieństwo awarii głównej 30.1. Page Object dla strony www wyszukiwania Google 30.2. Test z wykorzystaniem Page Object Pattern 30.3. Skrypt w Pythonie dla nagranego przypadku testowego 30.4. Pseudokod zorganizowanego skryptu 30.5. Skrypt dla testowania opartego na danych 30.6. Przypadki testowe dla systemu ELROJ oparte na słowach kluczowych 30.7. Fragment skryptu napisanego w Perlu 30.8. Skrypt VBScript wyszukujący dane w arkuszu kalkulacyjnym 30.9. Kod w OCL definiujący ograniczenia dla metody calcArea 30.10. Plik XML z rozkładem jazdy dla systemu ELROJ 33.1. Test statystyczny t-Studenta dla czasu naprawy defektów 39.1. Skrypt w pakiecie R budujący model liniowy 43.1. Kod programu w C++ sortującego tablicę 45.1. Skrypt wykorzystujący model Rayleigha do predykcji defektów 46.1. Model Jelinskiego–Morandy – praktyczne wykorzystanie 49.1. Statystyki opisowe dla zebranych danych
Znaki handlowe
CMM ® oraz CMMI® są zarejstrowanymi znakami Carnegie Mellon University. IDEALSM , PSPSM i TSPSM są zarejestrowanymi znakami Instytutu Inżynierii Oprogramowania (SEI), Carnegie Mellon University. EFQM Excellence ModelTM jest zarejestrowanym znakiem handlowym European Foundation for Quality Management. ISTQB® jest zarejestrowanym znakiem International Software Qualifications Board. ITILTM jest zarejestrowanym znakiem Office of Government Commerce. TMM TM jest zarejestrowanym znakiem Illinois Institute of Technology. TMMi ® jest zarejestrowanym znakiem TMMi Foundation. TPI® oraz TPI Next® są zarejestrowanymi znakami Sogeti Nederland B.V.
Testing
Wstęp
Mniej więcej w latach dziewięćdziesiątych XX wieku inżynieria oprogramowania weszła w erę jakości. Jakość, odnosząca się zarówno do produktu, jak i do procesu wytwórczego, stała się celem numer jeden w każdym poważnym projekcie IT. Wszystkie szanujące się firmy przeprowadzają u siebie audyty, aby wykazać zgodność swoich działań z takimi normami jak ISO 9001 czy określić poziom dojrzałości procesów według modelu CMMI (ang. Capability Maturity Model Integration). Powstało wiele metodologii zarządzania jakością, głównie na użytek „klasycznego” przemysłu. Równocześnie, do branży informatycznej przenikały i znalazły poczesne miejsce takie idee jak statystyczna kontrola procesu czy metoda Six Sigma. Jednym ze sposobów zapewniania jakości tworzonych aplikacji jest testowanie oprogramowania. Testowanie jest tak stare jak wytwarzanie oprogramowania i można bez cienia przesady powiedzieć, że istniało od momentu powstania pierwszego programu napisanego na komputerze. Mimo tak szacownej historii testowanie, zwłaszcza w polskich firmach, ciągle pozostaje mało docenianą dziedziną inżynierii oprogramowania. Wciąż można spotkać szefów firm, którzy – chcąc pokazać przyjazne nastawienie ich organizacji wobec wszystkich kandydatów – wyrażają gotowość zatrudniania osób z niewielkim doświadczeniem, oferując im stanowiska testerów. Uważają bowiem, że jest to zawód, który mogą wykonywać osoby bez jakiejkolwiek znajomości testowania. Wynika to z braku świadomości tego, że testowanie jest nie tylko dyscypliną trudną, lecz także odgrywa kluczową rolę w zapewnianiu jakości tworzonego oprogramowania. Taka postawa właścicieli firm produkujących oprogramowanie jest zupełnie niezrozumiała. Gdyby zaproponować im zatrudnienie programisty-amatora,
który dopiero chce się nauczyć profesjonalnego pisania kodu, zaprotestowaliby gwałtownie i odpowiedzieliby zapewne, że nie mogą sobie pozwolić na takie ryzyko, bo to obniżyłoby jakość tworzonego produktu. Paradoksalnie jednak, nie dbając o doświadczenie zatrudnianych testerów, pozwalają sobie na ryzyko o wiele większe, gdyż konsekwencje późnego wykrycia defektów (na skutek nieumiejętnego testowania), po przekazaniu oprogramowania klientowi, są o wiele poważniejsze, niż gdy wykrycie to następuje we wczesnych fazach cyklu życia. Takie podejście kierownictwa często wynika z powierzchownej wiedzy o testowaniu oprogramowania. Wielu menedżerów wyższego szczebla kojarzy proces testowania z praktykantem, który „siedzi, klika i patrzy, czy coś się nie zepsuje”. Nie zdają sobie sprawy, jak rozległą, trudną i skomplikowaną dziedziną jest testowanie. Często nie rozumieją, że efektywne testowanie oprogramowania wymaga wielkiego nakładu pracy, alokacji sporych środków finansowych, sprawnego zarządzania oraz nierzadko umiejętności zastosowania wysoce nietrywialnych technik. Proces testowania jest tak samo trudny, a może nawet trudniejszy, jak proces tworzenia oprogramowania. O znaczeniu testowania może świadczyć powstanie w ostatnich latach różnych organizacji międzynarodowych, takich jak np. ISTQB, które przeprowadzają egzaminy na certyfikowanych testerów, organizują konferencje czy publikują materiały opisujące dobre praktyki w testowaniu. Także na naszym, polskim podwórku pojawia się coraz więcej ciekawych inicjatyw związanych z jakością oprogramowania, np. Mistrzostwa Polski w Testowaniu czy też seria konferencji „Test Well” zainicjowana w 2013 roku. Te ostatnie przykłady pozwalają patrzeć optymistycznie na rozwój społeczności testersko-jakościowej w Polsce, ale trzeba pamiętać, że jest jeszcze wiele do zrobienia. I po to, między innymi, powstała ta książka. Niniejszy podręcznik został napisany dla: początkujących testerów, aby już na samym początku swojej kariery zawodowej poznali dobre praktyki oraz najefektywniejsze techniki i metody testowania, a także aby mogli spojrzeć na tę dziedzinę z lotu ptaka, zobaczyć, z jakich obszarów się składa i jakie są zależności między nimi; zawodowych testerów, aby ugruntowali i poszerzyli swoją wiedzę o zaawansowane techniki testowania;
inżynierów jakości, aby efektywnie wykorzystywali istniejące metody, modele i procesy zapewniania jakości oprogramowania; kierowników, menedżerów i dyrektorów testów, aby sprawniej zarządzali procesem testowym w organizacji i efektywnie go ulepszali; osób doskonalących lub chcących doskonalić proces w organizacji, aby nabyli niezbędną do tego celu wiedzę;
testowy
osób przygotowujących się do egzaminów na certyfikaty ISTQB (poziom podstawowy, zaawansowany i ekspercki), aby dostarczyć im zwarte źródło materiałów pomocnych w nauce do egzaminu; dyrektorów działów IT, aby mogli lepiej zrozumieć rolę testowania w organizacji; pracowników naukowych, którzy prowadzą lub zamierzają prowadzić zajęcia dydaktyczne i/lub badania naukowe w obszarze testowania i jakości oprogramowania; obecnych lub przyszłych pasjonatów testowania, którzy planują związać swój rozwój zawodowy z tą dziedziną inżynierii jakości oprogramowania. Podręcznik ten abstrahuje od konkretnych języków programowania czy narzędzi testowych. Wyjątkiem jest rozdział 30, gdzie podałem przykładowy katalog konkretnych aplikacji wspomagających proces testowania. W książce skupiłem się wyłącznie na metodach i technikach testowania, zarządzania testowaniem oraz zapewniania jakości, dzięki czemu zawarta w niej wiedza jest uniwersalna i przydatna dla każdego testera w każdej organizacji, niezależnie od typu projektu, w jakim uczestniczy, oraz narzędzi, jakich używa. Słowo „narzędzia” w podtytule książki jest związane z abstrakcyjnymi metodami pomocnymi w pracy inżyniera jakości, a nie konkretnymi aplikacjami typu CASE (ang. Compute-Aided Software Engineering) czy CAST (ang. Computer-Aided Software Testing). projekt (ang. project) – zestaw skoordynowanych i kontrolowanych aktywności o określonym czasie rozpoczęcia i zakończenia; jest powoływany, aby osiągnąć cel zgodnie z określonymi wymaganiami, w tym ograniczeniami czasowymi, kosztowymi i zasobowymi [1]
CASE (ang. Computer-Aided Software Engineering) – inżynieria oprogramowania wspomagana komputerowo CAST (ang. Computer-Aided Software Testing) – testowanie oprogramowania wspomagane komputerowo; patrz także: automatyzacja testowania Książka składa się z siedmiu części. Pierwsza jest wprowadzeniem w dyscyplinę testowania oprogramowania. Zawiera niezbędne pojęcia, opisuje proces testowy i wyjaśnia, jak ten proces jest umiejscowiony w różnych cyklach życia oprogramowania. Definiuje także typowe poziomy i typy testów. Część druga to obszerny przegląd technik projektowania testów. Opisane zostały w nim wszystkie najważniejsze oraz najczęściej stosowane techniki: czarnoskrzynkowe, białoskrzynkowe, oparte na defektach i oparte na doświadczeniu. Omówione są również techniki statyczne, takie jak przeglądy czy analiza statyczna, oraz metody analizy dynamicznej. W części trzeciej zostały opisane zagadnienia związane z testowaniem niefunkcjonalnym, czyli testowaniem cech jakościowych. Materiał tej części w dużym stopniu jest oparty na normie ISO/IEC/IEEE 25010 [2], która zastąpiła kilka lat temu normę ISO/IEC 9126 [3]. Część
czwarta
jest
przeznaczona
głównie
dla
osób
zajmujących
się
testowaniem od strony zarządzania, takich jak: kierownicy testów, menedżerowie jakości, kierownicy zespołów testowych. Omówione są w niej techniki zarządzania z naciskiem na metody oparte na ryzyku. Zawiera także przegląd dokumentacji testowej zgodny z normą ISO/IEC 29119-3 [4], która zastąpiła dotychczas stosowaną normą IEEE 829 [5]. Część piąta jest poświęcona ludziom i narzędziom w procesie testowym. Przedstawione są w niej zagadnienia komunikacji w zespole, kompetencji oraz motywacji. Zawiera także szeroki przegląd typów narzędzi wraz z konkretnymi przykładami ich zastosowań. Część szósta dotyczy metod doskonalenia procesu testowego. Jest przeznaczona głównie dla menedżerów, osób odpowiedzialnych w firmie za usprawnianie procesu testowego oraz dla kandydatów przygotowujących się do egzaminu ISTQB Expert Level – Test Process Improvement. Część siódma jest poświęcona zagadnieniom jakości oprogramowania: metrykom i modelom stosowanym w inżynierii jakości.
W książce, oprócz tekstu podstawowego, występują dwa inne rodzaje informacji: definicje oraz listingi (kody źródłowe): definicja – definicja pojęcia (wraz z jego angielskim odpowiednikiem) jest opisana na szarym tle; większość definicji jest podana za Słownikiem wyrażeń związanych z testowaniem pod redakcją Lucjana Stappa, opublikowanym przez Stowarzyszenie Jakości Systemów Informatycznych [6]
Kod źródłowy lub pseudokod programu – napisany czcionką Courier New, między dwiema poziomymi liniami oznaczającymi początek i koniec kodu. Linie kodu mogą być numerowane w celu odwołania się do nich w tekście. Nazwy zmiennych, funkcji oraz programów w zwykłym tekście są oznaczone kursywą. Fragmenty kodu w tekście są podane, podobnie jak listingi, czcionką maszynową (Courier New). Wartości logiczne są opisywane albo słownie, jako prawda i fałsz, albo za pomocą liczb 1 i 0, pogrubioną czcionką. Książka zawiera cztery dodatki. W Dodatku A został opisany system ELROJ (ELektroniczny ROzkład Jazdy), który będzie przykładem przewodnim służącym do ilustracji wielu metod i technik testowania. Dodatek ten zawiera opis systemu, specyfikację wymagań oraz fragment projektu architektury. Zaleca się, aby Czytelnik przed lekturą właściwej części książki zapoznał się z opisem tego systemu. W Dodatku B zebrano wszystkie najważniejsze normy i standardy bezpośrednio lub pośrednio związane z testowaniem i jakością oprogramowania. Dodatek C zawiera formalne definicje pojęć matematycznych oraz definicje z zakresu teoretycznych podstaw informatyki, które są wykorzystywane w książce. W Dodatku D zostały opisane najbardziej znane i cenione w środowisku testerów programy certyfikacji na testerów lub inżynierów jakości.
Część I Podstawy testowania
Początek to najważniejsza część pracy Platon
W części pierwszej wprowadzono podstawowe, niezbędne pojęcia z zakresu testowania oprogramowania, które będą wykorzystywane w całej książce. W rozdziale 1 opisano rolę i doniosłość procesu testowania w organizacji. Konsekwencje jego braku lub niedoskonałości są zilustrowane słynnymi przykładami awarii oprogramowania. Wprowadzono formalną definicję testowania oraz rys historyczny tej dyscypliny. Omówiono siedem podstawowych zasad testowania, a także – nie mniej ważne – kwestie psychologiczne oraz ekonomiczne związane z czynnościami testowymi. W rozdziale 2 wprowadzono niezbędne definicje związane z testowaniem. Pojęcia te występują w dalszej części podręcznika, dlatego Czytelnik powinien się z nimi dobrze zaznajomić. Rozdział 3 zawiera opis dwóch przykładowych procesów testowych: Podstawowego Procesu Testowego proponowanego przez ISTQB oraz procesu zdefiniowanego w normie ISO/IEC/IEEE 29119 Software Testing Standard. Procesy te są dosyć podobne i cała treść książki odnosząca się do zagadnień testowania (części I– VI) jest wyłożona zgodnie z tymi procesami. W rozdziale 4 omówiono testowanie w cyklu życia oprogramowania. Przedstawiono najpopularniejsze modele cyklu życia z uwzględnieniem roli ich testowania w tych modelach. W drugiej części tego rozdziału opisano poziomy oraz typy testów, które zwyczajowo przyjęło się stosować w branży IT.
Więcej na: www.ebook4all.pl
1. Wprowadzenie do testowania
1.1. Dlaczego testowanie jest niezbędne Komputery i systemy informatyczne stały się nieodzownym elementem naszego codziennego życia. Oprogramowanie – czy to w postaci aplikacji, czy też jako systemy wbudowane – można znaleźć prawie w każdym urządzeniu, poczynając od rakiety kosmicznej, a skończywszy na pralce czy kuchence mikrofalowej. Wszyscy nieustannie korzystamy z takich urządzeń, dlatego każdy z nas znalazł się wielokrotnie w sytuacji, w której używany przez niego sprzęt lub program komputerowy nie działał tak, jak powinien. Takie sytuacje nie są dla użytkownika komfortowe. Tracimy przez nie czas (ponowne uruchomienie komputera) i pieniądze (zakup nowego sprzętu po awarii). O ile niektóre objawy źle działającego oprogramowania mogą powodować jedynie irytację czy złość wywołaną np. koniecznością ponownego uruchomienia aplikacji, o tyle błędy w systemach o znaczeniu krytycznym mogą doprowadzić nawet do utraty zdrowia czy życia. Przykłady takich błędów zostały opisane w podrozdziale 1.3. system (ang. system) – zbiór modułów zorganizowany w celu osiągnięcia zadanej funkcjonalności [7] system krytyczny ze względów bezpieczeństwa (ang. safety critical system) – system, którego awaria lub nieprawidłowe działanie może skutkować śmiercią lub poważnymi obrażeniami ludzi, a także utratą uszkodzeniami urządzeń lub zanieczyszczeniem środowiska
Główną przyczyną awarii oprogramowania są tkwiące w nim usterki, które z kolei wynikają z pomyłek programistów. Programiści są tylko ludźmi, a każdy człowiek popełnia błędy, z wielu różnych przyczyn. Skomplikowany i złożony kod, praca pod presją czasu, wykorzystanie zaawansowanej technologii, duża interakcja między tworzonymi systemami czy po prostu zwykłe zmęczenie – każdy z tych czynników może stać się przyczyną popełnienia błędu. Awarie oprogramowania mogą występować również na skutek czynników niezależnych od ludzi. Na przykład wielu z nas spotkało się z sytuacją, w której nasz telefon komórkowy przestał działać na skutek czynników atmosferycznych (niska temperatura). Inne tego typu powody to pole magnetyczne, zanieczyszczenie środowiska, czy uszkodzenia mechaniczne sprzętu. Praktycznie każdy nietrywialny program komputerowy zawiera usterki. Jak już wspomnieliśmy, usterki te mogą mieć bardzo poważne konsekwencje. Profesjonalne, dobrze zaplanowane testowanie systemów oraz dokumentacji pozwala w znacznym stopniu zmniejszyć ryzyko awarii systemu, jeśli znalezione usterki zostaną usunięte w procesie debugowania. Często zdarza się, że testowanie jest wręcz wymogiem zawartym w kontrakcie na tworzenie aplikacji lub wynikającym z przepisów prawnych. Może też być obowiązkiem narzuconym przez normę lub standard przemysłowy, który jest stosowany w procesie budowy oprogramowania. Na przykład norma DO-178C1 „Software Considerations in Airborne Systems and Equipment Certification” [8], stosowana m.in. przy produkcji oprogramowania do awioniki, wymaga wprost zastosowania odpowiednich technik testowania kodu oraz kryteriów pokrycia, w zależności od poziomu krytyczności tworzonego systemu. Testowanie wpływa również bezpośrednio na jakość tworzonego produktu. Dobrze zdefiniowane metryki i modele stosowane podczas procesu testowego pozwalają w sposób ilościowy wyrazić jakość oprogramowania, mierząc np. liczbę znalezionych usterek, odnieść ich liczbę do przewidywanej łącznej liczby usterek ukrytych w kodzie, czy zmierzyć poziom obecnego wciąż w systemie ryzyka (tzw. ryzyko rezydualne). Dzięki tak przeprowadzonym pomiarom testowanie może służyć jako czynnik budujący zaufanie do tworzonego kodu: jeśli zespół testowy nie znajduje więcej usterek, lub częstotliwość ich znajdowania jest bardzo niska, może oznaczać to, że system ma wysoką jakość i jest gotowy do przekazania klientowi. Należy jednak pamiętać, że budowanie zaufania do kodu możliwe jest tylko wtedy, kiedy proces testowania przebiega prawidłowo
i zgodnie z zasadami. Tworzone przypadki testowe muszą być zaplanowane, zaprojektowane i wykonane poprawnie. W skrajnym bowiem przypadku, gdy zespół testowy nie wykona ani jednego testu i liczba znalezionych usterek będzie oczywiście równa zeru, nie oznacza to, że jakość tak „przetestowanego” systemu jest wysoka!
1.2. Definicja testowania W powszechnym rozumieniu testowanie jest kojarzone głównie z wykonywaniem testów, czyli z uruchamianiem oprogramowania. Jest to jednak tylko jeden z wielu etapów składających się na cały proces testowania. Zarówno przed, jak i po wykonaniu testów występuje wiele innych czynności związanych z testowaniem, takich jak planowanie, nadzór, kontrola, wybór warunków testowych, projektowanie i wykonanie przypadków testowych, sprawdzanie wyników, ocena spełnienia kryteriów zakończenia, raportowanie czy czynności zamykające fazę testowania. Wszystkie te etapy składają się na Podstawowy proces testowy omówiony w rozdziale 3. Istnieje wiele różnych definicji testowania. Według Myersa ([9], testowanie to
„proces
wykonywania
programu
lub
systemu
z
[10]), intencją
znajdowania w nim błędów”. Definicja ta nie uwzględnia technik statycznych testowania oraz tego, że testowaniu powinny podlegać wszystkie artefakty wytwarzane w procesie produkcji, a więc nie tylko kod, ale też takie dokumenty, jak wymagania czy projekt architektury systemu. Skupiając się wyłącznie na znajdowaniu defektów, nie uwzględniamy kwestii szerzej pojętej jakości i zaufania do kodu. Co więcej, przyjęcie tej definicji oznacza, że proces testowania może rozpocząć się dopiero po tym, gdy kod zostanie napisany. Hetzel [11] przez testowanie rozumie każdą czynność nakierowaną na sprawdzanie atrybutów i możliwości programu lub systemu oraz weryfikację tego, czy testowany system spełnia założone wymagania. Definicja ta bierze pod uwagę aspekt jakościowy tworzonego systemu. Norma IEEE 610 [7], będąca słownikiem pojęć używanych w inżynierii oprogramowania, podaje dwie definicje testowania. Pierwsza mówi, że testowanie to proces (lub przeprowadzanie procesu) obsługi systemu lub komponentu, w określonych warunkach, obserwowania lub nagrywania rezultatów działania i ewaluacji jego cech. Druga definicja odwołuje się z kolei do
normy IEEE 829 [5], która określa testowanie jako proces analizy elementu oprogramowania
w celu wykrycia
różnic (usterek)
między
istniejącymi
a wymaganymi warunkami oraz oceny cech tego elementu. Miller [12] z kolei pisze, że głównym celem testowania jest potwierdzenie jakości testowanego systemu przez systematyczne wykonywanie go w kontrolowanych warunkach. Sylabus ISTQB [13] wskazuje następujące cele testowania: znajdowanie usterek, nabieranie zaufania do poziomu jakości, dostarczanie informacji potrzebnych do podejmowania decyzji, zapobieganie defektom. Jest to dosyć dobra charakterystyka celów testowania, chociaż o ile dwa pierwsze cele są jasne i zrozumiałe, o tyle dwa ostatnie wymagają komentarza. Cel trzeci nie jest sformułowany wystarczająco precyzyjnie – nie jest on charakterystyczny wyłącznie dla testowania. Należałoby tu dodać, że informacje, o których mowa w tym celu, dotyczą właściwości testowanego oprogramowania, takich jak: liczba znalezionych błędów, odsetek wymagań pokrytych testami czy aktualny poziom jakości. Cel czwarty – zapobieganie defektom – pochodzi z klasycznej książki Beizera [14]. Twierdzi on, że sama czynność projektowania testów jest najlepszą metodą zapobiegania defektom. Część ekspertów uznaje testowanie wyłącznie za czynność „destrukcyjną”, nastawioną na znajdowanie usterek, a nie na zapobieganie im. Zapobieganie defektom odbywa się na ogół nie tylko przez sam proces projektowania, lecz także przez analizę statyczną – np. przeglądy formalne wymagań – zanim jeszcze rozpocznie się pisanie kodu. Niektórzy autorzy nie zaliczają metod statycznych do repertuaru technik testowania, ale wydaje się, że stanowią oni mniejszość. testowanie, ewaluacja (ang. testing, evaluation) – proces złożony z wszystkich czynności cyklu życia, zarówno statycznych, jak i dynamicznych, skoncentrowany na planowaniu, przygotowaniu i ewaluacji oprogramowania oraz powiązanych produktów w celu określenia, czy spełniają one wyspecyfikowane wymagania, wykazania ich dopasowania do założonych celów oraz wykrywania usterek Każdy z powyższych celów można odnieść do różnych etapów i celów projektu tworzenia oprogramowania. Na przykład podczas początkowej fazy testów
zwykle zależy nam na tym, aby znaleźć możliwie największą liczbę usterek. W końcowych fazach testowania, np. podczas testów akceptacyjnych, głównym celem może być nabranie zaufania do oprogramowania, przez sprawdzenie, czy spełnia wymagania i czy działa tak, jak powinien. Testowanie produkcyjne zwykle ocenia jakościowe cechy oprogramowania, takie jak funkcjonalność, bezpieczeństwo, niezawodność czy dostępność. Czasami testowanie nie ma na celu usuwania usterek, a jedynie ocenę jakości oprogramowania i dostarczenie interesariuszom informacji o ryzyku związanym z użytkowaniem systemu. Takie działanie może występować np. w sytuacji, gdy musimy zintegrować nasz system z innym, zewnętrznym systemem dostępnym w kilku różnych wersjach. Testowanie zewnętrznego systemu pozwoli ocenić jakość każdej z wersji i wybrać tę obarczoną najmniejszym ryzykiem lub tę, która spełnia nasze wymagania w największym stopniu. Testowanie skupiające się na tym, aby wykazać, że oprogramowanie nie działa, jest nazywane testowaniem negatywnym. Jest ono bardziej związane z postawą testerów niż z konkretnym podejściem czy techniką testowania. Często wykorzystuje np. nieprawidłowe dane wejściowe lub wymusza występowanie wyjątków podczas działania aplikacji. testowanie negatywne, brudne testowanie (ang. negative testing, dirty testing) – testowanie, którego celem jest pokazanie, że oprogramowanie nie działa poprawnie [14] debugowanie (ang. debugging) – proces znajdowania, izolowania, analizowania i usuwania przyczyn awarii obsługa wyjątków (ang. exception handling) – zachowanie modułu lub systemu w odpowiedzi na błędne wejście wprowadzone przez użytkownika lub inny moduł bądź system zachowanie (ang. behavior) – odpowiedź modułu lub systemu na zestaw wartości wejściowych i warunków wstępnych Przy okazji testowania wypada również wspomnieć o debugowaniu, gdyż są to pojęcia bardzo często mylone lub utożsamiane ze sobą. Debugowanie jest zupełnie inną czynnością niż testowanie i nie ma z nim nic wspólnego [15]. Jest niejako
czynnością „odwrotną”, czy też komplementarną do testowania: o ile testowanie służy do wykazywania obecności awarii w testowanym programie, o tyle debugowanie jest procesem usuwania defektów będących źródłem tych awarii, czyli naprawy wadliwego oprogramowania. Debugowanie jest oddzielną dyscypliną inżynierii oprogramowania. Efektywne debugowanie jest zwykle bardzo trudne. Teoria debugowania ma bardzo wiele wyspecjalizowanych, skomplikowanych narzędzi służących do szybkiej analizy kodu oraz izolacji miejsca zawierającego usterkę (patrz np. [16]).
1.3. Słynne przykłady awarii oprogramowania W tym podrozdziale przedstawimy słynne przykłady awarii oprogramowania. Niektóre są śmieszne, inne kosztowne, jeszcze inne – niestety – tragiczne w skutkach. Każdy z nich stanowi bardzo pouczającą lekcję mówiącą o tym, jak ważne jest testowanie i jak poważne skutki niesie za sobą jego brak lub niedbałe wykonanie. Przy każdym przykładzie zaznaczymy, jakie techniki testowania mogły być zastosowane tak, aby uniknąć awarii. Techniki te zostaną omówione dokładnie w rozdziałach 6–10. „Król Lew” Disneya. W 1994 roku Disney wydał na święta Bożego Narodzenia grę „Król Lew” na podstawie filmu o tym samym tytule. W owym czasie był to wielki hit kinowy i Disney liczył również na duże zyski ze sprzedaży gry. Była ona wydana w porozumieniu z Compaq Computers jako pre-instalowana aplikacja na laptopach firmy. Dzień po Wigilii centrum wsparcia technicznego Disneya zostało zasypane lawiną telefonów od zdenerwowanych klientów. Gra nie chciała się uruchomić na komputerach większości klientów. Okazało się, że nie została przetestowana na najbardziej popularnych konfiguracjach sprzętowych, czyli nie została wystarczająco przetestowana pod kątem przenaszalności. Można było użyć tu takich technik jak drzewa klasyfikacji czy testowanie kombinacyjne, np. testowanie par. Awaria ta spowodowała znaczne szkody wizerunkowe popularnej wytwórni. Rakieta Ariane 5. 4 czerwca 1996 roku Europejska Agencja Kosmiczna wystrzeliła rakietę Ariane 5. Po 39 sekundach lotu oprogramowanie rakiety uruchomiło procedurę samozniszczenia ze względu na niespodziewaną zmianę trajektorii lotu. Przyczyną błędu była konwersja liczby 64-bitowej na liczbę 16bitową [17], co spowodowało tzw. błąd operandu. Konwertowana liczba była
związana z wartością prędkości horyzontalnej i w poprzedniej wersji rakiety, Ariane 4, działała bez zarzutu. Niestety, w przypadku Ariane 5, wartość tej zmiennej mogła uzyskiwać o wiele wyższe wartości na skutek innej niż w Ariane 4 trajektorii lotu. Większość instrukcji była chroniona przed błędami przekroczenia wartości, jednak w tym przypadku to nie nastąpiło. Powody tego faktu są nieznane. Koszt całej misji to 500 milionów dolarów. Błąd można było wykryć, stosując statyczne techniki testowania, takie jak przeglądy kodu oraz techniki dynamicznego testowania opartego na profilu operacyjnym (czyli oczekiwany sposób użycia oprogramowania przez użytkownika). Awaria sieci AT&T. 15 stycznia 1990 roku nastąpiła wielka awaria sieci telekomunikacyjnej w Stanach Zjednoczonych. Od godziny 2:25 w nocy centrum operacyjne AT&T w Bedminster zaczęło odbierać komunikaty ostrzegawcze z różnych części sieci. Awaria postępowała błyskawicznie, infekując lawinowo stacje przekaźnikowe. Problemem okazał się defekt w kodzie mającym usprawniać szybkość przetwarzania komunikatów. Program wyglądał mniej więcej tak jak na listingu 1.1.
1 while (bufor odbieranych połączeń niepusty and bufor pomocniczy niepusty) do 2 3
inicjalizuj wskaźnik na pierwszy komunikat w buforze pomocniczym lub buforze połączeń pobierz kopię bufora
4
switch (komunikat)
5 6 7 8 9 10 11 12 13 14 15
case (incoming_message): if (przekaźnik nie działa) then if (bufor zapisu pusty) then wyślij komunikat “in service” else break end if end if przetwórz przychodzący komunikat, ustaw wskaźniki na opcjonalne parametry break end switch
16
przetwórz opcjonalne parametry
Listing 1.1. Procedura przetwarzania komunikatów w oprogramowaniu AT&T Gdy docelowy przekaźnik odbierał drugą spośród nadchodzących po sobie w bardzo krótkim czasie wiadomości, mógł być jeszcze zajęty przetwarzaniem pierwszej (bufor zapisu niepusty w linii 7.). W takim przypadku program miał opuścić ciało instrukcji if (linie 7.–8.) i przejść do linii 12., gdzie komunikat był przetwarzany, a wskaźniki miały być aktualizowane w bazie. Niestety, instrukcja break w linii 10. powodowała opuszczenie ciała całej instrukcji switch-case i przechodziła do przetwarzania parametrów opcjonalnych (linia 16.), co powodowało nadpisywanie danych w bazie. Oprogramowanie korygujące błędy wykrywało fakt nadpisania informacji w bazie i wyłączało przekaźnik, aby mógł zostać zresetowany. Wszystkie przekaźniki działały na tym samym oprogramowaniu, dlatego resetowanie urządzeń następowało powodując globalną awarię całej sieci.
kaskadowo,
bufor (ang. buffer) – urządzenie lub pamięć używana do czasowego przechowywania danych koniecznego w wyniku różnic czasowych w przepływie danych lub pojawienia się różnej ilości danych, jaką mogą przyjąć urządzenia lub procesy zaangażowane w przesyłanie lub wykorzystywanie tych danych [7] Koszt tej awarii szacuje się na 60 milionów dolarów. Obecnie większość kompilatorów ma mechanizmy, które podczas kompilacji wykonują elementy analizy statycznej kodu i potrafią ostrzec przed „podejrzanym” użyciem instrukcji break. Problem mógłby również zostać rozwiązany za pomocą inspekcji formalnych czy przeglądów kodu. kompilator (ang. compiler) – narzędzie tłumaczące program napisany w języku wysokiego poziomu na jego odpowiednik w języku maszynowym [7] Błąd przepełnienia w procesie dla protokołu finger. W 1988 roku Robert Morris stworzył pierwszego w historii robaka internetowego (worm), który – jak podają różne źródła – zainfekował od dwóch do sześciu tysięcy komputerów. Robak wykorzystywał trzy techniki ataków, przy czym tą, która odniosła
największy sukces w procesie infekowania była metoda przepełnienia bufora. Unixowy proces finger wykorzystywał polecenie gets, które pobiera ciąg znaków ze strumienia wejściowego. Pobranie ciągu wejściowego może być dokonane na dwa sposoby:
char * fgets(char * restrict str, int size, FILE * restrict stream); char * gets(char *str); Listing 1.2. Dwa sposoby pobrania ciągu znaków ze strumienia wejściowego W drugim przypadku argument funkcji gets nie ma ograniczenia na długość łańcucha str. Zbyt długi łańcuch wejściowy może spowodować przepełnienie bufora, co powoduje nadpisanie stosu i możliwość przejęcia kontroli nad komputerem. Testowanie wartości brzegowych, uwzględniające ekstremalne wartości i długości używanych zmiennych, zwykle bywa bardzo skuteczne w wykrywaniu tego typu problemów. Dobre rezultaty dadzą też metody testowania oparte na doświadczeniu lub na defektach (tzw. atak usterkowy). przepełnienie bufora (ang. buffer overflow) – wyjątek dostępu do pamięci na skutek usiłowania umieszczenia przez proces danych poza granicami wyznaczonego bufora. W rezultacie zostają nadpisane sąsiednie obszary pamięci; patrz także: bufor Błąd w MS Excel 2007. 23 września 2007 roku na grupie dyskusyjnej poświęconej Excelowi użytkownik Molham Serry zgłosił błąd polegający na tym, że jeżeli w Excelu wykona się mnożenie 850 ⋅ 77,1, to zamiast oczekiwanego wyniku 65 535 Excel zwraca... 100 000. Okazało się, że problem dotyczył tylko sposobu prezentacji wartości – Excel obliczał działanie poprawnie, ale przy prezentowaniu wyniku na ekran wypisywał błędną wartość na skutek defektu w kodzie formatującym wypisywane wartości. Zwrócona wartość 65 535 jest największą liczbą całkowitą, jaką można zapisać w zmiennej 16-bitowej. Prawdopodobnie kod formatujący wyświetlany wynik miał jakiś związek z tym faktem. Do wykrywania tego typu usterek jest przydatna analiza wartości brzegowych dla danych wyjściowych. To pouczający przykład błędu, gdyż często zapomina się, że techniki projektowania
testów można stosować nie tylko przez analizę i wykorzystanie danych wejściowych, lecz także przez wymuszenie zwrócenia przez program konkretnych wartości. Generator liczb losowych w systemie Kerberos. Kerberos to protokół uwierzytelniania i autoryzacji. Używa algorytmów kryptograficznych, więc wykorzystuje również generator liczb losowych. Okazało się, że przez dziewięć lat, od 1988 do 1996 roku, protokół ten wykorzystywał generator losowy z błędnie ustawionym tzw. ziarnem (ang. seed), czyli początkową wartością, na której podstawie był później generowany ciąg liczb losowych. W rezultacie, w sposób trywialny można było złamać zabezpieczenia systemu. Tego typu błąd najczęściej można odkryć podczas inspekcji formalnych oraz innych technik analizy statycznej kodu. Sonda kosmiczna Mariner I. 22 lipca 1962 roku NASA wystrzeliła sondę kosmiczną Marine I, mającą za zadanie zebrać informacje na temat planety Wenus. 294 sekundy po starcie oficer bezpieczeństwa zadecydował o zdalnym zniszczeniu sondy ze względu na niestabilną trajektorię lotu, co groziło upadkiem rakiety na Ziemię i spowodowaniem dużych zniszczeń lub nawet śmierci ludzi (dodatkowego smaczku całej historii dodaje fakt, że sygnał o zniszczeniu można było wysłać najpóźniej do 5 minut po starcie sondy. Gdyby oficer zwlekał z podjęciem decyzji 6 sekund dłużej, kto wie, jakie sonda spowodowałaby zniszczenia). Tak naprawdę do końca nie wiadomo, co było bezpośrednią przyczyną awarii. Większość źródeł mówi o symbolu kreski (łącznika), który został błędnie przepisany do kodu źródłowego z kartki zawierającej równania matematyczne wykorzystywane w oprogramowaniu, przez co prasa okrzyknęła całą historię „najdroższym łącznikiem w historii”. W astronautyce symbol kreski nad zmienną oznacza operację wygładzania. Omyłka, polegająca na opuszczeniu tego symbolu, spowodowała, że w programie wartości jednej ze zmiennych nie były wygładzane, powodując duże skoki wartości, co skutkowało zaburzeniem toru lotu rakiety wynoszącej sondę. Najlepszą techniką do wykrywania tego typu błędów są wszelkiego rodzaju inspekcje i przeglądy kodu. Mars Climate Orbiter to sonda NASA, która miała służyć do badania pogody i klimatu Marsa. Wystrzelono ją w 1998 roku, ale podczas dokonywania manewru wejścia na orbitę sonda znalazła się na złej wysokości, po czym stracono z nią kontakt i efekt projektu o łącznej wartości 125 milionów dolarów spłonął w atmosferze Marsa. Przyczyną awarii było stosowanie różnych systemów
metrycznych przez różne zespoły pracujące nad oprogramowaniem sondy. Oprogramowanie stosowane przez kontrolę naziemną stosowało dla parametru siły jednostki anglosaskie (funty), podczas gdy oprogramowanie sondy używało jednostek SI (niutonów). Tego typu błędy można wykryć podczas testów integracyjnych, których najprawdopodobniej nie wykonano. Dobrą praktyką jest również ścisłe trzymanie się ustalonych standardów (np. rodzaju systemu metrycznego wykorzystywanego w projekcie). Po tym wypadku NASA wprowadziła regulacje dotyczące takiej standaryzacji. Therac-25 to maszyny do terapii nowotworów stosowane w latach 80. Między 1985 a 1987 rokiem urządzenia te były przyczyną serii tragicznych wypadków. W wyniku podania pacjentom dawek promieniowania wyższych o kilka rzędów wielkości niż dawki dopuszczalne śmierć poniosło 6 osób. Przyczyną nieprawidłowego funkcjonowania urządzeń był tzw. wyścig (ang. race condition). W przypadku Therac-25 polegał on na tym, że gdy operator maszyny obsługiwał ją w szybkim tempie, nie wszystkie parametry programu były właściwie inicjalizowane. Późniejsze dochodzenie wykazało także poważne usterki w dokumentacji, która była niekompletna i w szczególności nie zawierała opisu kodów błędów zgłaszanych przez maszynę. Cobalt 60. Historia bardzo podobna do sprawy Therac-25 (i równie tragiczna w skutkach) zdarzyła się na początku XX wieku w National Cancer Institute w Panamie. Centrum to używało maszyn Cobalt 60 do radioterapii. Na skutek błędów w oprogramowaniu co najmniej ośmioro pacjentów zmarło z powodu przyjęcia zbyt wysokiej dawki promieniowania. Oprogramowanie pozwalało użytkownikowi na rysowanie na ekranie do czterech „tarcz”, czyli obszarów ciała pacjenta, które były chronione przed promieniowaniem w trakcie zabiegu. Technicy potrzebowali jednak definiować pięć takich tarcz. Poradzili sobie z tym w ten sposób, że reprezentację pięciu tarcz definiowali w programie przez rysowanie jednej dużej tarczy z otworem w środku. Niestety, sposób interpretacji przez program tego, co jest tarczą, a co otworem, zależał od sposobu rysowania. Gdy otwór był rysowany zgodnie z ruchem wskazówek zegara, program interpretował to inaczej, niż gdy był rysowany w przeciwnym kierunku. To powodowało, że w niektórych przypadkach zamiast podawać dawkę promieniowania w miejsce otworu, nakierowane było ono na całe ciało pacjenta z wyjątkiem tego obszaru. Tego typu błędy są trudne do wykrycia. Odpowiednią strategią mogłoby tu być testowanie GUI przy zastosowaniu technik opartych na
doświadczeniu, takich jak testowanie eksploracyjne czy zgadywanie błędów. Skuteczne stosowanie tych technik wymaga dostępności doświadczonego testera.
1.4. Rys historyczny Testowanie jest tak stare, jak stary jest pierwszy napisany program komputerowy. Na przestrzeni ostatnich 50 lat dojrzałość procesu testowania bardzo wzrosła. Zostało to wymuszone głównie czynnikami technologicznymi: rozwojem języków czwartej generacji (4GL), wzrostem roli utrzymania i aktualizacji oprogramowania, wynalezieniem metod formalnych inżynierii oprogramowania, powstaniem nowych metodyk wytwarzania oprogramowania itd. Podczas tych kilkudziesięciu lat zmieniało się także podejście do testowania oraz rozumienie jego roli (widać to zresztą wyraźnie, gdy porówna się definicje testowania opisane w rozdziale 1.2). Dalej przedstawiamy główne fazy rozwoju testowania oraz najważniejsze odkrycia w historii tej dziedziny, zarówno teoretyczne, jak i techniczne. Treść tego podrozdziału jest oparta na pracy Luo [18], która odwołuje się do artykułu Gelperina i Hetzela [19] opisującego rozwój testowania na przestrzeni kilkudziesięciu ostatnich lat. Faza pierwsza (przed rokiem 1956). Era debugowania. Przed 1956 rokiem testowanie nie było odróżniane od debugowania – były to pojęcia tożsame. W 1950 roku Turing opublikował swój słynny artykuł [20], w którym zadał pytanie o to, jak stwierdzić, że maszyna jest obdarzona inteligencją. Jeśli wymaganiem byłoby zbudowanie takiego inteligentnego programu, to pytanie Turinga można by przeformułować na pytanie o zgodność programu z wymaganiami. Test zaproponowany przez Turinga, pozwalający odróżnić maszynę od myślącego człowieka, jest chyba pierwszym w historii informatyki przykładem formy testowania funkcjonalnego. W latach pięćdziesiątych XX w. pojęcia: sprawdzania, debugowania i testowania programu nie były jeszcze jasno rozróżniane. Faza druga (lata 1957–1978). Era dowodzenia poprawności. W 1957 roku Charles Baker w recenzji książki Dana McCrackena o programowaniu komputerów odróżnił debugowanie od testowania. Sprawdzanie programu (ang. program checkout) miało dwa cele: upewnić się, że program działa oraz upewnić się, że program poprawnie rozwiązuje postawiony przed nim problem. Baker uznał cel pierwszy za domenę debugowania, a cel drugi – za domenę testowania. Sformułowanie „upewnić się” odnosiło się do postrzegania testowania jako
procesu, który ma na celu wykazać (udowodnić), że program poprawnie realizuje swoje funkcje. Lata 70. XX w. to także ogromny rozwój technik, które dziś nazywamy testowaniem pokrycia ścieżek. Nurt ten wypływał z idei, aby oprogramowanie testować wyczerpująco (ang. exhaustively). Faza trzecia (lata 1979–1982). Era dowodzenia niepoprawności. Okres ten wiąże się z publikacją w 1979 roku klasycznej już książki Myersa [10], z której pochodzi definicja testowania jako „procesu wykonywania programu z intencją znajdowania błędów”. Po raz pierwszy zaczęto postrzegać „niszczycielski” charakter testowania. Podkreślano, że przypadek testowy ma większą wartość, gdy jego wykonanie skutkuje wykryciem błędu. Przed 1979 rokiem testerzy mogli tworzyć, zupełnie nieświadomie, przypadki testowe, które cechowały się niskim prawdopodobieństwem wykrycia błędu. Po wydaniu podręcznika Myersa sytuacja diametralnie się zmieniła: mając na uwadze wykrywanie błędów jako podstawowy cel testowania, testerzy skupili się na konstruowaniu zestawów testów (zwanych również suitami testowymi), które były zdolne do wykrywania usterek z dużym prawdopodobieństwem. Ten paradygmat testowania spowodował, że zauważono, jak ważne jest rozpoczynanie czynności testowych już od najwcześniejszych faz cyklu życia oprogramowania. Takie podejście do testowania najpełniej chyba uwidacznia się w modelu V, który omówimy w punkcie 4.1.2. Faza czwarta (lata 1983–1987). Era ewaluacji. W 1983 roku Institute for Computer Sciences and Technology of the National Bureau of Standards opublikował dokument zatytułowany „Guideline for Lifecycle Validation, Verification, and Testing of Computer Software”. Została w nim opisana metodologia oceny oprogramowania w trakcie całego cyklu życia. Metodologia ta obejmowała i integrowała ze sobą analizę, przeglądy i czynności testowe. Żywiono przekonanie, iż precyzyjnie dobrany zbiór technik walidacji, weryfikacji i testowania pozwoli na wyprodukowanie oraz bezproblemowe utrzymywanie wysokiej jakości systemów. Pojęcia walidacji i weryfikacji zostały później zaadaptowane przez społeczność inżynierów jakości i funkcjonują do dziś. Faza piąta (od 1988 roku). Era zapobiegania. W 1990 roku Beizer opublikował książkę „Software Testing Techniques” [14], uznawaną za jedną z najważniejszych w historii testowania oprogramowania. Zawiera ona wiele technik projektowania testów. Beizer napisał w niej, że „czynność tworzenia testów jest
jedną z najefektywniejszych metod zapobiegania usterkom”. To sformułowanie rozszerzyło definicję testowania, które od tego momentu było ukierunkowane nie tylko na wykrywanie, lecz także zapobieganie defektom. Beizer opisuje cztery etapy myślenia o testowaniu: 1) testować, by oprogramowanie działało, 2) testować, by zepsuć oprogramowanie, 3) testować, by zredukować ryzyko, 4) testowanie jako „dyscyplina umysłowa” (ang. mental discipline), tzn. zapewnianie testowalności oprogramowania w ciągu całego cyklu życia. O ile podejście w fazie czwartej skupiało się na analizie i technikach przeglądu innych niż testowanie, o tyle podejście w fazie piątej jako głównych narzędzi używa planowania, analizy i projektowania testów. Książkę Beizera można uznać za tę, która ufundowała paradygmat wczesnego testowania opartego na ryzyku. O tym, że jest to paradygmat ważny i skuteczny, niech świadczy fakt, że funkcjonuje on z powodzeniem do dziś jako jedna z podstawowych zasad testowania oprogramowania. W tabeli 1.1 przedstawiono teorie, metody i techniki, które miały największy wpływ na rozwój testowania (część z nich jest podana za [18]). Tabela 1.1. Najważniejsze odkrycia w historii testowania oprogramowania
Rok Odkrycie 1975
Wybór tes tów na pods tawie analizy krawędzi g rafu przepływu s terowania
1976
Wybór tes tów na pods tawie analizy ś cieżek w g rafie przepływu s terowania
1980 S trateg ia oparta na dziedzinie (ang . domain strategy) 1980 Tes towanie funkcjonalne 1985 S trateg ie oparte na przepływie danych 1989
Formalne metody dla integ racji tes tów opartych na s pecyfikacji i na s trukturze
1990 Tes towanie oparte na log ice
1994 Model s zacowania niezawodnoś ci oprog ramowania oparty na pokryciu 1995 Ortog onalna klas yfikacja defektów (IBM) 1997 Tes towanie integ racyjne oparte na opis ie architektury 1997 Probabilis tyczne tes towanie funkcjonalne 1997 Narzędzia automatyzacji tes towania 2000 Tes towanie integ racyjne oparte na diag ramach UML 2000 Metoda S ixS ig ma dla tworzenia oprog ramowania 2001
Zinteg rowana technika dla oprog ramowania oparteg o na komponentach
2004 Automatyczne narzędzia analizy s tatycznej 2005 Podejś cie Goal-Ques tion-Metric 2008
Automatyczne, zoptymalizowane projektowanie przypadków tes towych
2010 Wczes na analiza ryzyka
1.5. Ogólne zasady testowania W ciągu kilkudziesięciu ostatnich lat pojawiło się wiele zasad dotyczących testowania oprogramowania. Poniższa lista, zaczerpnięta z [13], podaje siedem uniwersalnych zasad, które obowiązują niezależnie od zakresu testowania, rodzaju testowanego systemu czy używanych technik. Zasada 1. Testowanie ujawnia usterki. „Testowanie pokazuje obecność, a nie brak usterek” – to słynne zdanie zostało wygłoszone przez Dijkstrę podczas dyskusji na konferencji „Software Engineering Techniques” w 1969 roku w Rzymie [21]. Test, którego wynik różni się od oczekiwanego, pokazuje istnienie jakiegoś
problemu w kodzie lub dokumentacji. Wystarczy jeden taki test, abyśmy mogli stwierdzić wystąpienie błędu. Jeśli jednak wszystkie testy dadzą poprawne, tzn. zgodne z oczekiwaniem, wyniki, czy jest to dowód poprawności testowanego programu? Niestety nie. W kodzie może istnieć defekt, którego żaden test nie wykrył. Stwierdzenie Dijkstry ma wręcz charakter twierdzenia matematycznego – można je formalnie udowodnić na gruncie teorii rozstrzygalności. Formalnie, problem stwierdzenia poprawności programu jest nierozstrzygalny, to znaczy nie istnieje algorytm stwierdzający, czy program podany mu na wejściu jest poprawny2. Oczywiście testowanie zmniejsza prawdopodobieństwo występowania w programie niewykrytych defektów, niemniej nieznalezienie żadnych usterek nie jest dowodem poprawności oprogramowania. Zasada 2. Testowanie gruntowne jest niewykonalne. Przetestowanie wszystkich możliwych kombinacji warunków początkowych i danych wejściowych do programu jest wykonalne tylko w trywialnych przypadkach. Poza trywialnymi przypadkami nie da się również przetestować wszystkich możliwych ścieżek przepływu sterowania. Jeśli w programie istnieje przynajmniej jedna pętla, to może się ona wykonywać dowolną liczbę razy, zatem istnieje nieskończenie wiele ścieżek wykonania. Nawet jeśli założymy, że dziedzina wejściowa jest ograniczona (w końcu komputer operuje na zbiorach dyskretnych, nie ciągłych i każda zmienna może przyjmować skończoną liczbę możliwych wartości), to i tak liczba kombinacji wejść czy ścieżek jest zbyt duża, aby dała się przetestować w sensownym czasie. zmienna (ang. variable) – element pamięci komputera, dostępny w programie przez swoją nazwę Rozważmy przykład pseudokodu z listingu 1.3.
1 function Pętla(int x) 2 for (int i=1; i0) zamiast if (x>=0). błąd (ang. error) – nieprawidłowy stan wewnętrzny programu (wg [7]) awaria (ang. failure) – widoczne na zewnątrz odchylenie modułu lub systemu od oczekiwanego zachowania lub rezultatu działania (wg [26]) Niektóre źródła (np. [13]) traktują błąd jako formę awarii. W takim przypadku awaria niekoniecznie musi dać o sobie znać w widoczny sposób (bo stan programu jest jego wewnętrzną konfiguracją). Wykrywanie błędnych stanów jest o wiele trudniejsze niż wykrywanie „zwykłych” awarii, gdyż te ostatnie zwykle można dostrzec gołym okiem. Błędy i awarie objawiają się podczas wykonywania testów, co pośrednio umożliwia odkrycie usterki w kodzie. Samym usuwaniem usterek zajmuje się debugowanie debugowanie (ang. debugging) – proces wyszukiwania, i usuwania przyczyn błędów i awarii oprogramowania
analizowania
Jedna usterka może spowodować jeden lub więcej błędów lub awarii. Jedna awaria może być spowodowana jedną lub więcej usterkami. Często odniesienie awarii do konkretnej usterki jest trudne. Te rozważania doprowadziły do stworzenia modelu usterka/awaria, który opisuje sekwencję zdarzeń, jakie muszą nastąpić, aby zaobserwować awarię. Model ten nosi nazwę modelu RIP, od pierwszych liter angielskich słów: reachability, infection i propagation. W języku polskim czasami model ten nazywa się modelem usterka–błąd–awaria. model RIP (ang. RIP model) – model opisujący trzy kroki prowadzące do obserwacji awarii. Kroki te to: osiągalność (ang. reachability) – miejsce w programie, które zawiera usterkę, musi zostać osiągnięte;
infekcja (ang. infection) – po wykonaniu niepoprawnej instrukcji stan programu musi przyjąć nieprawidłową wartość; propagacja (ang. propagation) – zainfekowany stan musi propagować się i wywołać nieprawidłowe wyjście lub inne zewnętrzne zachowanie się programu. Wystąpienie błędu nie zawsze musi oznaczać wystąpienie awarii. Rozważmy program mający za zadanie sortować tablicę T metodą bąbelkową. Jego pseudokod jest przedstawiony na listingu 2.1. Zakładamy, że pola tablic są indeksowane od zera. W linii 4. program zawiera usterkę. Pętla powinna przebiegać od i równego 0, nie 1. Rozważmy następujące dwa wykonania programu SortBąbelkowe w kontekście modelu RIP: SortBąbelkowe((1, 4, 3, 6, 2)) SortBąbelkowe((5, 2, 7, 1, 4))
1 function SortBąbelkowe(int T[]) 2 n=liczba_elementów_T 3 do 4 5 6 7
for (i=1; iT[i+1]) then zamień miejscami T[i] z T[i+1] end if
8 end for 9 n=n-1 10 while (n>1) 11 return T 12 end Listing 2.1. Algorytm sortowania bąbelkowego W pierwszym przypadku po dojściu sterowania do linii 4. nastąpi osiągnięcie defektu (reachability). W tym samym momencie stan programu, niezależnie od postaci wejścia, stanie się błędny (infekcja), gdyż licznik zmiennej i powinien
wskazywać na 0, a nie na 1. W rezultacie, tablica będzie sortowana od drugiego elementu. Jednak tak się szczęśliwie złożyło, że najmniejszym elementem tablicy jest jej pierwszy element, dlatego nie trzeba go sortować. Program zwróci poprawnie posortowaną tablicę (1, 2, 3, 4, 6). W drugim przypadku osiągnięcie defektu i infekcja są analogiczne. Różnica jest taka, że pierwszy element tablicy nie jest najmniejszy, dlatego nastąpi propagacja zainfekowanego stanu (pierwszy element nie będzie podlegał sortowaniu). W rezultacie program ulegnie awarii, gdyż zwróci nieprawidłowy wynik (5, 1, 2, 4, 7). Model RIP odgrywa bardzo ważną rolę w kryteriach pokrycia stosowanych, zwłaszcza w testowaniu mutacyjnym (opiszemy je dokładnie w podrozdz. 9.15) i automatycznej generacji danych testowych [27]. Omówimy teraz pozostałe podstawowe pojęcia, których będziemy używać w kolejnych rozdziałach. Pierwszym z nich jest pojęcie jakości. Jakość jest cechą trudną do zmierzenia ilościowego, jednak jej poziom można odnieść do wartości różnych metryk stosowanych w procesie testowym, takich jak stopień pokrycia przez testy ryzyka zidentyfikowanego w produkcie. Im więcej ryzyk jest pokrytych przez testy, tym większą możemy mieć pewność, co do poziomu jakości testowanego programu. Zagadnienia inżynierii jakości są omówione w rozdziałach 38–49. jakość (ang. quality) – stopień, w jakim moduł, system lub proces spełnia określone wymagania, potrzeby lub oczekiwania (wg [7]) Testy tworzy się na bazie tzw. podstawy testu, czyli wszelkiego rodzaju dokumentów opisujących wymagania dla projektu, np. specyfikacji: wymagań, projektu, architektury. Jeśli dokument może być zmieniony tylko w wyniku formalnego procesu dokonywania zmian, to podstawę testu nazywa się zamrożoną podstawą testu. specyfikacja (ang. specification) – dokument określający w kompletny, precyzyjny i możliwy do weryfikacji sposób wymagania, projekt, zachowanie lub inne właściwości modułu lub systemu, może również opisywać procedury sprawdzania, czy warunki te zostały spełnione Analiza podstawy testu pozwala zdefiniować warunki testowe oraz przedmioty pokrycia (zwane też elementami pokrycia), które następnie staramy
się pokryć przypadkami testowymi. Warunkiem testowym może być np. wykonanie wszystkich instrukcji w programie, wymuszenie prawdziwości oraz fałszywości wszystkich predykatów w instrukcjach warunkowych warunkowej, przetestowanie wszystkich elementów GUI. podstawa testu (ang. test basis) – dokument, z którego można wnioskować o wymaganiach dla modułu lub systemu (wg [28]) zamrożona podstawa testu (ang. frozen test basis) – dokument podstawy testu, który może być zmieniony jedynie przez formalny proces kontroli zmiany podstawa (ang. baseline) – specyfikacja lub oprogramowanie, które było poddane formalnemu przeglądowi lub uprzednio uzgodnione, będące odniesieniem dla dalszych prac programistycznych, które może ulec zmianie tylko przez formalną procedurę zmian (wg [7]) warunek testowy (ang. test condition), inaczej wymaganie testowe, sytuacja testowa (ang. test requirement, test situation) – element lub działanie systemu, którego istnienie bądź poprawność może być zweryfikowana za pomocą jednego lub wielu przypadków testowych (np. funkcja, transakcja, cecha, atrybut jakości lub element struktury) cecha, cecha oprogramowania (ang. feature, software feature) – atrybut modułu lub systemu wyspecyfikowany w dokumentacji wymagań lub wywnioskowany z niej, np. niezawodność, użyteczność, ograniczenia projektowe [29] projekt testu (ang. test design) – 1) patrz: specyfikacja projektu testu; 2) proces przekształcania ogólnych celów testowania w uszczegółowione warunki testowe i przypadki testowe Warunki testowe służą do definiowania elementów pokrycia lub same je wprost stanowią. Na przykład, jeśli warunkiem testowym jest wartość brzegowa (np. wartość leżąca na końcu analizowanego przedziału liczbowego), elementami pokrycia dla tego warunku mogą być: ona sama oraz najbliższa jej wartość leżąca po drugiej stronie granicy (np. dla instrukcji x ≥ 8, gdzie x jest zmienną całkowitą,
warunkiem testowym może być wartość brzegowa 8, a elementami pokrycia liczby 8 oraz 7). Jeśli warunkami testowymi są klasy równoważności, to stanowią one jednocześnie elementy pokrycia. Zwykle warunki testowe i elementy pokrycia są fragmentami modelu opisującego działanie programu (np. wierzchołki lub krawędzie grafu przepływu sterowania, stany maszyny stanowej, kolumny tablicy decyzyjnej). element pokrycia, element testowy, przedmiot pokrycia (ang. coverage item, test item) – obiekt lub właściwość wykorzystywane jako punkt wyjścia do zaprojektowania testów pokrycia, np. klasy równoważności lub pokrycie kodu element przetestowany (ang. exercised) – element oprogramowania określa się jako przetestowany (sprawdzony) przez przypadek testowy, gdy wartość wejściowa powoduje wykonanie tego elementu, np. instrukcji, decyzji lub innego elementu strukturalnego Dla każdego warunku testowego (lub elementu pokrycia) należy stworzyć przypadek testowy pokrywający go. Na przykład, jeśli elementami pokrycia są linie kodu, a przypadek testowy powoduje wykonanie instrukcji nr 1, 2, 5, 8 i 9, to mówimy, że ten test pokrył elementy pokrycia nr 1, 2, 5, 8 i 9. Po wykonaniu tego testu na pokrycie czekają wciąż inne elementy, np. linie 3, 4, 6, 7. przypadek testowy (ang. test case) – zbiór danych wejściowych, wstępnych warunków wykonania, oczekiwanych rezultatów i końcowych warunków wykonania opracowany w celu zweryfikowania zgodności działania programu z oczekiwanym rezultatem lub sprawdzenia warunku testowego (wg [7]) Odsetek pokrytych elementów pokrycia nazywa się stopniem pokrycia. Zawsze dążymy do osiągnięcia stuprocentowego stopnia pokrycia, to znaczy do dostarczenia takiego zestawu testów, że dla każdego elementu pokrycia istnieje przynajmniej jeden test, który go pokrywa. Zestaw taki określa się mianem podstawowego zestawu testów. podstawowy zestaw testów (ang. basis test set) – zestaw przypadków testowych (zwykle najmniejszy możliwy) zapewniający osiągnięcie 100% określonego kryterium pokrycia
analiza pokrycia (ang. coverage analysis) – pomiar pokrycia osiągnięty podczas wykonywania testów wg z góry określonych kryteriów, przeprowadzany w celu określenia czy są potrzebne dodatkowe testy; jeśli odpowiedź brzmi tak, to jest podejmowana decyzja, jakie dodatkowe przypadki testowe wykonać Po wykonaniu testu porównuje się wynik tego testu z wynikiem oczekiwanym. Jeśli rezultaty są zgodne, to mówimy, że test jest zaliczony. W przeciwnym przypadku test nazywamy niezaliczonym. Testy niezaliczone wymagają szczegółowego dalszego badania, gdyż mogą świadczyć o istnieniu błędu. oczekiwany rezultat, wynik oczekiwany, przewidywany wynik (ang. expected result, expected outcome, predicted outcome) – zachowanie modułu lub systemu w ustalonych warunkach określone na podstawie specyfikacji lub innego źródła wynik, wynik testu, rezultat, rezultat testu (ang. outcome, test outcome, result, test result) – konsekwencja, wynik wykonania testu. Zawiera wyjścia na ekran, zmiany danych, raporty oraz wysyłane komunikaty; patrz także: rzeczywisty rezultat rzeczywisty rezultat, rzeczywisty wynik (ang. actual result, actual outcome) – wytworzone/zaobserwowane zachowanie się modułu lub systemu podczas jego testowania zaliczenie, test zaliczony (ang. pass, test pass) – test uważa się za zaliczony, jeśli jego rezultat pasuje do rezultatu oczekiwanego niezaliczenie, niezdanie, test niezaliczony, test niezdany (ang. fail, test fail) – test uznaje się za niezaliczony, jeśli jego rezultat różni się od oczekiwanego kryteria zaliczenia/niezaliczenia (ang. pass/fail criteria) – reguły decyzyjne wykorzystywane do określenia, czy obiekt testowany (funkcja) lub cecha zaliczył(a) test [5] Po wykonaniu wszystkich testów możemy zmierzyć pokrycie, czyli stopień, w jakim zestaw testów pokrył elementy pokrycia. Zwykle jest to miara ilorazowa,
wyrażająca stosunek wykorzystanych elementów pokrycia do wszystkich elementów pokrycia. pokrycie, pokrycie testowe (ang. coverage, test coverage) – stopień, wyrażony w procentach, w jakim zakresie zestaw testowy wykorzystał przedmiot pokrycia Rozważmy prosty przykład kodu z listingu 2.2.
function ProstyProgram(int x, int y) 1 z:=x+y 2 if (z==0) then 3 return 0 4 5 6 7 8 9
else if (x>y) then z:=z+x else z:=z+y return z end
Listing 2.2. Przykładowy program ilustrujący stopień pokrycia Program ten stanowi przedmiot testów. Załóżmy, że warunkiem testowym jest pokrycie instrukcji. Zatem elementami pokrycia są wszystkie linie kodu. Weźmy następujący zestaw przypadków testowych: PT1: (x = 4, y = –4) oczekiwane wyjście: 0 PT2: (x = 0, y = 3) oczekiwane wyjście: 6 Wykonanie PT1 spowoduje przejście przez instrukcje 1, 2, 3 (w linii 1. zmienna z otrzyma wartość 4 – 4 = 0, będzie spełniony warunek instrukcji if w linii 2., zatem nastąpi wykonanie linii 3., co będzie skutkować zwróceniem wartości 0 i zakończeniem działania programu). Wykonanie PT2 spowoduje przejście przez instrukcje 1, 2, 4, 5, 7, 8, 9. W sumie oba przypadki wykorzystały elementy pokrycia
odpowiadające liniom 1, 2, 3, 4, 5, 7, 8, 9. Pokryły więc 8 spośród 9 elementów pokrycia, zatem stopień pokrycia instrukcji dla powyższego zestawu testów wynosi 8/9 ≈ 89%. Gdybyśmy chcieli osiągnąć pokrycie stuprocentowe, musielibyśmy dodać do zestawu testów taki, który wymusi przejście przez linię 6. Nastąpi to tylko wtedy, gdy warunek w instrukcji if w linii 3. nie będzie spełniony i jednocześnie warunek w linii 5. będzie spełniony, czyli gdy x + y ≠ 0 oraz x > y. Warunki te są spełnione np. dla wejścia (x = 5, y = 3). Należy pamiętać, że pokrycie niekoniecznie musi odnosić się wyłącznie do elementów strukturalnych oprogramowania, takich jak linie kodu, decyzje w programie itp. Pokrycie może dotyczyć innych artefaktów procesu wytwórczego, np. wymagań, funkcjonalności, przypadków użycia, ryzyk, cech niefunkcjonalnych oprogramowania. kryterium pokrycia (ang. coverage criterion) – reguła lub zestaw reguł wymuszających wykorzystanie elementów pokrycia przez zbiór przypadków testowych kryteria zaliczenia testu (ang. pass/fail criteria) – reguły decyzyjne wykorzystywane do określenia, czy obiekt testowany lub cecha zaliczył(a) test [5]
3. Proces testowy
Testowanie jest pełnoprawną, osobną dziedziną inżynierii oprogramowania i musi być właściwie zorganizowane, aby było efektywne. Powinno więc być ujęte w ramy dobrze zdefiniowanego procesu. Proces jest sekwencją działań, które na podstawie warunków początkowych (wejść) pozwalają osiągnąć założony cel (wyjścia). Warunki początkowe są nazywane kryteriami wejścia, a warunki wyjściowe – kryteriami wyjścia. Zbiór kryteriów wyjścia nazywa się efektem testu. Proces testowy w odniesieniu do pojedynczego obiektu testów jest nazywany cyklem testowym. kryterium wejścia (ang. entry criteria) – zbiór ogólnych i szczególnych warunków, których spełnienie jest wymagane do kontynuacji procesu dla określonego zadania [30] kryterium wyjścia, kryterium ukończenia, kryterium zakończenia testu (ang. exit criteria, completion criteria, test completion criteria) – zbiór ogólnych i szczególnych warunków, uzgodnionych z udziałowcami, których spełnienie jest wymagane do oficjalnego zakończenia procesu [30] efekt testu (ang. test target) – zbiór kryteriów wyjścia cykl testowy (ang. test cycle) – wykonanie procesu testowego w stosunku do pojedynczego, możliwego do zidentyfikowania wydania testowanego obiektu Kryteria wejścia stosuje się po to, aby ochronić projekt przed wykonaniem zadania, na którego przeprowadzenie nie jesteśmy jeszcze przygotowani. Przedwczesne rozpoczęcie zadania może spowodować zmarnowanie nakładów
pracy, bo np. trzeba je będzie powtórzyć lub poświęcić o wiele więcej czasu na jego ukończenie niż w sytuacji, gdy warunki wejścia są spełnione. Kryteria wyjścia stosuje się z kolei, aby uchronić projekt przez przedwczesnym zakończeniem określonych zadań, gdy nie wszystkie czynności związane z tymi zadaniami zostały wykonane. Kryteria wyjścia są stosowane jako argument przeciwko zakończeniu testów oraz w celu precyzyjnego zdefiniowania momentu ich zakończenia [30]. W każdej firmie proces testowy będzie wyglądał inaczej, jednak praktycznie we wszystkich projektach uwzględniających w jakiś sposób fazę testowania da się wyróżnić wspólny zestaw czynności, takich jak: planowanie, kontrola i nadzór, projektowanie testów, implementacja testów, wykonanie testów, przygotowanie i utrzymanie środowiska testowego, zamykanie czynności testowych. W tym rozdziale omówimy dwa modele procesu testowego. Pierwszy, proponowany przez ISTQB [31] to tzw. Podstawowy proces testowy. Drugi jest opisany przez normę ISO/IEC/IEEE 29119 [32]. Oba procesy mogą być stosowane dla dowolnego modelu cyklu życia oprogramowania. W podrozdziale 3.1, opisując szczegółowo fazy Podstawowego procesu testowego, wprowadzimy jednocześnie wiele definicji występujących także w normie ISO/IEC/IEEE 29119. Dlatego w podrozdziale 3.2 proces proponowany przez tę normę omówimy pobieżnie, skupiając się jedynie na ogólnym opisie oraz wyszczególnieniu różnic między oboma podejściami.
3.1. Podstawowy proces testowy Na rysunku 3.1 przedstawiono czynności Podstawowego procesu testowego. Proces ten, oparty na metodologii TMap [33] składa się z następujących faz: planowanie testów; kontrola i nadzór; analiza testów; projektowanie testów; implementacja testów;
Rysunek 3.1. Podstawowy proces testowy (wg ISTQB) wykonanie testów; ewaluacja kryteriów wyjścia i raportowanie; zamykanie czynności testowych. Sposób prezentacji
poszczególnych faz na
rysunku ma
sugerować, iż
poszczególne etapy procesu nie muszą następować po sobie w sposób sekwencyjny. Na przykład w testowaniu eksploracyjnym projektowanie i implementacja testów następują równolegle z ich wykonaniem. Po zakończeniu fazy analizy testów może się okazać, że zmieniły się istotne warunki projektu (zmiana wymagań, odkrycie nowego, poważnego ryzyka itp.) i należy ponownie przeprowadzić fazę planowania, analizy itd. Proces można zaadaptować dla konkretnego poziomu testów (np. dla poziomu testów systemowych) lub dla całego procesu testowego. W kolejnych podrozdziałach opiszemy, jakie czynności przynależą do poszczególnych faz Podstawowego procesu testowego. proces (ang. process) – powiązane ze sobą działania przetwarzające wejścia w wyjścia (wg [34])
proces testowy (ang. test process) – model opisujący zestaw czynności wykonywanych przez osoby zaangażowane w testowanie oprogramowania; Podstawowy proces testowy składa się z następujących faz: planowanie testów, nadzór i kontrola, analiza, projektowanie, implementacja, wykonanie, ocena kryteriów wyjścia, zamykanie testów
3.1.1. Planowanie Planowanie testów jest czynnością wykonywaną głównie przez kierownika testów przy udziale analityka testów. Zaczyna się na początku procesu testowego dla każdego poziomu testów. Polega na zidentyfikowaniu wszystkich czynności testowych oraz zasobów potrzebnych do osiągnięcia celu opisanego w strategii testów1. Jest to również faza, w której się określa, jakie metryki będą wykorzystywane w monitorowaniu i kontroli procesu testowego oraz definiuje się sposoby ich zbierania. Na tej podstawie planuje się użycie określonych narzędzi do zbierania metryk i raportowania. Należy również zaplanować, jakie szkolenia będą przeprowadzone i kto ma w nich wziąć udział. Jeśli np. testerzy nie mieli dotychczas doświadczenia z metodyką Scrum, która ma być wykorzystywana w projekcie, to powinni zostać przeszkoleni w zakresie zwinnych metodyk wytwarzania oprogramowania. planowanie testów (ang. test planning) – tworzenie planów testów lub ich modyfikacja Dokładne czynności planowania wynikają zawsze z przyjętej strategii testowania2. Jeśli na przykład jest stosowane podejście oparte na ryzyku, to planowanie musi brać pod uwagę analizę ryzyka projektowego (np. jeśli wiadomo, że wiele błędów wynika ze źle rozumianych wymagań, należy zaplanować więcej czasu na przeglądy formalne wymagań i dokumentów projektowych). Należy również określić kryteria priorytetyzacji, które będą wykorzystywane podczas faz projektowania i wykonywania testów. Jeśli stosuje się podejście oparte na standardzie, to trzeba zadbać, aby wszystkie planowane czynności wynikały ze standardu lub były z nim zgodne. Jeśli wykorzystywane będzie podejście reaktywne, to należy precyzyjnie określić, jakie narzędzia będą używane do testowania dynamicznego.
testowanie dynamiczne (ang. dynamic testing) – testowanie, podczas którego jest wykonywany kod modułu lub systemu testowanie reaktywne (ang. reactive testing) – testowanie, które dynamicznie reaguje na testowany system i otrzymane wyniki testowania; w typowym podejściu testowanie reaktywne ma zredukowany cykl planowania, a fazy projektowania oraz implementacji testów nie wykonuje się, dopóki obiekt testów nie zostanie dostarczony Podczas planowania ustala się, jakie poziomy testów będą wykorzystywane (patrz podrozdz. 4.2), jakie są cele dla każdego z tych poziomów oraz jakie techniki projektowania testów będą używane na poszczególnych poziomach. Definiuje się także typy testów, które będą używane w procesie testowym (patrz podrozdz. 4.4). Menedżer testów, w porozumieniu z analitykiem testów, określa specyfikację środowiska testowego oraz upewnia się, że osoby odpowiedzialne za jego budowę i utrzymywanie są kompetentne i będą dostępne przez cały czas wykorzystywania tego środowiska. Identyfikuje również wszystkie ograniczenia, wymagania i obowiązki wynikające z różnego rodzaju umów, takich jak umowa o standard usługi (ang. Service Level Agreement, SLA) czy też z zewnętrznych zależności (np. zależności od innych projektów, usługi zlecane na zewnątrz, dostawcy, licencje). środowisko testowe, osprzęt testowy, podłoże testowe3 (ang. test environment, test rig, test bed) – środowisko, w którego skład wchodzi sprzęt, wyposażenie, symulatory, oprogramowanie oraz inne elementy wspierające, potrzebne do przeprowadzenia testów [7] Bardzo ważne jest, aby zweryfikować oszacowania czasowe i budżetowe nałożone na proces testowy, a także zaplanować testowanie wszystkich artefaktów procesu wytwórczego, nie tylko kodu. Są to na przykład: konfiguracje systemów i środowisk, dokumentacja, procedury instalacyjne. W fazie planowania należy również określić, w jaki sposób proces testowy będzie dopasowany do przyjętego modelu cyklu życia oprogramowania. konfiguracja (ang. configuration) – układ modułów lub system zdefiniowany za pomocą liczby, natury oraz wewnętrznych powiązań jego części składowych
3.1.2. Monitorowanie i nadzór Monitorowanie i nadzór to dwie różne czynności, choć ich nazwy brzmią podobnie i sugerują podobne aktywności. Wrażenie synonimiczności tych pojęć wynikać może z faktu, że są one przeprowadzane jednocześnie, w trakcie całego czasu trwania projektu. Monitorowanie testów jest czynnością zarządczą, polegającą na sprawdzaniu statusu i postępu projektu oraz porównywaniu uzyskanych wyników z zaplanowanymi. Weryfikacji tej dokonuje się za pomocą pomiarów metryk zdefiniowanych w fazie planowania. Przykładami metryk mogą być: procent zakończonych zadań spośród wszystkich zaplanowanych, osiągnięty stopień pokrycia, liczba stworzonych, wykonanych i zaliczonych przypadków testowych itd. W dalszej części książki omówimy wiele takich miar. Faza monitorowania obejmuje również tworzenie raportów z pomiarów. Jeśli okaże się, że jakiś parametr projektu odstaje od normy, to należy przedsięwziąć czynności zaradcze, aby sprowadzić projekt na właściwy tor. Opracowanie oraz wdrażanie takich działań to nadzór. Można więc stwierdzić, że monitorowanie jest czynnością „pasywną”, polegającą na obserwacji, natomiast nadzór jest czynnością „aktywną”, związaną z podejmowaniem działań. Czynności monitorowania i nadzoru są częścią większego procesu zarządczego, mianowicie zarządzania testami. Raportowanie to zwykle zadanie menedżera testów, natomiast samym zbieraniem metryk zajmuje się analityk testów. monitorowanie testów (ang. test monitoring) – zadanie z zakresu zarządzania testowaniem, zajmujące się okresowym sprawdzaniem statusu projektu testowego oraz raportowaniem stanu aktualnego w porównaniu ze stanem planowanym nadzór nad testami (ang. test control) – zadanie z zakresu zarządzania testowaniem, polegające na opracowaniu i wdrażaniu działań korygujących projekt testowy, gdy monitorowanie pokazuje odchylenie od planu zarządzanie testami (ang. test management) – planowanie, szacowanie, monitorowanie oraz kontrola przebiegu testów, na ogół przeprowadzane przez menedżera testów
Przykładowym rezultatem kontroli może być raport w formie graficznej porównujący skumulowaną liczbę znalezionych i naprawionych defektów. Przykład takiego wykresu był wcześniej pokazany na rysunku 1.2b. Nadzór mógłby tutaj polegać na przeprowadzeniu analizy źródłowej problemu i podjęciu decyzji o tym, by wydłużyć daną fazę testów, przeznaczyć więcej programistów do naprawy błędów, przeprowadzić inspekcje kodu pod kątem stosowania wzorców projektowych ułatwiających debugowanie itp.
3.1.3. Analiza testów Analiza testów to faza, w której określa się zakres testów, a więc „co” ma być przetestowane. Najpierw należy wybrać przedmiot testów oraz elementy testowe. Następnie dokonuje się przeglądu podstawy testów, celów testowania bądź analizy ryzyka oraz identyfikuje na ich podstawie warunki testowe i elementy pokrycia. Warunki te mogą służyć jako miary osiągniętego sukcesu i stanowić część warunków wyjściowych. przedmiot testów (ang. test object) – moduł lub system podlegający testowaniu Podstawa testów będzie się różnić w zależności od poziomu testów. W tabeli 3.1 (opracowanej na podstawie [35]) przedstawiono najczęściej spotykane poziomy testów oraz typową podstawę testów dla tych poziomów. analiza testów (ang. test analysis) – faza procesu testowania polegająca na identyfikacji obiektów podlegających testowaniu oraz na wyprowadzeniu warunków testowych z podstawy testów Tabela 3.1. Typowe poziomy testów i odpowiadająca im podstawa testów
Poziom testów
T ypowa podstawa testów
tes towanie wymag ania, projekt komponentów s zczeg ółowy, kod
projekt architektury
T ypowe elementy do pokrycia ins trukcje, decyzje, warunki, ś cieżki interfejs y wewnętrzne, niezmienniki, parametry,
protokoły komunikacji
tes towanie integ racyjne tes towanie s ys temowe
s pecyfikacja wymag ań oprog ramowania
tes ty integ racji projekt produktu 4 s ys temów
wymag ania funkcjonalne i niefunkcjonalne interfejs y zewnętrzne (API), niezmienniki, parametry
tes towanie akceptacyjne
s pecyfikacja wymag ań użytkownika, podręczniki użytkownika
przypadki użycia, s cenarius ze
analiza s tatyczna
dokumenty, których dotyczy analiza
s trony, wymag ania, przypadki tes towe, kod
Warunki testowe mogą być definiowane na różnych poziomach ogólności. Generalnie im wyższy poziom szczegółowości przyjmiemy, tym więcej wyprowadzimy warunków testowych. Na przykład, warunek testowy wysokiego poziomu dla systemu ELROJ (patrz Dodatek A) może mieć postać „przetestuj poprawność wyświetlania informacji na ekranie”, a warunkiem szczegółowym może być np. „przetestuj, czy metoda setActualDate działa poprawnie wtedy, gdy zmienna określająca rok jest liczbą dwucyfrową”. Bardzo ważne jest, aby warunki testowe były precyzyjnie powiązane z podstawą testów. Właściwość ta określana jest mianem śledzenia i w ogólności dotyczy nie tylko warunków testowych, lecz także wszystkich elementów dokumentacji i oprogramowania. Dzięki możliwości śledzenia jesteśmy w stanie kontrolować stopień pokrycia wymagań, ryzyk, elementów projektowych itd. Ponadto, gdy w projekcie pojawia się jakaś zmiana (np. modyfikacja wymagania), możemy przeprowadzić tzw. analizę wpływu, to znaczy, określić, na które testy ta zmiana będzie miała wpływ, czy wymagane jest przeprojektowanie i ponowne uruchomienie tych testów oraz jak dużo czasu to zajmie.
analiza wpływu (ang. impact analysis) – oszacowanie wpływu zmiany w dokumentacji lub systemie na przypadków testowych oraz ryzyka
pozostałe obszary
systemu,
postać
Rysunek 3.2. Przykład obustronnego śledzenia między artefaktami projektu Na rysunku 3.2 przedstawiono przykładowy schemat opisujący relacje między poszczególnymi elementami projektowymi, które umożliwiają śledzenie. Każdy przypadek testowy możemy odnieść do konkretnego elementu projektowego (linia kodu, funkcja, moduł, system itp.), który jest pokrywany tym testem. Z każdym elementem projektowym może być związane jakieś ryzyko, każde ryzyko można odnieść do konkretnego wymagania itd. Relacje między elementami mogą być jeden do jednego, jeden do wielu lub wiele do wielu. Strzałki na schemacie reprezentują śledzenie obustronne. Mając przypadek testowy, jesteśmy w stanie odnieść go do jednego lub kilku wymagań biznesowych i na odwrót – dla każdego wymagania biznesowego możemy znaleźć jeden lub kilka przypadków testowych odpowiadających temu wymaganiu. Jeśli w naszym projekcie schemat śledzenia wyglądałby tak, jak na rysunku 3.2, to np. na podstawie informacji o tym, które
testy zostały zaliczone, moglibyśmy wnioskować o tym, w jakim stopniu pokryliśmy ryzyko projektowe lub jaka część wymagań może być uznana za przetestowaną. śledzenie (ang. traceability) – zdolność identyfikowania elementów w dokumentacji i oprogramowaniu
powiązanych
Rozważmy przykład systemu ELROJ. Możemy odwzorować wymagania na poszczególne metody w następujący sposób: Tabela 3.2. Mapowanie wymagań na elementy projektowe systemu ELROJ
W ymaganie
Odpowiadająca metoda lub metody
R01
setInfo(), getInfo(), removeInfo()
R02
addLine(), removeLine()
R03
addBus()
R04
addBusInterval()
R05
removeBus()
R06
getDisplayString()
R07
setActualDate()
Możemy zdecydować, że wymaganie R01 będzie pokryte w 100% dopiero po pozytywnym przetestowaniu wszystkich trzech odpowiadających mu metod (setInfo, getInfo, removeInfo). Możemy też określić stopień pokrycia tego wymagania dla wszystkich trzech funkcji z osobna (np. każda może pokrywać wymaganie w jednej trzeciej). Zauważmy, że w tabeli brakuje metod getAllBusLines, getAllBusesTimes oraz getNextBusTime. Są one wywoływane w innych metodach w celu uzyskania określonych informacji i nie są bezpośrednio związane z żadnym z wymagań. Ale moglibyśmy zdecydować, że jeśli np. metoda getDisplayString wywołuje te trzy funkcje (i żadne inne), to wymaganie R06 może zostać pokryte w stu procentach tylko wtedy, gdy pozytywnie przetestowana będzie zarówno metoda getDisplayString, jak i wszystkie wywoływane przez nią
metody. Podejście takie jest szczególnie warte rozważenia w kontekście określania rezydualnego (pozostałego) poziomu ryzyka w testowanym systemie.
3.1.4. Projektowanie testów Efektem fazy analizy jest odpowiedź na pytanie – „co” testować. Faza projektowania odpowiada z kolei na pytanie – „jak” testować zidentyfikowane obiekty testów. Składa się z trzech głównych kroków: 1) wybór poziomu ogólności przypadków testowych dla poszczególnych obszarów testowania; 2) wybór technik projektowania, które zapewnią odpowiednie pokrycie; 3) stworzenie przypadków testowych weryfikujących zidentyfikowane warunki testowe. Projektowanie testów może się rozpocząć dopiero wtedy, gdy zidentyfikowano wszystkie warunki testowe. Projektowane przypadki testowe mogą mieć charakter przypadków ogólnych lub szczególnych. Te pierwsze są nazywane przypadkami testowymi wysokiego poziomu, drugie – przypadkami testowymi niskiego poziomu. przypadek testowy wysokiego poziomu, logiczny przypadek testowy, abstrakcyjny przypadek testowy (ang. high level test case, logical test case, abstract test case) – przypadek testowy bez konkretnych wartości wejściowych i oczekiwanych rezultatów przypadek testowy niskiego poziomu, konkretny przypadek testowy (ang. low level test case, concrete test case) – przypadek testowy z konkretnymi (na poziomie implementacji) wartościami wejściowymi i wynikami oczekiwanymi test (ang. test) – pojedynczy przypadek testowy lub zestaw przypadków testowych [5] Przypadki testowe wysokiego poziomu mogą być przydatne w sytuacji, gdy rzeczywiste wartości oczekiwane nie są jeszcze znane lub dostępne. Ten rodzaj przypadków stosuje się również w sytuacji, gdy chcemy dać więcej swobody
testerowi w stosowaniu konkretnych technik testowania. Zwykle mamy wtedy do czynienia z testerem doświadczonym, zarówno w zakresie technik testowania, jak i samego produktu. Ten poziom ogólności pozwala uzyskać większy stopień pokrycia, gdyż każde wykonanie przypadku testowego wysokiego poziomu może wyglądać nieco inaczej. Logiczne przypadki testowe znajdują również zastosowanie w projektach, w których wymagania są niedodefiniowane lub podane w sposób ogólny. Sprawdzają się dobrze tam, gdzie nie występuje duży narzut dokumentacyjny na czynności testerskie. Z przypadków ogólnych można wyprowadzać przypadki szczególne. Przypadki testowe niskiego poziomu są stosowane w sytuacji, w której tester jest niedoświadczony lub system jest testowany przez eksperta dziedzinowego, mającego wiedzę o systemie, ale niekoniecznie o testowaniu. Przypadki niskiego poziomu opisują szczegółowo całą procedurę testową, dzięki czemu nawet osoba niedoświadczona w testowaniu może takie przypadki z powodzeniem wykonać. Przypadki niskiego poziomu sprawdzają się dobrze w projektach, w których wymagania są dobrze zdefiniowane oraz w sytuacji, gdy jest wymagana zewnętrzna weryfikacja procesu testowego, np. w postaci audytu. Ze względu na precyzję ich konstrukcji, nadają się doskonale do wielokrotnego wykonania w tym sensie, że każda realizacja takiego przypadku zawsze będzie wyglądać tak samo i zawsze powinna dać ten sam (oczekiwany) wynik. Przypadki testowe tworzy się przy użyciu technik projektowania testów omówionych w rozdziałach 8–10, wykorzystując zidentyfikowane warunki testowe. Techniki projektowania mogą być narzucone lub zasugerowane przez dokumenty takie jak polityka, strategia czy plan testów. Dobry przypadek testowy powinien cechować się następującymi atrybutami: powtarzalność – każde wykonanie tego samego przypadku testowego powinno dać taki sam wynik (zakładając brak zmian w kodzie między kolejnymi wykonaniami); niezależność – kolejność wykonywania przypadków testowych nie powinna mieć wpływu na ich wyniki; weryfikowalność – po wykonaniu przypadku testowego da się jednoznacznie określić rezultat jego wykonania i jego zgodność z wynikiem spodziewanym; możliwość śledzenia – każdy przypadek powinno dać się odnieść do podstawy testów (wymagań, elementów projektowych, funkcjonalności,
ryzyka itp.) w sposób określony przez używaną strategię testową. powtarzalność testów (ang. test reproducibility) – atrybut testu wskazujący, czy przy każdym wykonaniu testu otrzymujemy te same wyniki Niezależność może być niespełniona w przypadku, gdy mamy do czynienia z sekwencją przypadków testowych ujętych w jeden duży scenariusz testowy, dlatego należy ją rozumieć jako cechę odnoszącą się do pojedynczego przypadku lub – jeśli to konieczne – do scenariusza testowego. Przypadek testowy może zawierać różne dane, takie jak imię i nazwisko autora, data utworzenia przypadku testowego, funkcjonalność, której dotyczy. W szczególności powinien jednak obowiązkowo zawierać następujące informacje: identyfikator – w celu jednoznacznej identyfikacji każdego przypadku; cel – opis tego, co chcemy sprawdzić przez wykonanie przypadku; warunki wstępne – opisujące stan środowiska i systemu tuż przed wykonaniem przypadku; dane testowe (nie dotyczy przypadków wysokiego poziomu); oczekiwany wynik; warunki wyjściowe – opisujące stan środowiska i systemu po wykonaniu przypadku. identyfikator (ang. identifier) – unikatowe oznaczenie testu, wymagania lub innego artefaktu testaliów, pozwalające na jednoznaczne określenie tego elementu cel testu (ang. test objective) – przyczyna lub powód zaprojektowania i przeprowadzenia testu dane testowe (ang. test data) – dane, które istnieją przed wykonaniem testu i które mają wpływ na testowany moduł lub system bądź na które wpływa testowany moduł lub system warunek wstępny (ang. precondition) – warunki środowiska i stanu oprogramowania, które muszą być spełnione zanim moduł lub system będzie mógł być uruchomiony przez określony test lub procedurę testową
warunek wyjściowy (ang. postcondition) – warunki środowiska lub stanu oprogramowania, które muszą być spełnione po wykonaniu testu lub procedury testowej Dostarczenie wspomnianych informacji, z wyjątkiem identyfikatora i celu, może być zadaniem trudnym. Na przykład, danymi testowymi może być zestaw kilkunastu plików o dużym rozmiarze i skomplikowanej strukturze. Podczas testowania programów wykorzystujących sieci telekomunikacyjne ważne jest dokładne określenie wszystkich parametrów takiej sieci jako warunków wstępnych oraz wyjściowych. Oczekiwany wynik nie musi zawsze być widocznym na zewnątrz zachowaniem aplikacji. Może to być wewnętrzny stan programu, bazy danych, pamięci itd. Porównanie rezultatu aktualnego ze spodziewanym czasami jest trudne, żmudne lub czasochłonne, zwłaszcza, gdy musi być dokonywane dynamicznie. Dlatego często wymaga zastosowania specjalnych narzędzi (np. komparatorów). komparator, komparator testowy (ang. comparator, test comparator) – narzędzie testowe do przeprowadzania rezultatów rzeczywistych z oczekiwanymi
automatycznego
porównania
Przypadek tes towy PT008 Opis : s prawdzenie funkcjonalnoś ci ekranu Warunki ws tępne: baza danych o rozkładach jes t pus ta Krok Czynnoś ć 008.1
S prawdź, czy s ys tem poprawnie dodaje linię autobus ową do rozkładu
008.2 S prawdź, czy s ys tem poprawnie dodaje kurs y do rozkładu 008.3 S prawdź, czy s ys tem poprawnie wyś wietla kurs y na ekranie Warunki wyjś ciowe: baza zawiera poprawnie dodaną linię wraz z jej kurs ami, a informacja na ekranie jes t wyś wietlona poprawnie
Rysunek 3.3. Przypadek testowy wysokiego poziomu
Przypadek tes towy PT004 Opis : weryfikacja poprawneg o dodawania kurs u do rozkładu i wyś wietlania g o na ekranie Warunki ws tępne: baza danych o rozkładach jes t pus ta Krok Czynnoś ć
Oczekiwany wynik
004.1 Dodaj do rozkładu linię 104 W bazie s tworzono nową linię 104 004.2
Dodaj kurs w 45. minucie dla linii 104
W bazie dodano kurs 45 dla linii 104
004.3
Dodaj ponownie ten s am kurs dla linii 104
S tan bazy nie uleg ł zmianie
004.4
Dodaj kurs w 8. minucie dla W bazie dodano kurs 08 dla linii linii 104 104
004.5 Us tal dzień na 11.12.2013
Data zos tała us tawiona na 11.12.2013
Wyś wietl informację na 004.6 ekranie zawierającym 2 linie dla liczby minut = 40
Ekran zawiera nas tępujące informacje: 11.12.2013 Linia Czas 104 5 min 104 28 min Co odpowiada łańcuchowi „ 104 5 104 28” zwróconemu przez metodę g etDis playS tring (40, 2)
Warunki wyjś ciowe: baza zawiera dwa kurs y dla linii 104 (o minutach 45 i 08), a s tan ekranu wyg ląda tak, jak oczekiwany wynik dla kroku 004.6.
Rysunek 3.4. Przypadek testowy niskiego poziomu dynamiczne porównanie (ang. dynamic comparison) – porównywanie rzeczywistych i oczekiwanych rezultatów podczas wykonywania testów, na przykład za pomocą narzędzia do automatyzacji testów Rozważmy ponownie system ELROJ z Dodatku A. Przykłady przypadków testowych wysokiego i niskiego poziomu pokazane są odpowiednio na rysunkach 3.3 oraz 3.4. Dla przypadku wysokiego poziomu nie są podawane żadne dane wejściowe ani oczekiwane wyjścia, natomiast dla przypadku niskiego poziomu każdy krok jest opisany dokładnie, wraz z konkretnymi wartościami wejściowymi oraz oczekiwanymi rezultatami.
3.1.5. Implementacja testów Implementacja testów jest inżynierską realizacją produktów koncepcyjnej pracy wykonanej w fazie projektowania. Ten etap procesu dotyczy trzech aspektów testowania: przypadków testowych, danych testowych oraz środowiska testowego. Czynności związane z przypadkami testowymi to: implementacja przypadków testowych – realizowana na przykład w postaci fragmentu kodu wywołującego konkretną metodę z określonymi parametrami (np. testy jednostkowe), skryptu wywołującego szereg metod (implementacja scenariusza testowego), nagrywanie wykonywanych czynności (zwykle przy testowaniu GUI, przy użyciu takich narzędzi jak np. Selenium, opisanych w rozdz. 30); pisanie skryptów testowych – dotyczy głównie automatyzacji testów. Skrypty mogą zawierać zestaw często wykonywanych testów (np. testy regresji), mogą też reprezentować przypadki testowe wysokiego poziomu i być sparametryzowane, tzn. mogą umożliwiać wykonywanie tego samego testu dla różnych wartości wejściowych; ustalanie kolejności wykonywania przypadków testowych – kolejność ta może być podyktowana przeprowadzoną wcześniej analizą ryzyka lub specyficznymi zależnościami między przypadkami. Może również wynikać z tego, że niektórych przypadków nie można jeszcze wykonać
ze względu na
brak
odpowiedniej infrastruktury, która
będzie
dostarczona w późniejszym czasie, lub z braku odpowiednich ludzi; organizacja testów w tzw. suity testowe – suita testowa może być złożona z przypadków testowych, które muszą być wykonane w ściśle określonej kolejności lub też być zbiorem przypadków testujących np. tę samą funkcjonalność. W tym drugim znaczeniu suity testowe stosuje się w celu lepszej, czytelniejszej organizacji testów. implementacja testów (ang. test implementation) – proces projektowania i nadawania priorytetów procedurom testowym, tworzenia danych testowych i, opcjonalnie, przygotowywania uprzęży testowych, pisania automatycznych skryptów testowych, infrastruktury testowej: organizacyjnych artefaktów potrzebnych do wykonania testów, składających się ze środowisk testowych, narzędzi testowych, wyposażenia biurowego i procedur postępowania; dane testowe mogą być pozyskane z zewnętrznych źródeł przez obiekty testowe podczas wykonywania testów; zewnętrznym źródłem może być sprzęt, oprogramowanie lub człowiek skrypt testowy, specyfikacja procedury testowej, scenariusz testowy, procedura testowa (ang. test script, test procedure specification, test scenario) – dokument określający ciąg akcji umożliwiający wykonanie testu [5] automatyzacja testowania (ang. test automation) – użycie oprogramowania do wykonania lub wspierania czynności testowych, np. zarządzania testami, projektowania testów, wykonywania testów oraz sprawdzania i porównywania wyników automatyzacja wykonania testu (ang. test execution automation) – użycie oprogramowania, np. urządzenia rejestrująco-odtwarzającego, w celu kontrolowania wykonania testu, porównania rezultatów rzeczywistych z oczekiwanymi, ustawienia warunków wstępnych testu i innych funkcji kontroli i raportowania testu suita testowa, zestaw testowy, zestaw przypadków testowych, zbiór testów (ang. test suite, test case suite, test set) – 1) ciąg przypadków testowych,
w którym warunki wyjściowe z jednego testu używane są jako warunki wejściowe do następnego testu; 2) zestaw logicznie powiązanych ze sobą przypadków testowych, testujących np. tą samą funkcjonalność bądź obszar modułu lub systemu infrastruktura testu (ang. test infrastructure) – organizacyjne artefakty niezbędne do przeprowadzenia testu, składające się ze środowisk testowych, narzędzi testowych, wyposażenia biurowego i procedur Czynności związane z danymi testowymi to: implementacja konkretnych danych w przypadkach testowych – jeśli przypadki są opisane na szczegółowym poziomie i wymagają konkretnych wartości wejściowych; przygotowanie zbiorów danych, które mogą w przypadkach wysokiego poziomu;
być wykorzystane
implementacja generatorów danych – przydatna, gdy dane wejściowe są duże, mają skomplikowaną strukturę lub gdy przypadek testowy wymaga podania ciągu losowych danych symulujących określony profil użytkowania systemu; implementacja komparatorów – czyli narzędzi, które są w stanie porównać wyniki oczekiwane z rzeczywistymi. Jest to element automatyzacji testów, gdyż ułatwia pracę testerom, którzy nie muszą porównywać wyników testów ręcznie. dane wejściowe do testów (ang. test input) – dane otrzymywane z zewnętrznego źródła dostarczane do obiektu testów podczas wykonywania testu. Źródłem zewnętrznym może być sprzęt, oprogramowanie lub człowiek Czynności dotyczące środowiska testowego obejmują m.in.: tworzenie zaślepek i sterowników, niezbędnych do wykonania przypadków testowych, które używają niezaimplementowanych jeszcze elementów systemu; konfigurację sprzętu i narzędzi, które będą wykorzystywane w fazie uruchamiania testów, np. stacje bazowe w testowaniu
oprogramowania telekomunikacyjnego; instalację i konfigurację baz danych, systemów operacyjnych oraz innego niezbędnego oprogramowania – jest to czynność niezwykle ważna w przypadku testowania tzw. produktów z półki (ang. Commercial Off-The-Shelf, COTS), które należy przetestować w różnych konfiguracjach sprzętowych i software’owych (np. pod różnymi przeglądarkami, pod różnymi wersjami systemu operacyjnego). oprogramowanie z półki, COTS, oprogramowanie standardowe, komercyjne oprogramowanie z półki (ang. off-the-shelf software, standard software, commercial off-the-shelf software) – oprogramowanie stanowiące produkt wytworzony na szeroki rynek, to znaczy dla dużej liczby klientów, które jest dostarczane wielu klientom w identycznej postaci oprogramowanie na zamówienie, niestandardowe oprogramowanie (ang. bespoke software, custom software) – oprogramowanie projektowane dla grupy ściśle określonych użytkowników lub klientów; przeciwieństwo oprogramowania z półki sterownik, sterownik testowy (ang. driver, test driver) – moduł oprogramowania lub narzędzie testowe, które zastępuje (symuluje) moduł kontrolujący lub wywołujący funkcje testowanego modułu lub systemu [28] zaślepka (ang. stub) – szkieletowa albo specjalna implementacja modułu używana podczas produkcji lub testów innego modułu, który tę zaślepkę wywołuje albo jest w inny sposób od niej zależny; zaślepka zastępuje wywoływany moduł [7] Rozważmy projekty przypadków testowych wysokiego i niskiego poziomu przedstawionych na rysunkach 3.3 i 3.4. Ich implementacje mogłyby wyglądać tak, jak na listingach 3.1 i 3.2.
1 void TestCase008(int line, int[] courses, int displayLines, int mm) { 2 TimetableFacade facade = new TimetableFacade();
3 4
facade.addLine(line); for (i=0; i=3, gdzie x jest zmienną typu int), to elementami pokrycia dla tego warunku może być sama wartość 3 oraz wartość brzegowa leżąca po drugiej stronie granicy przedziału [3, ∞), czyli 2. Jeśli warunkiem testowym są klasyfikacje i klasy drzewa decyzyjnego, to elementami pokrycia mogą być kombinacje tych klasyfikacji. Jeśli warunkami testowymi są klasy równoważności, to są one jednocześnie elementami pokrycia, itd. Elementy pokrycia należy opisać w specyfikacji przypadku testowego. Etap czwarty to wyprowadzenie przypadków testowych w celu pokrycia elementów pokrycia. Dokonuje się tego przez wybór wartości wejściowych. Może się zdarzyć, że jeden przypadek testowy pokryje więcej niż jeden element pokrycia. Pozwala to zredukować liczbę przypadków testowych, ale może wydłużyć czas debugowania. Mamy zatem następujące dwa, skrajne podejścia do projektowania przypadków testowych ze względu na sposób traktowania elementów pokrycia ([36], [10]): podejście „jeden do jednego” (ang. one-to-one), w którym jeden przypadek testowy pokrywa dokładnie jeden element pokrycia; podejście minimalizujące, w którym jeden przypadek pokrywa wiele (możliwie jak najwięcej) elementów pokrycia.
testowy
Wady i zalety obu podejść, a także rozwiązania pośrednie przedyskutujemy przy omawianiu technik projektowania przypadków testowych w rozdziale 8. Etap piąty to stworzenie suit testowych, które norma ISO/IEC/IEEE 29119 nazywa zbiorami testów. Jeden zbiór testów może np. zawierać przypadki testowe, które należy wykonać ręcznie, a inny przypadki, które można zautomatyzować. Innym przykładem może być wydzielenie zbioru testów, które wymagają specjalnej konfiguracji środowiska. Zbiory testów powinny być opisane w specyfikacji procedury testowej.
Ostatnim etapem jest stworzenie procedur testowych. Polega on na uporządkowaniu przypadków testowych w obrębie zbiorów testowych na podstawie zależności opisywanych warunkami wejściowymi i wyjściowymi, a także na podstawie innych wymagań testowych. Na każdym etapie należy zapewnić śledzenie między
poszczególnymi
artefaktami. Na samym końcu procesu powinniśmy mieć obustronne śledzenie między podstawą testów, zbiorami cech, warunkami testowymi, elementami pokrycia, przypadkami testowymi, zbiorami testów oraz procedurami testów. Wszystkie artefakty (np. specyfikacje, projekty, zaakceptowane przez udziałowców projektu.
plany)
powinny
zostać
1 Dokładny opis dokumentów wykorzystywanych w procesie testowym oraz ich zawartości jest zawarty w rozdziale 21. 2 Strategie testowania są omówione w rozdziałach 19 oraz 20. 3 W słowniku ISTQB [6] termin test bed jest przetłumaczony jako... łoże testowe (sic!). Angielskie słowo bed ma kilka znaczeń i w tym kontekście nie oznacza łóżka, ale podstawę, podłoże czegoś. 4 Projekt produktu (systemu) może składać się z takich elementów jak: specyfikacja wymagań, projekt funkcjonalny, projekt techniczny, diagramy UML, projekt bazy danych, specyfikacje interfejsów, atrybuty jakościowe oprogramowania, kryteria akceptacji przy przechodzeniu do poszczególnych faz projektu, przypadki i scenariusze użycia, normy, standardy, regulacje prawne, które produkt musi spełniać.
4. Testowanie w cyklu życia oprogramowania
Testowanie oprogramowania nigdy nie odbywa się w próżni – zawsze dotyczy konkretnego projektu, który jest wytwarzany według swojego własnego cyklu życia. Dlatego proces testowy musi być dopasowany do procesu wytwarzania oprogramowania. Na każdym etapie produkcji oprogramowania przedmiotem testów mogą być zupełnie inne artefakty. Może to mieć wpływ na to, jakie typy testów zostaną przeprowadzone. W tym rozdziale przedstawimy najczęściej spotykane modele wytwórcze i przeanalizujemy ich wady i zalety pod kątem czynności testowych. Następnie opiszemy typowe poziomy oraz typy testów. cykl życia oprogramowania (ang. software life cycle) – okres czasu rozpoczynający się, gdy pojawi się pomysł na oprogramowanie i kończący się, gdy oprogramowanie nie jest już dostępne do użytku; cykl życia oprogramowania zawiera zazwyczaj fazę koncepcji, fazę wymagań, fazę projektowania, fazę implementacji, fazę testów, fazę instalacji i zastępowania, fazę wykorzystania produkcyjnego i pielęgnowania oraz – czasami – fazę wycofania; fazy te mogą na siebie nachodzić, być wykonywane równolegle lub iteracyjnie model cyklu życia (ang. lifecycle model) – podział życia produktu lub projektu na fazy faza wymagań (ang. requirements phase) – przedział czasu w cyklu życia oprogramowania, podczas którego są zbierane i dokumentowane wymagania na oprogramowanie [7]
4.1. Modele wytwarzania oprogramowania Model cyklu życia (lub model wytwarzania oprogramowania) to wysokopoziomowy, abstrakcyjny opis procesu produkcji oprogramowania. Model taki zwykle dzieli proces tworzenia oprogramowania na fazy i określa relacje między nimi. Wybór modelu cyklu życia ma wpływ na podstawowy proces testowy, który musi być dopasowany do modelu wytwórczego. Modele cyklu życia można podzielić na trzy główne grupy: modele sekwencyjne, w których fazy następują po sobie; modele iteracyjne, w których cykl składa się z szeregu strukturalnie podobnych do siebie iteracji, kończących się w momencie osiągnięcia założonego poziomu jakości; w modelach tych duży nacisk kładzie się na testy regresji, a więc i na zagadnienia automatyzacji testów; modele przyrostowe, stanowiące kombinację modeli sekwencyjnych i iteracyjnych; w modelach przyrostowych tworzone są prototypy produktu, z których każdy jest w pełni działającym programem, ale z ograniczoną funkcjonalnością, dodawaną w kolejnych fazach prototypowania. Niektóre podejścia łączą w sobie cechy dwóch ostatnich grup, dlatego zwykle mówi się o modelach iteracyjno-przyrostowych. iteracyjny model wytwarzania (ang. iterative development model) – metoda wytwarzania oprogramowania, w której projekt jest podzielony na dużą liczbę iteracji; iteracja jest zamkniętym cyklem wytwórczym dającym działającą wersję produktu (wewnętrzną lub zewnętrzną) będącą podzbiorem finalnego produktu, który rozrasta się z iteracji na iterację aż do powstania produktu końcowego przyrostowy model wytwarzania (ang. incremental development model) – model wytwarzania oprogramowania, w którym przedsięwzięcie jest realizowane przyrostowo, w cyklach, z których każdy dostarcza część funkcjonalności z całego zbioru wymagań; wymagania są porządkowane według priorytetów i realizowane w tej kolejności w odpowiednich przyrostach; w niektórych
wersjach tego modelu każdy podprojekt jest realizowany zgodnie z „mini” modelem V z fazami projektowania, kodowania i testowania priorytet (ang. priority) – poziom (biznesowej) ważności określony dla elementu, np. wymagania, defektu, testu
4.1.1. Model kaskadowy Model kaskadowy (nazwany przez Winstona Royce’a modelem wodospadowym) jest klasycznym modelem sekwencyjnym. Składa się z liniowo uporządkowanych, następujących po sobie faz. W ścisłym modelu kaskadowym po zakończeniu danej fazy nie da się już do niej powrócić. Został on przedstawiony na rysunku 4.1. Istnieją odmiany tego modelu, które pozwalają na powrót do faz wcześniejszych. Zauważmy, że niezależnie od tego, czy mamy do czynienia z modelem ścisłym, czy dopuszczającym powroty, faza testowania zawsze rozpoczyna się dopiero po ukończeniu implementacji. Model nie uwzględnia bezpośrednio możliwości wykorzystania faz wcześniejszych do planowania i projektowania testów. Błędy będą znajdowane dopiero po fazie implementacji, co zwiększa koszt ich usunięcia. Model sprawdza się w projektach, w których wymagania są bardzo dobrze określone i nie będą się zmieniać w czasie trwania prac deweloperskich, aczkolwiek coraz rzadziej można spotkać organizację, która wprost przyznawałaby się do stosowania tego modelu. Obecnie nie przystaje on do większości technik wytwarzania oprogramowania.
Rysunek 4.1. Model kaskadowy (wodospadowy)
4.1.2. Model V Model V jest sekwencyjnym modelem starającym się przezwyciężyć wady modelu kaskadowego. Jego schemat przedstawiono na rysunku 4.2. Szare strzałki pokazują sekwencyjność faz, a ich graficzny układ tworzy literę V – stąd nazwa modelu. Jego lewe ramię pokazuje następstwo faz wytwórczych, a prawe – następstwo faz testowych. Testowanie wciąż odbywa się po fazie kodowania, ale poziome strzałki biegnące od faz testowych do faz projektowych sugerują bardzo istotną rzecz, stanowiącą główną ideę modelu V. Chodzi o to, że czynności testowe powinny rozpocząć się jak najwcześniej. Podczas zbierania wymagań mogą powstawać plany testów akceptacyjnych; w fazie specyfikacji powstają plany testów systemowych itd. Ponadto, w każdej fazie wytwórczej (lewe ramię modelu) przeprowadza się przeglądy i inspekcje produktów faz poprzednich, co reprezentują czarne strzałki idące w górę ramienia. Zarówno inspekcje, jak i plany testów pozwalają już na początkowych etapach wytwórczych znaleźć wiele potencjalnych problemów i umożliwiają ich wczesne usunięcie.
Zalety modelu V to: klarowne wyróżnienie i odróżnienie od siebie faz cyklu życia oraz zależności między nimi; wymóg jak najwcześniejszego tworzenia dokumentacji testowej; równomiernie rozłożony nacisk na fazy tworzenia i fazy testowania.
Rysunek 4.2. Model V Model V jest równocześnie krytykowany z kilku powodów. Przede wszystkim jest modelem sekwencyjnym – w rzeczywistych warunkach wymagania zawsze się zmieniają; model tego nie uwzględnia. Brian Marick [37] zarzuca modelowi V rozdzielenie różnych poziomów testów (np. osobne wykonywanie testów jednostkowych i osobne testów integracyjnych). To rozdzielenie powoduje np. w przypadku testów jednostkowych konieczność tworzenia wielu namiastek bądź sterowników. Marick uważa, że lepszym rozwiązaniem jest testowanie modułów podczas integracji, gdy wszystkie otaczające testowany moduł systemy i inne moduły są już zaimplementowane. Jednocześnie sugeruje, że wszystkie elementy na prawej gałęzi modelu powinny po prostu nazywać się „wykonaj odpowiednie testy”. Krytyka dotyczy również zbytniej prostoty samego modelu – projekty
informatyczne często są bardzo skomplikowane i trudno opisać ich złożoność za pomocą prostych modeli. Może to bezpieczeństwa.
dawać menedżerom
złudne poczucie
model V (ang. V model) – opis czynności cyklu wytwarzania oprogramowania od specyfikacji wymagań do pielęgnacji. Model V ilustruje, jak czynności testowe mogą być integrowane z każdym etapem cyklu życia wytwarzania oprogramowania
4.1.3. Model W Model W jest modyfikacją modelu V, w której wprost opisuje się tworzenie planów testów podczas kolejnych faz wytwórczych. Model jest przedstawiony na rysunku 4.3. W zasadzie cechy modelu W można opisać, rozszerzając nieco opis modelu V. Wszystkie argumenty
Rysunek 4.3. Model W za i przeciw modelowi V można powtórzyć pod adresem modelu W, dlatego nie będziemy go szczegółowo omawiać.
4.1.4. Rational Unified Process (RUP)
Rational Unified Process należy do rodziny modeli iteracyjnych, choć w zasadzie jest „produktem w postaci procesu”, czyli adaptacyjnym frameworkiem, który można konfigurować i dopasowywać do swoich potrzeb. RUP został stworzony w 1996 roku przez Rational Software. RUP definiuje 9 tzw. dyscyplin (wymiar zawartości) podzielonych na 4 etapy (wymiar czasu) [38], [39]. Architektura RUP jest przedstawiona na rysunku 4.4. Wykresy po prawej stronie pokazują intensywność prac w ramach poszczególnych dyscyplin w funkcji czasu. W każdej dyscyplinie zdefiniowanych jest wiele ról i artefaktów. Jeśli chodzi o dyscyplinę testowania, to RUP wymienia cztery główne role: kierownik testowania – związanych z testowaniem;
odpowiadający
za
powodzenie
prac
analityk testowania – wybiera metody testowania, definiuje testy, monitoruje ich postępy; projektant testowania – wybiera właściwe techniki, narzędzia i zasady implementowania testów, zgłasza zapotrzebowanie na zasoby niezbędne do realizacji zadań testowania; tester – wykonuje testy, porównuje wyniki, ocenia przebieg testowania, rejestruje żądania zmian. Jeśli chodzi o artefakty dla procesu testowego, to RUP wymienia ich bardzo wiele, wraz z opisem zależności między nimi. W większości są to klasycznie stosowane dokumenty, takie jak: plan testów, strategia testowania, przypadki testowe, konfiguracja środowiska
Rysunek 4.4. Model RUP testowego, suity testowe, scenariusze testowe, dziennik testów, żądania zmiany. Niestandardowym dokumentem jest „spis pomysłów testowania”, który tworzy się w wyniku zaistnienia żądania zmiany. RUP zawiera także diagram przebiegu prac testowania ze szczegółami i zależnościami między nimi. RUP wymaga testowania poszczególnych funkcjonalności systemu w miarę ich implementowania. Zastosowanie podejścia iteracyjnego w RUP wymusza częste testowanie oraz wczesne rozpoczęcie tej czynności. Duży nacisk jest położony na testy regresji. RUP można dostosować do większości projektów, ale należy uważać, by nie ulec fałszywemu przekonaniu, że samo wykorzystanie wszystkich proponowanych przez model artefaktów sprawi, że proces będzie przebiegał bez problemów. Zadaniem kierownika testów jest to, aby proces oferowany przez RUP skroić na miarę dla konkretnego projektu. Rational Unified Process, RUP – komercyjna, adaptowalna struktura iteracyjnego wytwarzania oprogramowania składająca się z czterech faz życia projektu: rozpoczęcie, opracowanie, tworzenie, przekazanie
4.1.5. Rapid Application Development (RAD) Model RAD, czyli model szybkiego prototypowania jest prekursorem metodyk
zwinnych. Stawia on nacisk na szybkie dostarczanie prototypów. Cadle i Yeats [40] wskazują na zastosowanie modelu RAD w celu uzyskania przewagi konkurencyjnej, gdzie akceptujemy fakt, że wdrażany system może być niedoskonały. Schemat modelu RAD (przebieg jednej iteracji) jest przedstawiony na rysunku 4.5.
Rysunek 4.5. Model szybkiego prototypowania RAD Cechą odróżniającą RAD od bardziej klasycznych, sekwencyjnych modeli wytwarzania oprogramowania jest to, że iteracje i udoskonalania są traktowane jako integralna część procesu, a nie jako niepożądane zdarzenia, których należy unikać. Istnieje kilka szkół myślenia na temat RAD, ale jedną z cech wspólnych dla nich wszystkich jest to, że testowanie traktuje się jako integralną część cyklu iteracyjnego. Model jest stosowany w środowisku, w którym wytwarzanie kolejnych iteracyjnych bądź przyrostowych wersji oprogramowania następuje bardzo szybko. Menedżer testów nie może więc pozwolić sobie na precyzyjne planowanie procesu testowego. Podejście analityczne może się tu nie sprawdzić. Lepsza będzie strategia reaktywna testowania. Jednocześnie, model RAD kładzie nacisk na stosowanie technik komponentowych [41], co może ułatwić proces testowy, gdyż powtórnie używane komponenty są już zwykle przetestowane.
4.1.6. Model spiralny Boehma Model spiralny został zaproponowany przez Boehma w 1986 roku [42]. Jest to model z rodziny iteracyjno-przyrostowej. Jego schemat jest przedstawiony na rysunku 4.6. Cykl życia jest tu opisany jako ewolucja następujących po sobie faz zakończonych dostarczeniem kolejnych wersji prototypów. Ostatni cykl spirali
można traktować jak model kaskadowy. Podejście ewolucyjne dobrze sprawdza się w projektach, w których wymagania podlegają częstym zmianom. Kluczową cechą modelu spiralnego jest analiza ryzyka, występująca przed początkiem każdej fazy. Podejście oparte na ryzyku umożliwia łączenie w ramach modelu spiralnego elementów podejść opartych na specyfikacji, prototypach, symulacji itd. Boehm w swojej późniejszej pracy [43] opisał wręcz model spiralny jako „generator modeli procesów”, w którym wybory oparte na analizie ryzyka projektowego generują właściwy model procesu dla tego projektu. W takim ujęciu modele: kaskadowy, V, RUP, zwinne, inkrementacyjne i inne mogą być traktowane jako szczególne postaci modelu spiralnego. W modelu spiralnym proces testowy rozpoczyna się w początkowych fazach projektu. Jeśli chodzi o strategię testowania, to sugeruje on wybór strategii analitycznej opartej na ryzyku. Model uwzględnia zmieniające się wymagania, co może stanowić element analizy ryzyka. W takim przypadku kierownik testów, w porozumieniu z analitykiem testów, może odpowiednio zaplanować czynności testowe oraz rozłożyć wysiłek zespołu w procesie tworzenia i wykonywania testów.
Rysunek 4.6. Model spiralny Boehma
4.1.7. Metodyki zwinne Metodyki zwinne to grupa iteracyjnych metod wytwarzania oprogramowania opartych na tzw. manifeście Agile zdefiniowanym w 2001 roku. manifest Agile (ang. Agile Manifesto) – manifest określający zasady stanowiące zwinne wytwarzanie oprogramowania: – ludzie i współpraca ponad procesy i narzędzia; – działające oprogramowanie ponad obszerną dokumentację; – współpraca z klientem ponad formalne ustalenia; – reagowanie na zmiany ponad podążanie za planem [44] Do grupy metodyk zwinnych zalicza się m.in. Scrum, Dynamic Systems Development Method (powstały na podstawie metodyki RAD), Test Driven Development, Behavioral Driven Development, Feature Driven Development, z których najpopularniejszą obecnie jest Scrum. Często do grupy metodyk zwinnych zalicza się tzw. programowanie ekstremalne (eXtreme Programming, XP), jednak jest to raczej paradygmat wytwarzania oprogramowania podany w formie tzw. dobrych praktyk niż dobrze zdefiniowana metodyka. wytwarzanie ukierunkowane na cechy (ang. Feature-Driven Development, FDD) – iteracyjny i przyrostowy proces wytwarzania oprogramowania ukierunkowany na punkt oceny funkcjonalności (cechy) przez jej wartość dla klienta. Wytwarzanie ukierunkowane na cechy jest wykorzystywane głównie w zwinnym wytwarzaniu oprogramowania Z punktu widzenia testera zasadniczą wadą metodyk zwinnych, która prawdopodobnie wynika z ich wciąż dość młodego wieku, jest brak spójnej koncepcji zarządzania jakością. O ile metodyki zwinne bardzo dobrze opisują sam proces wytwórczy, o tyle wciąż nie ma zgody co do właściwego opisu procesów dotyczących zapewniania jakości, a w szczególności testowania. Niektórzy twierdzą, że zwinne testowanie jest lub powinno być czymś zupełnie innym niż w starszych modelach, inni uważają, że ma ono dokładnie taką samą formę jak testowanie w metodykach klasycznych. Powstaje dużo publikacji poświęconych tej tematyce (np. [45]). Zwraca się coraz większą uwagę na testy niefunkcjonalne
(zwykle metodyki zwinne kojarzone są wyłącznie z podejściem TDD – Test Driven Development, wykorzystywanym głównie w testach jednostkowych), na rolę testów w dokumentowaniu z klientem.
wymagań,
automatyzację,
ścisłą
współpracę
zwinne wytwarzanie oprogramowania (ang. agile software development) – grupa metodyk wytwarzania oprogramowania oparta na iteracyjnym, przyrostowym modelu wytwarzania oprogramowania, w którym wymagania i rozwiązania ewoluują przez współpracę w ramach samoorganizujących się, realizujących wiele funkcji zespołów Niezależnie od wciąż istniejącego zamieszania co do zarządzania jakością w metodykach zwinnych, da się w nich wyodrębnić pewne techniki czy praktyki związane z testowaniem. Omawiamy je dalej w kolejnych podrozdziałach. testowanie zwinne (ang. agile testing) – metoda testowania stosowana w projektach korzystających z metodologii zwinnych stosująca techniki i metody takie jak programowanie ekstremalne (XP), traktująca wytwarzanie jako klienta testowania i kładąca nacisk na podejście „najpierw test” (ang. test driven) 4.1.7.1. XP Programowanie ekstremalne [46] (ang. eXtreme Programming) proponuje bardzo ciekawą z punktu widzenia testowania technikę, mianowicie tzw. programowanie w parach. Polega ona na tym, że proces tworzenia kodu odbywa się przy udziale dwóch osób, z których jedna koduje, a druga kontroluje to, co jest kodowane. Technika ta jest bardzo skuteczna w błyskawicznym wykrywaniu wielu błędów dzięki niezależnej „drugiej parze oczu”. Osoba niepisząca nie musi koncentrować się na czynności kodowania, dzięki czemu może w pełni skupić się na samym kodzie. Część błędów mogłaby zostać wyłapana przez kompilator, ale są one poprawiane na bieżąco, dlatego oszczędzamy czas na nieudane kompilacje i wyszukiwanie błędów w gotowym, kompilowalnym kodzie. Jeśli w projekcie możemy pozwolić sobie na przeznaczenie dwóch programistów do pisania jednego kodu, to zdecydowanie powinniśmy to zrobić.
programowanie ekstremalne (ang. eXtreme Programming, XP) – zbiór praktyk inżynierii oprogramowania wykorzystywanych w ramach zwinnego wytwarzania oprogramowania. Podstawowe praktyki to: programowanie w parach, wykonywanie dokładnych przeglądów kodu, testowanie modułowe całego kodu, jasność i przejrzystość kodu programowanie parami (ang. pair programming) – metoda wytwarzania oprogramowania, w której linie kodu (produkcyjne i/lub testowe) modułu są pisane przez dwóch programistów siedzących przy jednym komputerze. Domyślnie oznacza to odbywający się w czasie rzeczywistym przegląd kodu Można nieco zmodyfikować zasadę programowania w parach i jeszcze bardziej zwiększyć skuteczność jej stosowania, gdy programiście towarzyszyć będzie nie drugi deweloper, ale tester. Poza tym, że na bieżąco będzie wskazywać popełniane przez programistę błędy, tester może sobie notować na boku uwagi dotyczące tego, co należy przetestować w dalszych etapach tworzenia oprogramowania, w szczególności podczas testów integracyjnych. Uwagi te mogą mu przychodzić do głowy na skutek obserwacji pracy programisty oraz rozmawiania z nim podczas tworzenia kodu. Ponadto, tester w trakcie prac deweloperskich może sugerować programiście rozwiązania projektowe zwiększające testowalność kodu. To podejście będzie skuteczne, gdy tester ma również jakieś doświadczenie w tworzeniu oprogramowania (musi co najmniej znać wykorzystywany w kodowaniu język programowania). skuteczność (ang. effectiveness) – zdolność do osiągania zamierzonego celu 4.1.7.2. Scrum Podstawy metodyki Scrum zostały opisane w [47]. Metodyka ta zakłada istnienie samoorganizujących się zespołów w projekcie, w którym występują częste zmiany wymagań. Jest metodyką iteracyjną. Proces wytwarzania składa się z przebiegów (tzw. sprintów) trwających zwykle od 2 tygodni do 1 miesiąca. Każdy przebieg jest złożony z przebiegów dziennych. Cechami charakterystycznymi Scruma są: przebiegi (sprinty, iteracje), czyli ściśle określone ramami czasowymi okresy stanowiące podstawowe jednostki wysiłku zespołu, w ramach
których realizowane są poszczególne funkcjonalności systemu; codzienne spotkania (ang. stand-up meeting), zwykle odbywane na stojąco, aby wymusić szybki i sprawny ich przebieg; na spotkaniach tych omawia się zakres prac wykonanych w poprzednim dniu, podział prac w nadchodzącej iteracji, problemy, z jakimi członkowie zespołu, metody ich rozwiązywania itp.;
spotkali
się
Rysunek 4.7. Model Scrum backlog sprintu (lista zaległości), czyli spis wszystkich prac, które pozostały do wykonania w ramach danego przebiegu. Elementy do backlogu sprintu są wybierane z backlogu produktu przez zespół scrumowy, na podstawie rozmów z klientem oraz własnej oceny. Metodyka Scrum nie skupia się na zagadnieniach jakości. W niektórych wersjach modelu można spotkać się z dokumentem „Definition of Done” zawierającym kryteria wyjścia dla zakończenia prac nad danym produktem z backlogu. Zwykle kryteria te zawierają wymóg zaliczenia wszystkich testów regresji. SCRUM (ang. SCRUM) – iteracyjna, przyrostowa metodologia zarządzania projektem, powszechnie stosowana w zwinnym wytwarzaniu oprogramowania W Scrum, jako metodyce iteracyjnej, uwzględniającej częste zmiany wymagań, należy zwrócić szczególną uwagę na projektowanie i utrzymywanie zestawów testów regresyjnych. Istnieje pewna sprzeczność między celami jakościowymi
a filozofią metodyk zwinnych: metodyki te z jednej strony nie przykładają dużej wagi do dokumentacji, ale z drugiej zaś, ze względu na częste zmiany wymagań jest niezwykle istotne, aby móc śledzić testy wstecz do tych wymagań. Nie da się tego zorganizować dobrze bez odpowiedniego narzutu dokumentacyjnego. Więcej informacji o Scrum Czytelnik może znaleźć w książkach [48] i [49]. Pozycja [50] omawia dokładnie testowanie w tej metodyce. 4.1.7.3. TDD Test Driven Development (TDD), czyli technika wytwarzania oparta na testach, polega na tworzeniu testów dla danego modułu zanim rozpocznie się właściwe tworzenie tego modułu. TDD obecna była w eXtreme Programming jako jedna z dobrych praktyk testowania, ale obecnie wydzieliła się z XP i rozwinęła w osobną, samodzielną metodykę. Nie obejmuje ona całości procesu testowego. Skupia się głównie na niższych poziomach testowania (testowanie komponentów) i jest związana raczej z pracą programisty niż testera.
Rysunek 4.8. TDD Filozofia TDD [51] jest w swym założeniu bardzo prosta. Schematycznie jest pokazana na rysunku 4.8. Opisuje styl programowania składający się z trzech kroków: pierwszą czynnością jest zaprojektowanie i napisanie testu. Test ten oczywiście nie będzie działał, bo nie ma kodu do testowania. Drugi krok polega na napisaniu tego kodu. Po jego napisaniu test jest ponownie uruchamiany i powinien być zaliczony. Wtedy kod poddawany jest refaktoryzacji i można przystąpić do pisania kolejnego fragmentu, znów rozpoczynając od napisania testu.
wytwarzanie sterowane testami (ang. Test Deiven Development, TTD) – sposób wytwarzania oprogramowania, w którym przypadki testowe są przygotowywane i często automatyzowane zanim powstanie oprogramowanie, które będzie testowane za ich pomocą Takie podejście ma kilka zalet. Po pierwsze, zaczynamy od zaprojektowania testu, dlatego już na tym etapie możemy odkryć pewne pułapki i niebezpieczeństwa związane np. z niejasnymi bądź sprzecznymi wymaganiami. Skupiając się na teście, a nie na kodzie programista ukierunkowuje swoje myślenie na to, co może pójść źle. Wiedza ta ma charakter prewencyjny – podczas pisania kodu programista jest już świadomy różnych zagrożeń i dlatego tworzony kod będzie miał lepszą jakość, niż w przypadku rozpoczęcia kodowania bez uprzedniego tworzenia testu. Po drugie, jeśli uda nam się w ten sposób zmniejszyć gęstość defektów, to zapewnianie jakości może stać się czynnością proaktywną, a nie reaktywną. Po trzecie, niższa gęstość defektów i lepsza jakość kodu pozwalają menedżerom w lepszym szacowaniu parametrów projektu, ponieważ wystąpi mniej niespodziewanych zdarzeń zaburzających planowe czynności wytwórcze. Po czwarte, element refaktoryzacji sprawi, że tworzony kod będzie lepiej poddawał się testowaniu. 4.1.7.4. BDD Filozofia podejścia BDD jest oparta na TDD. W BDD, tak jak w TDD, zanim napisze się kod, tworzy się test. Różnica jest taka, że w TDD jest to zwykle test jednostkowy. W BDD są to raczej testy akceptacyjne, związane z logiką biznesową programu. Tworzy się je w postaci historyjki użytkownika (zwanej scenariuszem), opisującej pożądane zachowanie fragmentu kodu, który chcemy stworzyć. Historyjka składa się z trzech części: given, when oraz then („mając zadane…”, „gdy…”, „wtedy…”). Testy są pisane w języku naturalnym, a odpowiednie narzędzie (patrz p. 30.6.10) przypasowuje odpowiednie fragmenty zdań języka naturalnego do wzorców transformowanych na wykonywalny kod, dzięki czemu wykonywanie testów jest automatyzowane. Rozważmy prosty przykład oparty na [52]. Chcemy stworzyć kalkulator, w którym operator jest poprzedzany operandami (czyli np. działanie 3+4 polega najpierw na wpisaniu liczb 3 i 4, a potem naciśnięciu klawisza +). Oto przykładowy scenariusz, opisujący jedno z wymagań wobec tego kalkulatora:
Cecha: dzielenie Kalkulator mus i mieć możliwoś ć wykonania dzielenia dwóch liczb S cenarius z: dzielenie pros tych liczb całkowitych * Wpis ałem 3 do kalkulatora * Wpis ałem 2 do kalkulatora * Nacis nąłem / * Kalkulator powinien wyś wietlić 1.5 Scenariusz ten jest napisany w języku naturalnym. Aby obsłużyć taką historyjkę, należy napisać kod transformujący wyrażenia z języka naturalnego na konkretne operacje w systemie. Przykład takiego kodu jest pokazany na listingu 4.1.
Before do @calc = Calculator.new end After do end Given /Wpisałem (\d+) do kalkulatora/ do |n| @calc.push n.to_i end When /Nacisnąłem (\w+)/ do |op| @result = @calc.send op end Then /Kalkulator powinien wyświetlić (.*)/ do |result| @result.should == result.to_f
end Listing 4.1. Historyjka użytkownika w podejściu BDD Wyrażenia w nawiasach /.../ oznaczają frazy wyszukiwane w tekście. W nawiasach okrągłych znajdują się wartości, które są przypisywane do zmiennych o nazwach ujętych w znaki |...|. Na przykład tekst „Nacisnąłem /” zostanie przypisany do wzorca /Nacisnąłem (\w+)/, przy czym zmienna op przyjmie wartość „/”. Składnia wyrażeń w nawiasach okrągłych jest związana z obsługą wyrażeń regularnych1, np. „.*” oznacza dowolny ciąg znaków, „\d+” – dowolny niepusty ciąg cyfr itd. Dla każdego przypasowanego fragmentu kod wykonuje odpowiednią operację, np. dla tekstu w sekcji Given jest wykonywana instrukcja calc.push n.to_i oznaczająca wpisanie do kalkulatora wartości zmiennej n zrzutowanej na typ całkowity. Sekcje Before oraz After w skrypcie zawierają instrukcje służące odpowiednio do montowania i rozmontowania środowiska testowego, potrzebnego do przeprowadzenia danego testu. W naszym przypadku sekcja Before składa się z jednej instrukcji, tworzącej instancję kalkulatora, na której będą wykonywane zadane operacje. Sekcja After jest pusta – po zakończeniu testu nic nie musimy rozmontowywać. Osoba tworząca wymagania w postaci scenariuszy nie musi znać języka programowania. Wystarczy, że będzie posługiwała się odpowiednimi frazami języka naturalnego. Skrypt przetransformuje to na konkretne operacje w programie i przetestuje taką historyjkę, porównując wynik wykonania z tym, co zostało zapisane w sekcji Then. W podejściu BDD konstruujemy najpierw scenariusz testowy zachowania systemu, a następnie tworzymy kod, który zaliczy ten test.
4.1.8. Metodologia Cleanroom Metodologia Cleanroom postrzega proces wytwarzania oprogramowania jako czynność stricte inżynierską, opartą na podstawach naukowych, głównie na metodach statystycznych i matematycznych [53]. Aspekt tworzenia produktu jako proces programistycznych prób i błędów ma tu drugorzędne znaczenie. Innymi przykładami tego typu formalnych podejść są Wiedeńska Metoda Produkcji (ang. Vienna Development Model, VDM) oraz notacja Z [54], [55].
W podejściu Cleanroom dąży się do osiągnięcia przez produkt określonego poziomu niezawodności przez rygorystyczny proces kontroli nastawiony na zapobieganie
defektom
niż
na
ich
późniejsze
wykrywanie
i
usuwanie.
W osiągnięciu tego celu pomaga wykorzystanie metod formalnych podczas projektowania oprogramowania, metod statystycznej kontroli jakości podczas inkrementacyjnych testowania.
faz
wytwórczych
oraz
statystycznego
podejścia
do
kontrola jakości (ang. quality control) – operacyjne techniki i działania, część zarządzania jakością, koncentrująca się na spełnieniu wymagań jakościowych [56] Testowanie (w Cleanroom nazywane certyfikacją) oparte jest na formalnej specyfikacji, wykorzystywanej do tworzenia profili operacyjnych programu, czyli oczekiwanych sposobów jego użycia. Na ich podstawie projektuje się testy tak, aby skupić się głównie na najczęściej wykorzystywanych obszarach funkcjonalności testowanego programu. Poziom jakości nie jest obliczany jako gęstość defektów, ale wyrażany w terminach odchyleń standardowych, co przypomina podejście stosowane w Six Sigma. Chodzi tu o to, aby tak zminimalizować zmienność procesu, aby mierząca go wartość, odchylając się od wartości przeciętnej o określoną liczbę odchyleń standardowych, wciąż mieściła się w założonych granicach normy. Inną stosowaną w Cleanroom metryką jest metryka średniego czasu między awariami, która służy do oceny jakości produktu.
Rysunek 4.9. Metodologia Cleanroom certyfikacja (ang. certification) – proces potwierdzający, że moduł, system lub osoba spełnia określone wymagania. W stosunku do osób przyznanie certyfikatu może być uzależnione od zdania egzaminu Stosowanie metodologii Cleanroom wymaga od członków zespołu znajomości metod formalnych i statystyki. Jest to często trudne do osiągnięcia. Zwykle decyzja o stosowaniu tej metodologii wiąże się z zaplanowaniem szkoleń dla wszystkich członków zespołu. Koszt ten jest jednak wart rozważenia, gdyż źródła (np. [57]) podają, że średnia gęstość defektów w wykonywanym po raz pierwszy kodzie wynosiła ok. 2,9 defektów/KLOC, co było znacznie lepszym rezultatem niż przeciętna wartość przemysłowa.
4.2. Weryfikacja i walidacja
Pojęcia weryfikacji i walidacji są używane w testowaniu oprogramowania bardzo często, jednak chyba do dziś nie ma pełnej zgody co do ich precyzyjnej definicji oraz rozróżnienia. Poniżej podajemy definicje tych pojęć za normą ISO 9000 [1]. weryfikacja (ang. verification) – egzaminowanie poprawności i dostarczenie obiektywnego dowodu, że produkt procesu wytwarzania oprogramowania spełnia zdefiniowane wymagania walidacja (ang. validation) – sprawdzenie poprawności i dostarczenie obiektywnego dowodu, że produkt procesu wytwarzania oprogramowania spełnia potrzeby i wymagania użytkownika Gdy porównamy te definicje, zobaczymy, że brzmią bardzo podobnie, a różnią się jedynie tym, że walidacja dotyczy spełnienia potrzeb użytkownika, podczas gdy weryfikacja odwołuje się do wymagań niekoniecznie dotyczących wprost użytkownika końcowego. Jest to główne kryterium rozróżnienia tych pojęć. Ostatecznym i nadrzędnym celem procesu produkcji oprogramowania jest wytworzenie i dostarczenie klientowi (użytkownikowi) produktu, który będzie spełniał jego wymagania. Na przykład użytkownicy końcowi systemu ELROJ oczekują, że program ten pomoże im w procesie definiowania i wyświetlania na przystankach rozkładów jazdy autobusów. Sprawdzenie, że gotowy produkt rzeczywiście spełnia te wymagania użytkowe to walidacja. W przeprowadzeniu walidacji można stosować symulacje bądź ewaluacje, np. za pomocą ankiet badających satysfakcję użytkownika z dostarczonego produktu. Aby jednak zbudować takie oprogramowanie, projekt musi przejść przez wiele różnych faz wynikających z przyjętego modelu cyklu życia. Przejście z jednego etapu do drugiego powinno następować dopiero wtedy, kiedy wszystkie czynności fazy poprzedzającej zostaną ukończone i kiedy zespół jest gotowy do przeprowadzenia fazy następującej. Innymi słowy, muszą być spełnione zarówno kryteria wyjścia fazy wcześniejszej, jak i kryteria wejścia fazy kolejnej. Kryteria te zwykle dotyczą czynności technicznych, które w ogóle nie interesują użytkownika końcowego. Na przykład kryterium wyjścia z fazy testów modułowych dla systemu ELROJ może być przetestowanie funkcjonalności metody setInfo zgodnie ze specyfikacją wymagań dla tej metody. Takie sprawdzanie „wewnętrznych” wymagań, przyjętych przez członków zespołu projektowego, to weryfikacja.
Weryfikacja dotyczy więc czynności inżynierskich, „technicznych”. Jest zawsze związana z jakimś fragmentem oprogramowania, nie z gotowym produktem. Walidacja następuje w końcowych fazach i dotyczy gotowego produktu. Weryfikacja bada zgodność z wymaganiami danej fazy, walidacja – zgodność z wymaganiami biznesowymi. Sommerville definiuje weryfikację jako odpowiedź na pytanie „are we building the product right” (czy budujemy produkt dobrze?), a walidację jako odpowiedź na pytanie „are we building the right product?” (czy budujemy dobry produkt?). Techniki testowania można uznać za formę weryfikacji i walidacji. Istnieją różne taksonomie metod walidacji i weryfikacji. Na przykład Balci [58] wyróżnia następujące: nieformalne (m.in.: audyty, inspekcje, kontrola przy biurku, przeglądy, test Turinga); statyczne (m.in.: analiza przepływu danych, analiza semantyczna, analiza strukturalna, analiza składniowa); dynamiczne (m.in.: testowanie czarnoskrzynkowe, testowanie bottomup, debugowanie, śledzenie wykonania, testowanie polowe, testy regresji, techniki statystyczne, testy obciążeniowe, symboliczne, testowanie białoskrzynkowe);
debugowanie
symboliczne (m.in.: grafy przyczynowo-skutkowe, analiza ścieżek, wykonanie symboliczne); formalne (m.in.: indukcja, inferencja, rachunek lambda, dedukcja logiczna, rachunek predykatów, dowodzenie poprawności). W następnych rozdziałach omówimy dokładnie większość z tych technik. Przykładem weryfikacji jest kontrola jakości przez stosowanie tzw. bramek kontroli jakości. bramka kontroli jakości (ang. quality gate) – specjalny kamień milowy w projekcie umieszczany między tymi fazami projektu, które silnie zależą od wyników poprzedzających je faz; bramka kontroli jakości zawiera formalną kontrolę dokumentów z poprzedzającej fazy
Rysunek 4.10. Hierarchia czynności weryfikacji i walidacji wg ISO/IEC/IEEE 29119 W kontekście normy ISO/IEC/IEEE 29119 weryfikacja i walidacja jest z kolei nazywana „analizą weryfikacji i walidacji”, która wraz z dwiema innymi kategoriami: metodami formalnymi oraz testowaniem wchodzi w skład ogółu czynności związanych z zapewnianiem jakości oprogramowania, zwanych „weryfikacją i walidacją” (rys. 4.10).
4.3. Poziomy testów Testowanie zawsze odbywa się w konkretnym modelu cyklu życia, dlatego musi uwzględniać fazy tego cyklu. Poziomy testów odnoszą się właśnie do faz wytwarzania oprogramowania. Opisane typowe poziomy testów są najściślej związane z fazami modeli sekwencyjnych, ale mogą być stosowane w dowolnym modelu wytwórczym. Należy również pamiętać, że wymienione w kolejnych podrozdziałach poziomy nie są jedynymi możliwymi – w jednym projekcie mogą występować dokładnie w takiej formie, w innym może być tylko jeden poziom
testów, w jeszcze innym poziomy mogą wyglądać i nazywać się zupełnie inaczej. Niezależnie od rodzaju i liczby poziomów przyjętych w konkretnym projekcie, istotą rozróżniania poziomów testów jest to, że każdy poziom ma inne cele testowania, ma zwykle inną podstawę testów, a także inny obiekt testowania. Ze względu na odmienną podstawę i cele testowania, na różnych poziomach testów są zwykle wykrywane różne typy defektów. Typowe poziomy to: testy jednostkowe (zwane też komponentów czy unit-testami);
testami
modułowymi,
testami
testy integracyjne; testy systemowe; testy akceptacyjne. Poza tymi czterema klasycznymi poziomami często spotyka się również takie poziomy jak: testy produkcyjne, testy zgodności z umową, testy zgodności legislacyjnej, testy alfa, testy beta. Niektórzy autorzy klasyfikują je jako odmiany testów akceptacyjnych. poziom testów, etap testów (ang. test level, test stage) – grupa czynności testowych, które są razem zorganizowane i zarządzane; poziom testów jest powiązany z poziomami odpowiedzialności w projekcie; przykładami poziomów testów są testy modułowe, integracyjne, systemowe i akceptacyjne [28] faza testów (ang. test phase) – wyróżniony zbiór aktywności testowych zebrany w podlegającą zarządzaniu fazę projektu, np. wykonanie testów na jakimś poziomie testów [59]
4.3.1. Testy jednostkowe
Testy modułowe polegają na wyszukiwaniu usterek w logicznie wydzielonych jednostkach oprogramowania (moduły, klasy, funkcje), które można testować w izolacji od innych jednostek. Jest to typ testów najczęściej wykonywany przez programistów, zwykle z wykorzystaniem narzędzi typu xUnit (np. JUnit). Niektórzy nie uważają tego typu testów za „prawdziwe” testowanie, właśnie ze względu na fakt, iż jest to faza, w której biorą udział zwykle deweloperzy, a nie testerzy. Testy modułowe idealnie nadają się do stosowania podejścia Test Driven Development. testowanie modułowe, testowanie komponentów, testowanie jednostkowe (ang. module testing, component testing, unit testing) – testowanie pojedynczych modułów oprogramowania [7] Testy modułowe wykonuje się w izolacji od innych komponentów, ale często działanie testowanego modułu zależy w jakiś sposób od innych części oprogramowania, które w chwili testów mogą nie być jeszcze gotowe. Są dwa typy takich sytuacji: 1) testowany moduł wywołuje inny komponent (np. funkcję); 2) testowany moduł lub jego część jest wywoływany przez inny komponent. W obu przypadkach zewnętrzne wobec testowanego modułu jednostki mogą jeszcze nie być gotowe i trzeba na czas testowania stworzyć ich „zastępcze” wersje. W pierwszym przypadku zastępczą wersją jest zaślepka, w drugim – sterownik. Niektórzy autorzy rozróżniają dwie2 wersje zaślepek (ang. stub oraz mock). Pierwsza (stub) jest „statycznym” komponentem mającym zawsze ten sam stan, zwracającym zwykle tą samą wartość, a druga (mock) jest bardziej skomplikowaną wersją zaślepki, potrafiącą symulować zachowanie komponentu. Zaślepki i sterowniki składają się na tzw. uprząż testową, zwaną również jarzmem testowym. uprząż testowa, jarzmo testowe (ang. test harness) – środowisko testowe, składające się z zaślepek i sterowników potrzebnych do wykonania testu Podstawą testów są wymagania dla modułów, projekt szczegółowy oraz kod źródłowy. Typowymi obiektami testów modułowych są najmniejsze dające się wydzielić logicznie części oprogramowania, takie jak pojedyncze funkcje, pojedyncze klasy, moduły czy programy. Tester na poziomie testów modułowych ma zwykle dostęp do kodu źródłowego. Proces testowania na tym poziomie cechuje się bardzo małą
formalnością
–
testowanie
oraz
usuwanie
błędów
(debugowanie)
jest
wykonywane bez stosowania formalnego procesu zarządzania usterkami. Wymóg większej formalizacji poziomu testów jednostkowych może mieć miejsce np. w przypadku systemów krytycznych. Podczas testowania jednostkowego często wykorzystuje się odpowiednie środowisko (tzw. strukturę do testów jednostkowych), w którym moduł może być przetestowany w izolacji od pozostałych części systemu. Środowisko takie umożliwia również automatyczne wykonanie testów i w graficzny sposób pokazuje odsetek zdanych testów oraz stopień pokrycia kodu. struktura do testów jednostkowych (ang. unit test framework) – narzędzie dostarczające środowisko do testów jednostkowych. W takim środowisku moduł może być testowany niezależnie (w izolacji) lub z użyciem odpowiednich zaślepek i sterowników. Dostarcza również innego rodzaju wsparcia dla programistów, np. możliwość debugowania [60]
4.3.2. Testy integracyjne Testy integracyjne sprawdzają poprawność współdziałania (interakcji) oddzielnych komponentów systemu oraz poprawność interfejsów między nimi. Przykładem interakcji może być współpraca między programem a systemem plików, ale może ona dotyczyć również komunikacji między dwiema klasami lub nawet dwiema metodami tej samej klasy. Rozważmy system ELROJ z Dodatku A. Występuje w nim między innymi metoda addBusInterval, która do poprawnego działania wymaga wywołań metod addLine oraz addBus. Test poprawności współdziałania tych metod jest przykładem testu integracji. testowanie integracyjne (ang. integration testing) – testowanie wykonywane w celu wykrycia defektów podczas interakcji między komponentami lub systemami W zależności od typu obiektów, których współdziałanie chcemy testować, możemy mówić o testach małej lub dużej skali integracji. Integracja małej skali, jak sama nazwa wskazuje, dotyczy łączenia ze sobą „małych” elementów oprogramowania, takich jak metody czy klasy. Integracja dużej skali polega na łączeniu ze sobą większych elementów, takich jak całe programy lub systemy
w tzw. system systemów. Czasami używa się również rozróżnienia na testy integracji modułów oraz testy integracji systemów. system systemów (ang. system of systems) – mnogie, heterogeniczne rozproszone systemy, które mogą być zagnieżdżone w sieciach na wielu poziomach
i
w
wielu
połączonych
domenach,
ukierunkowane
na
rozwiązywanie interdyscyplinarnych, wspólnych problemów wielkiej skali i służeniu takim celom integracja (ang. integration) – proces łączenia komponentów w większe zespoły integracja podstawowych funkcjonalności systemu (ang. functional integration) – metoda integracji, w której moduły lub systemy łączy się jak najwcześniej w celu uzyskania działającej podstawowej funkcjonalności testowanie integracji modułów, testowanie integracyjne małej skali (ang. component integration testing, integration testing in the small) – testowanie wykonywane w celu wykrycia usterek w interfejsach i interakcjach między integrowanymi komponentami oprogramowania testowanie integracji systemów, testowanie integracyjne zewnętrzne (dużej skali) (ang. system integration testing, integration testing in the large) – testowanie integracji systemów i pakietów oraz interfejsów z obiektami zewnętrznymi wobec systemu (np. zewnętrzne bazy danych, systemy plików, internet, elektroniczna wymiana danych) W testach integracyjnych wyróżnia się również testowanie interfejsów oraz testowanie integracji sprzęt–oprogramowanie. Specjalnym typem testowania interfejsów jest testowanie interfejsu programowania aplikacji API (ang. Application Programming Interface), które bardzo silnie jest oparte na tzw. testowaniu negatywnym w celu sprawdzenia odporności na błędy. Przykładem testowania integracji sprzęt–oprogramowanie w przypadku systemu ELROJ byłoby przetestowanie komunikacji między oprogramowaniem a fizycznym ekranem zainstalowanym na przystanku i wyświetlającym czasy przyjazdu autobusów.
testowanie interfejsu (ang. interface testing) – testowanie wykonywane w celu wykrycia usterek w interfejsach między modułami testowanie interfejsu programowania aplikacji API (ang. API testing) – testowanie kodu, który umożliwia komunikację między różnymi procesami, programami i/lub systemami; testowanie interfejsu programowania aplikacji często zawiera testowanie negatywne, np. aby sprawdzić odporność na syntaktycznie niepoprawne wejścia oraz poprawność obsługi błędów testowanie
integracji
sprzęt–oprogramowanie
(ang.
hardware-software
integration testing) – testowanie wykonywane, by odnaleźć w interfejsach i współdziałaniu między sprzętem i oprogramowaniem
defekty
Podstawą dla testów integracji jest przede wszystkim projekt architektury systemu, a także opis przepływu procesów. W przypadku dużej integracji podstawą testów mogą być także przypadki użycia. Typowymi obiektami testów są interfejsy zapewniające komunikację między modułami. Ważnym obiektem testów, o którym często się zapomina, są dane konfiguracyjne systemu. Konfiguracja różnych ustawień systemowych może mieć wpływ na działanie wszystkich modułów wchodzących w skład aplikacji. Na przykład w systemie ELROJ daną konfiguracyjną może być liczba linii ekranu. Od tej wartości zależy działanie metody przygotowującej dane do wyświetlenia. Innym przykładem danej konfiguracyjnej w systemie ELROJ może być maksymalna długość łańcucha znaków przechowującego komunikat. Liczba ta wpływa na sposób obsługi metod getInfo, setInfo oraz getDisplayString. element konfiguracji (ang. configuration item) – zbiór zawierający sprzęt, oprogramowanie lub te dwie rzeczy, które poddawane są zarządzaniu konfiguracją i traktowane jako pojedyncza składowa w procesie zarządzania konfiguracją [7]) zarządzanie konfiguracją (ang. configuration management) – dyscyplina stosująca techniczne i administracyjne metody kierowania i nadzoru w celu: określenia i udokumentowania charakterystyk funkcjonalnych i fizycznych konfiguracji, kontroli zmiany tych charakterystyk, zapisywania
i
raportowania
wykonywanych zmian i
statusów implementacji
oraz
weryfikacji zgodności z wyspecyfikowanymi wymaganiami [7] informacja o statusie (ang. status accounting)
–
element
zarządzania
konfiguracją składający się z rejestrowania i raportowania informacji potrzebnych do efektywnego zarządzania konfiguracją; informacje te zawierają zestawienie
zaakceptowanych
zaproponowanych zmian zaakceptowanych zmian [7]
elementów
konfiguracji
oraz
konfiguracji, status
status
implementacji
kontrola konfiguracji, kontrola wersji, kontrola zmiany (ang. configuration control, version control, change control) – element zarządzania konfiguracją składający się z oceny, koordynacji oraz udzielenia lub nieudzielenia zgody na zmianę elementów konfiguracji oraz implementacji zmian po formalnej identyfikacji elementu konfiguracji Podczas testowania integracyjnego testerzy powinni skupiać się wyłącznie na aspekcie współdziałania testowanych komponentów, np. na testowaniu interfejsu. Na tym etapie testowania nie interesuje nas poprawność funkcjonalności każdego z modułów z osobna. W przypadku większych projektów, gdy liczba elementów poddawanych testom integracji jest duża, pojawia się następujący problem: w jakiej kolejności powinny być tworzone komponenty, aby jak najefektywniej przeprowadzić testy modułowe oraz integracyjne? Efektywność ta może dotyczyć np. jak najwcześniejszego minimalizowania jak największej liczby ryzyk, ograniczania liczby zaślepek i sterowników koniecznych do przeprowadzenia testów (to zabiera zwykle dużo czasu), chęci wykonania jak najwcześniej testów w celu integracji modułów krytycznych itd. Kolejność tworzenia modułów nie zawsze jest podyktowana wolą testera – zwykle wynika ona z planu projektu. Niemniej jednak testerzy powinni mieć wpływ na podejmowanie decyzji w tym względzie. Bardzo często spotyka się sytuacje, w których etap testowania integracyjnego sprowadza się do tzw. metody wielkiego wybuchu (ang. big bang). Polega ona na testowaniu całkowicie zintegrowanego systemu, który nie był wcześniej testowany na poziomie testów integracyjnych w sposób inkrementacyjny ze względu na brak czasu, zasobów itd. Wadą tej metody jest to, że zwykle podczas jej stosowania testerzy napotykają wiele błędów, które trudno zlokalizować
w postaci usterki, gdyż nie wiadomo który interfejs bądź która interakcja jest za nią odpowiedzialna. Dlatego – z punktu widzenia testowania – rozsądnym podejściem do integracji jest podejście inkrementacyjne. Istnieją dwie podstawowe techniki integracji oparte na architekturze systemu: integracja zstępująca i wstępująca. Systematyczne strategie integracji mogą również być oparte na funkcjonalności (integrowanie w kolejności wyznaczonej funkcjonalnością modułów) czy też na ryzyku. Strategie te omówimy na przykładzie programu przedstawionego na rysunku 4.11. Program ten składa się z dziewięciu modułów, a wywołania (interakcje) między nimi są oznaczone kreskami. Na przykład moduł A wywołuje moduły B, F oraz G, moduł I jest wywoływany zarówno przez moduł D, jak i przez moduł H itd. Strzałka wchodząca do modułu E oznacza, że moduł ten pobiera dane z zewnętrznego źródła danych. Strzałka wychodząca z modułu H symbolizuje wypisywanie przez ten moduł danych na wyjście (w przypadku systemu ELROJ moduł E mógłby zawierać metodę getInfo, a moduł H – metodę getDisplayString). Szare tło modułów D, G oraz I oznacza, że moduły te są krytyczne dla działania systemu i jako takie przypisane mają większą część zidentyfikowanych ryzyk jakościowych (produktowych).
Rysunek 4.11. Projekt architektury przykładowego programu W przypadku integracji metodą zstępującą (ang. top-down) najpierw tworzony jest moduł A, następnie moduły B, F i G, w dalszej kolejności moduły C, D oraz H, a na samym końcu E oraz I. W przypadku integracji metodą wstępującą (ang. bottom-up) najpierw tworzy się moduły położone na najniższych poziomach diagramu zależności, czyli E oraz I. W następnej kolejności tworzy się moduły C, D oraz H, potem B, F, G, a na końcu moduł główny A. testowanie zstępujące (ang. top-down testing) – podejście przyrostowe do testowania integracyjnego, w którym jako pierwszy jest testowany moduł na górze hierarchii, a moduły niższych rzędów są symulowane przez zaślepki; przetestowane moduły są później używane do testowania modułów niższych rzędów; proces ten jest powtarzany, aż zostaną przetestowane moduły leżące najniżej w hierarchii testowanie wstępujące (ang. bottom-up testing) – podejście przyrostowe do testowania integracyjnego polegające na testowaniu modułów najniższego poziomu jako pierwszych, co ułatwia testowanie modułów wyższych poziomów; proces ten jest powtarzany dopóty, dopóki moduł na szczycie hierarchii nie zostanie przetestowany Strategia integracji funkcjonalnej wyznacza kolejność tworzenia modułów tak, aby jak najwcześniej przetestować wybrane obszary funkcjonalne. Załóżmy, że chcemy przetestować funkcjonalność pobierania i wypisywania danych na ekran. Moduł wypisujący H korzysta z modułu pobierającego E, dlatego w pierwszej kolejności należy stworzyć moduły umożliwiające przepływ danych z E do H. Jest to np. ścieżka E, D, B, A, G, H. Moduł G może być wymieniony na moduł F, ale oba wywołują H, a G jest modułem krytycznym, dlatego w pierwszym przypadku oprócz uzyskania ścieżki zapewniającej żądaną funkcjonalność otrzymujemy „gratis” możliwość wczesnego przetestowania modułu o znaczeniu krytycznym. Niektóre z tych modułów mogą być zaślepkami lub sterownikami (np. zamiast tworzyć duży moduł główny A, można stworzyć jego sterownik, który będzie symulował wywoływanie modułów B i G, potrzebnych do zrealizowania naszej ścieżki).
Strategia integracji opartej na ryzyku zakłada, że najwcześniej są tworzone moduły krytyczne, a w testach integracji najwcześniej testuje się integrację modułów krytycznych. W przypadku naszego programu mamy trzy moduły krytyczne: D, G, I, które powinny być stworzone najwcześniej. Po stworzeniu D oraz I od razu możemy przeprowadzić test integracji D–I. W dalszej kolejności można tworzyć moduły wchodzące w interakcję z modułami krytycznymi, a więc moduły B, E (dla interakcji z D), H (dla interakcji z G oraz I) i A dla interakcji z G. Źródłem ryzyka może być też fakt, że część modułów (w integracji małej) lub systemów (w integracji dużej) jest tworzona przez stronę trzecią, niezależnie od organizacji rozwijającej system. Na przykład moduł G może być dostarczany przez firmę zewnętrzną. W takim przypadku można uznać, że testy integracji A–G oraz G–H powinny być wykonane szczególnie starannie. Zasadniczo, po każdym stworzeniu komunikujących się ze sobą modułów X i Y można przeprowadzić testy integracji X–Y. Kolejność tworzenia modułów ma również wpływ na efektywność testów modułowych. Jeśli np. moduły są tworzone sekwencyjnie, a testowanie modułowe chcemy rozpocząć jak najwcześniej, to w przypadku strategii zstępującej po dostarczeniu modułu A programista musi stworzyć zaślepki dla B, F i G, aby w pełni przetestować działanie A. Musi również stworzyć zaślepki: C i D do przetestowania B, H do przetestowania F i G, E do przetestowania D oraz I do przetestowania D i H. W tym przypadku programista musi w sumie stworzyć 8 zaślepek. Oczywiście chcąc przetestować integrację modułu A z B, F i G, możemy po prostu poczekać, aż te trzy ostatnie moduły zostaną stworzone. W takim przypadku proces testowania rozpoczyna się później (następuje przesunięcie go w czasie).
4.3.3. Testy systemowe Z testami systemowymi mamy do czynienia, gdy poszczególne elementy systemu zostały z sobą zintegrowane w spójny, działający system. Jest to poziom, na którym można sprawdzać wysokopoziomową funkcjonalność systemu oraz przeprowadzać pełne scenariusze testowe z punktu widzenia użytkownika (nazywane czasem end-to-end testing), od momentu zalogowania się do systemu po jego opuszczenie. Na tym etapie najpełniej wykorzystuje się czarnoskrzynkowe metody testowania, a także najczęściej przeprowadza się testy niefunkcjonalne, na przykład testy wydajności, niezawodności czy bezpieczeństwa. Testowanie systemowe uważa się za jeden z najtrudniejszych do przeprowadzenia poziomów
testów [61]. Programiści rzadko w nim uczestniczą – testy systemowe są zwykle wykonywane przez niezależny zespół testerski. testowanie systemowe (ang. system zintegrowanego systemu w celu z wyspecyfikowanymi wymaganiami [11]
testing) – proces sprawdzenia jego
testowania zgodności
Zadaniem testowania systemowego jest porównanie stworzonego programu z jego pierwotnymi celami projektowymi. Podstawę testów stanowią więc wymagania systemowe, przypadki użycia, specyfikacja funkcjonalna, a także specyfikacja procesów biznesowych realizowanych przez system. Obiektami testów są: sam system, podręczniki systemowe, dokumentacja użytkowa, a także konfiguracja systemu. Testy systemowe dotyczą gotowego systemu, który działa w konkretnym środowisku. Dlatego bardzo ważne jest, aby środowisko testowe przypominało w jak największym stopniu docelowe środowisko produkcyjne. Czasami kwestia środowiska nie ma żadnego lub prawie żadnego znaczenia, czasami wymaga stosowania technik kombinacyjnych (zwłaszcza w przypadku produktów z półki, dla których nieznane jest docelowe środowisko), a czasem przygotowanie środowiska testowego jest zajęciem niezwykle skomplikowanym (np. dla systemów rozproszonych lub oprogramowania telekomunikacyjnego, które jest bardzo silnie związane z infrastrukturą telekomunikacyjną). Na przykład środowisko testowe dla systemu ELROJ powinno zawierać w szczególności egzemplarz modelu wyświetlacza, jaki będzie montowany na przystankach autobusowych. środowisko
produkcyjne
(ang.
operational
environment)
–
sprzęt
i oprogramowanie zainstalowane w siedzibie użytkownika lub klienta, w którym moduł lub system będzie używany; w skład oprogramowania mogą wchodzić systemy operacyjne, bazy danych i inne aplikacje
4.3.4. Testy akceptacyjne Testy akceptacyjne są zwykle wykonywane przez klienta lub użytkowników końcowych na gotowym, w pełni działającym produkcie, choć można również przeprowadzać testy akceptacyjne dla oprogramowania z niepełną
funkcjonalnością (np. test akceptacyjny jednego systemu wchodzącego w skład systemu systemów). Oprogramowanie z półki może być poddane testom akceptacyjnym w momencie instalacji lub integracji z innymi systemami; testy akceptacyjne mogą dotyczyć nawet pojedynczych modułów, itd. testowanie
akceptacyjne
(przez użytkownika), akceptacja
(ang.
(user)
acceptance testing, acceptance) – testowanie formalne przeprowadzane w celu umożliwienia użytkownikowi, klientowi lub innemu uprawnionemu podmiotowi ustalenia, czy zaakceptować system lub moduł [7] testowanie akceptacyjne w środowisku użytkownika (ang. site acceptance testing) – testowanie akceptacyjne wykonywane przez użytkowników/klientów w ich środowisku pracy w celu określenia, czy moduł lub system spełnia ich potrzeby oraz czy realizuje procesy biznesowe; standardowo zawierają zarówno testy sprzętu, jak i oprogramowania kryteria akceptacji (ang. acceptance criteria) – kryteria wyjścia, które moduł lub system musi spełniać, aby został zaakceptowany przez klienta, użytkownika lub inny uprawniony podmiot [7] Głównym celem testowania akceptacyjnego Większość z nich powinna zostać znaleziona wykonywanych poziomach testów. Testowanie przede wszystkim do nabrania zaufania do niefunkcjonalnych. Testowanie akceptacyjne
nie jest znajdowanie usterek. na innych, zwykle wcześniej akceptacyjne powinno służyć systemu lub jego atrybutów jest formą walidacji, czyli
potwierdzenia, że produkt spełnia oczekiwania klienta. Często testy akceptacyjne przybierają formę testów niefunkcjonalnych (np. testowanie użyteczności – patrz podrozdz. 15.3). Wyniki testów akceptacyjnych mogą mieć formę ankiet/kwestionariuszy dotyczących zadowolenia klienta z systemu. Ocenie podlegać mogą zarówno aspekty funkcjonalne systemu (co system robi), jak i niefunkcjonalne (jak system pracuje). Nie można oczywiście wykluczyć, że podczas przeprowadzania testów akceptacyjnych, zwłaszcza w siedzibie klienta, w docelowym środowisku produkcyjnym, wykryte zostaną jakieś błędy. Ważne jest, aby zapewnić sobie możliwie najlepszą jakość informacji zwrotnej od klienta dotyczącej zgłaszanej
usterki. Użytkownik nie jest testerem i w szczególności nie zna się na procedurach śledzenia i zgłaszania defektów. Można zastosować różne podejścia do tego problemu, na przykład: przed
przeprowadzeniem
testów
użytkownik
powinien
być
poinformowany w jaki sposób rejestrować bądź zgłaszać błędy; użytkownik powinien przeprowadzać testy w obecności testera lub programisty; działania użytkownika podczas testów powinny być nagrywane, np. za pomocą kamery lub programu rejestrującego akcje użytkownika (np. tzw. keylogger). Dzięki temu zespół testerów i programistów łatwiej zreprodukuje błąd i zlokalizuje miejsce odpowiedzialnej za niego usterki. Należy pamiętać, że koszty usuwania usterek w późniejszych fazach cyklu życia są o wiele wyższe niż koszty usuwania usterek w fazach wcześniejszych. Dlatego trzeba łagodzić ryzyko trudności naprawy błędu znalezionego przez użytkownika, np. przez stosowanie opisanych metod.
4.3.5. Pozostałe poziomy testów Wśród poziomów testów wyróżnia się również testy: produkcyjne, zgodności z umową, zgodności legislacyjnej, alfa oraz beta. Testy produkcyjne lub akceptacyjne testy produkcyjne mają za zadanie akceptację systemu przez administratorów. Typowe czynności podczas tego typu testów to sprawdzanie: poprawności tworzenia i odtwarzania kopii zapasowych systemu (w tym automatycznego tworzenia kopii zapasowych); odtwarzania systemu po awarii; funkcjonalności związanej z zarządzaniem użytkownikami. Pod pojęciem testów produkcyjnych rozumie się także testowanie mające za zadanie ocenić system w jego środowisku produkcyjnym. Testy zgodności z umową to testy sprawdzające, czy wszystkie zawarte w kontrakcie warunki akceptacji zostały spełnione. Testy te występują głównie w przypadku oprogramowania tworzonego na zamówienie, gdzie zawsze mamy
do czynienia z podpisaniem umowy określającej wymagania na system. Może je przeprowadzać
zespół
produkcyjny
na
etapie
testów
systemowych
lub
użytkownik końcowy na etapie testów akceptacyjnych. Raport z testów zgodności z umową powinien być podpisany i zaakceptowany przez obie strony umowy. Stanowi on bowiem formalne potwierdzenie wykonania warunków umowy. Testy zgodności legislacyjnej dotyczą oprogramowania realizującego zadania w obszarze, w którym istnieją regulacje prawne. Na przykład program zarządzający procesem fakturowania w firmie musi spełniać odpowiednie wymagania ustawy o rachunkowości; programy dla instytucji finansowych muszą spełniać wymagania ustawy o bankowości itd. Najczęściej testy te przeprowadza się na etapie testów systemowych lub akceptacyjnych, ale są one najczęściej wykonywane przez zespół testerów lub audytora. Testy alfa i beta (testy polowe) to testy akceptacyjne przeprowadzane przez klienta lub użytkownika końcowego. Testy alfa przeprowadza się u producenta, w środowisku testowym. Oczywiście środowisko to powinno być jak najbardziej zbliżone do docelowego. Testy beta (polowe) przeprowadzane są przez klientów w ich własnych lokalizacjach. Testy alfa i beta są popularne w przypadku produkcji oprogramowania z półki, tworzonego na szeroki rynek. Celem tych testów może być: uzyskanie zaufania docelowego testowanego programu; obliczenie profilów oprogramowania;
operacyjnych,
lub
potencjalnego
czyli
sposobów
klienta
dla
użytkowania
uzyskanie oceny klienta dotyczącej użyteczności bądź innych cech niefunkcjonalnych testowanego programu; zebranie od klientów-testerów uwag dotyczących testowanego oprogramowania, które pomogą zwiększyć jakość produktu. Wyniki testów beta można wykorzystać w pomiarze jakości procesu wytwórczego (np. w mierzeniu efektywności usuwania defektów – patrz rozdz. 45), gdyż błędy (defekty) znalezione przez użytkownika można traktować jak błędy (defekty) polowe (zwane również operacyjnymi), wykryte po wydaniu oprogramowania.
testowanie produkcyjne (ang. operational testing) – 1) testowanie mające na celu ocenę modułu lub systemu w jego środowisku produkcyjnym [7]; 2) forma testowania akceptacyjnego przez administratorów systemu produkcyjne testy akceptacyjne, akceptacyjne testowanie produkcyjne (ang. Operational Acceptance Testing, Production Acceptance Testing, OAT) – testowanie produkcyjne w fazie testów akceptacyjnych, zwykle przeprowadzane w środowisku produkcyjnym będącym symulacją rzeczywistego środowiska docelowego; wykonywane zazwyczaj przez operatora lub administratora, zorientowane na takie charakterystyki jakościowe jak: odtwarzalność, zarządzanie zasobami, łatwość instalacji i zgodność techniczna; patrz także: testowanie produkcyjne testowanie alfa (ang. alpha testing) – symulowane lub rzeczywiste testy produkcyjne przeprowadzane przez potencjalnych użytkowników lub niezależny zespół testowy, przeprowadzane u producenta, ale bez udziału wytwórców oprogramowania; testowanie alfa jest często wykorzystywane jako forma wewnętrznych testów akceptacyjnych dla oprogramowania z półki testowanie beta, testowanie polowe (ang. beta testing, field testing) – testowanie produkcyjne wykonywane przez potencjalnego i/lub istniejącego użytkownika/klienta w zewnętrznym miejscu niezwiązanym z programistami/twórcami – poza organizacją wytwórczą, w celu podjęcia decyzji, czy moduł albo system zaspokaja potrzeby użytkownika/klienta i współgra z procesami biznesowymi; testowanie beta jest często traktowane jako forma zewnętrznych testów akceptacyjnych oprogramowania z półki w celu uzyskania informacji zwrotnej z rynku akceptacyjne testy przemysłowe (ang. factory acceptance testing) – testy akceptacyjne wykonywane w środowisku, w którym produkt jest wytwarzany, wykonywane przez pracowników dostawcy, w celu sprawdzenia czy moduł lub system spełnia wymagania, zwykle włączając w to zarówno sprzęt, jak i oprogramowanie; patrz również: testowanie alfa
zgodność (ang. compliance) – zdolność oprogramowania standardom, konwencjom rozporządzeniom [3]
lub
regulacjom
prawnym
do podlegania i
podobnym
testowanie zgodności (ang. compliance testing) – proces testowania określający zgodność modułu lub systemu (np. z umową lub z obowiązującymi przepisami prawa)
4.4. Typy testów Poziomy testów wyznaczają podział testów ze względu na ich umiejscowienie w procesie wytwórczym. Testy można jednak sklasyfikować również ze względu na ich cel. Taki podział wyznacza typy testów. Cztery główne typy to: testy funkcjonalne; testy niefunkcjonalne; testy strukturalne; testy związane ze zmianą. typ testów (ang. test type) – grupa czynności testowych nakierowanych na testowanie modułu, komponentu lub systemu, skoncentrowanych na szczegółowych, konkretnych celach, takich jak testy funkcjonalności, testy użyteczności, testy regresyjne itp.; typ testów może być użyty na jednym lub na wielu poziomach testów [28]
4.4.1. Testy funkcjonalne Testy funkcjonalne sprawdzają funkcje systemu, przy czym słowo „funkcje” rozumiemy tu jako funkcjonalności, czyli czynności, do których wykonywania program jest przeznaczony (w odróżnieniu od funkcji jako elementów kodu źródłowego w paradygmacie programowania strukturalnego). Testy te sprawdzają więc, „co” program robi, weryfikują spełnienie wymagań funkcjonalnych. Cel testowania funkcjonalnego jest zwykle jasno określony, gdyż wynika wprost lub pośrednio ze specyfikacji wymagań, przypadków użycia czy specyfikacji funkcjonalnej. Ten typ testowania jest domeną analityka testów.
testowanie funkcjonalne (ang. functional testing) – testowanie oparte na analizie specyfikacji funkcjonalnej modułu lub systemu wymaganie
funkcjonalne
(ang.
functional
requirement)
–
wymaganie
specyfikujące funkcję, którą moduł lub system musi realizować [7] Przy tworzeniu testów funkcjonalnych ma zastosowanie wiele technik czarnoskrzynkowych, gdyż są to techniki oparte na specyfikacji. Techniki czarnoskrzynkowe zostały opisane w rozdziale 8. Testowanie funkcjonalne obejmuje swoim zakresem także niektóre spośród atrybutów jakościowych, takie jak dokładność, odpowiedniość, współdziałanie, bezpieczeństwo funkcjonalne czy użyteczność. Omówimy je dokładnie w rozdziale 15.
4.4.2. Testy niefunkcjonalne Testowanie niefunkcjonalne sprawdza, „jak” system działa. Nie skupiamy się tu na aspektach funkcjonalnych, ale na „parametrach technicznych” oprogramowania opisywanych wymaganiami niefunkcjonalnymi. Typowe atrybuty techniczne podlegające testowaniu niefunkcjonalnemu to: niezawodność, efektywność, bezpieczeństwo techniczne, pielęgnowalność czy przenaszalność. Metody testowania – choć głównie czarnoskrzynkowe – są zupełnie odmienne niż w przypadku testowania funkcjonalnego. Często wymagają specjalnie skonfigurowanego środowiska lub dedykowanego oprogramowania pozwalającego na symulację różnego rodzaju obciążeń systemu. Metody te są opisane dokładniej w rozdziale 16. Testowanie niefunkcjonalne jest domeną technicznego analityka testów. testowanie niefunkcjonalne (ang. non-functional testing) – testowanie atrybutów modułu lub systemu, które nie odnoszą się do jego funkcjonalności, np. niezawodność, efektywność, pielęgnowalność czy przenaszalność testowanie czarnoskrzynkowe (ang. black box testing) – testowanie funkcjonalne lub niefunkcjonalne, bez odniesienia do wewnętrznej struktury modułu lub systemu
wymaganie niefunkcjonalne (ang. non-functional requirement) – wymaganie, które nie dotyczy funkcjonalności, ale cech oprogramowania takich jak niezawodność, efektywność, użyteczność, pielęgnowalność czy przenaszalność Z
testowaniem
niefunkcjonalnym
wiąże
się
pewna
trudność.
Zwykle
dokumenty wymagań skupiają się na wymaganiach funkcjonalnych, a te niefunkcjonalne specyfikowane są bardzo rzadko. Często zakłada się domyślne wymagania niefunkcjonalne (np. odpowiedni poziom bezpieczeństwa lub szybkość działania programu). Tester musi zwracać na to uwagę. Dobre testowanie nigdy nie ogranicza się do testowania funkcjonalności – jeśli wymagania niefunkcjonalne nie są podane wprost w specyfikacji, to należy je uzyskać od klienta lub po prostu założyć domyślnie. Katalog atrybutów jakościowych dla testów niefunkcjonalnych można znaleźć np. w normie ISO 9126 [3] lub w zastępującej ją normie ISO/IEC 25010 [2]. Używając tego typu standardów, można projektować testy tak, aby dało się je odnieść do poszczególnych elementów modelu jakości oprogramowania, dzięki czemu testy niefunkcjonalne mogą pokrywać wymagania jakościowe. technika projektowania testów niefunkcjonalnych (ang. non-functional test design technique) – procedura otrzymywania i/lub wyboru przypadków testowych dla testów niefunkcjonalnych oparta na analizie specyfikacji modułu lub systemu bez odniesienia do jego wewnętrznej struktury
4.4.3. Testy strukturalne Testowanie strukturalne, zwane też testowaniem
architektury, polega
na
upewnieniu się, że wszystkie strukturalne elementy oprogramowania zostały przetestowane (to znaczy, zostały pokryte przez testy). Aby zweryfikować stopień pokrycia, musimy mieć wgląd w wewnętrzną strukturę programu. Dlatego testy strukturalne stosują zawsze techniki białoskrzynkowe. testowanie strukturalne, testowanie białoskrzynkowe, testowanie szklanoskrzynkowe, testowanie na podstawie kodu (ang. structural testing, white box testing, glass box testing, code-based testing) – testowanie oparte na analizie wewnętrznej struktury modułu lub systemu
Strukturalnym elementem oprogramowania mogą być: linie kodu, rozgałęzienia w instrukcjach warunkowych, warunki logiczne w predykatach, wywoływane funkcje itp. Techniki białoskrzynkowe są dokładnie omówione w rozdziale 9. Badanie stopnia pokrycia elementów strukturalnych wymaga zwykle użycia narzędzi, które wykonują to zadanie automatycznie. Testowanie strukturalne może być również przeprowadzone na podstawie analizy architektury systemu (np. hierarchii wywołań metod, funkcji lub innych obiektów). Dobrą praktyką jest rozpoczęcie testowania przy użyciu technik opartych na specyfikacji, a następnie użycie technik opartych na strukturze, aby przetestować elementy strukturalne testowanego programu, nie pokryte podczas testów czarnoskrzynkowych.
4.4.4. Testy związane ze zmianami Testy związane ze zmianami dotyczą powtórnego wykonania testu lub zestawu testów w związku ze zmianą w produkcie lub środowisku. Przykładowe zmiany to: poprawiony kod w związku z wykryciem i naprawieniem defektu; wydanie nowej wersji oprogramowania; opublikowanie „łatki” na oprogramowanie; zmiana wersji systemu operacyjnego, bazy danych lub oprogramowania współpracującego z tworzonym produktem.
innego
W zależności od typu zmiany mówimy o testowaniu potwierdzającym (tzw. retestowaniu) lub o testowaniu regresywnym. Jeśli jakiś test wykrył usterkę i została ona naprawiona, to należy ponownie wykonać ten sam test, aby upewnić się, że naprawa dokonana została prawidłowo. Nierzadko okazuje się, że naprawa nie tylko nie usunęła defektu, lecz także w jej wyniku pojawiło się jeszcze więcej błędów. W przypadku wprowadzania planowanych zmian w oprogramowaniu, niezwiązanych z żadnymi usterkami czy błędami, należy nową wersję przetestować przy użyciu tych samym przypadków testowych, za pomocą których testowaliśmy wersję wcześniejszą. Takie testowanie nazywamy testowaniem regresywnym, a same testy – testami regresji. Testowanie
regresywne ma na celu sprawdzenie, czy wprowadzone do oprogramowania zmiany nie spowodowały powstania jakichś usterek. Zasadnicza różnica między retestami a testami regresji jest taka, że wykonanie retestów związane jest z konkretną, znaną nam usterką, a wykonanie testów regresji nie dotyczy istniejących błędów, lecz jest ukierunkowane na wykrywanie nowych, nieznanych nam dotąd usterek. Testy regresji są pierwszymi i naturalnymi kandydatami do zautomatyzowania. Suita testów regresji często osiąga bardzo duże rozmiary, a w przypadku tzw. codziennych wydań (daily buids) należy je wykonywać bardzo często. Ręczne przeprowadzanie tego procesu często jest bardzo trudne lub wręcz niemożliwe. Organizacje zwykle przeprowadzają testy regresji w nocy, poza godzinami pracy, na sprzęcie deweloperów lub na specjalnie dedykowanych do tego celu serwerach testowych. testowanie potwierdzające, retestowanie (ang. confirmation testing, re-testing) – testowanie polegające na uruchomieniu przypadków testowych, które podczas ostatniego uruchomienia wykryły błędy, w celu sprawdzenia poprawności naprawy testowanie regresywne (ang. regression testing) – ponowne przetestowanie uprzednio testowanego programu po dokonaniu w nim modyfikacji, w celu upewnienia się, że w wyniku zmian nie powstały nowe defekty lub nie ujawniły się defekty w niezmienionej części
oprogramowania; takie testy
są
przeprowadzane po
zmianach
oprogramowania lub jego środowiska W przypadku stosowania techniki ciągłej integracji czy tzw. codziennego budowania wersji (np. w metodykach zwinnych) spotkać można również stosowanie tzw. testów dymnych. Test dymny to niewielki podzbiór testów sprawdzających główne funkcjonalności systemu. Wykonuje się go przed „właściwym” testowaniem kolejnej wersji systemu, aby sprawdzić, czy system ten jest stabilny i czy nie wystąpią jakieś poważne awarie uniemożliwiające bądź opóźniające czynności testowe.
codzienne budowanie wersji (ang. daily build) – aktywność programistyczna, której celem jest kompletna kompilacja i integracja systemu każdej doby (zwykle nocą) tak, aby zintegrowany system wraz z ostatnimi zmianami był dostępny w dowolnym czasie test dymny, test kondycji, test potwierdzający3 (ang. smoke test, sanity test, confidence test) – podzbiór wszystkich zdefiniowanych/zaplanowanych przypadków testowych, które pokrywają główne funkcjonalności modułu lub systemu, mający na celu potwierdzenie, że kluczowe funkcjonalności programu działają, bez zagłębiania się w szczegóły; codzienne wydania (ang. daily builds) i testy dymne stanowią dobre praktyki wytwarzania oprogramowania; patrz także: test wstępny test wstępny, pretest (ang. intake test, pretest) – szczególny rodzaj testu dymnego mający na celu podjęcie decyzji, czy moduł lub system jest gotowy do dalszego szczegółowego testowania; najczęściej przeprowadzany na początku fazy wykonywania testów
4.5. Poziomy a typy testów Poziomy i typy testów są dwoma zupełnie niezależnymi od siebie podziałami testów. Oznacza to, że w praktyce każdy typ testów może być wykonywany na każdym poziomie testowania. Na przykład: testy modułowe oraz systemowe mogą dotyczyć zarówno funkcjonalnych, jak i niefunkcjonalnych cech systemu; testy integracji mogą być wykonywane na więcej niż jednym poziomie i dotyczyć zarówno modułów, jak i systemów; testowanie funkcjonalne może być przeprowadzane zarówno podczas testów modułowych, jak i akceptacyjnych, itd. Oczywiście, w praktyce pewne połączenia poziomów z typami występują częściej niż inne, np. testy niefunkcjonalne zwykle wykonuje się podczas testów systemowych, ale należy pamiętać, że tego typu połączenia nie są regułą i każdy kierownik testów powinien swobodnie operować poziomami i typami, zgodnie z planem testów, tak, aby proces testowy przebiegał jak najefektywniej.
1 Więcej informacji o wyrażeniach regularnych Czytelnik znajdzie w Dodatku C. 2 Część autorów, np. [50] wyróżnia jeszcze więcej rodzajów komponentów zastępczych: stub, spy, mock, fake, dummy. 3 Nazwa „test potwierdzający” występująca w słowniku ISTQB [6] nie jest zbyt szczęśliwa, ponieważ kojarzyć się może z testowaniem potwierdzającym, które oznacza coś zupełnie odmiennego. Lepiej używać pojęcia „test dymny”.
Część II Techniki projektowania testów
Be as simple as possible, but not simpler Albert Einstein
Część
ta
jest
poświęcona
technikom
projektowania
testów.
Zdolność
do
projektowania i tworzenia efektywnych przypadków testowych jest kluczową umiejętnością dobrego testera. W rozdziałach 8 i 9 poznamy bardzo wiele modeli działania oprogramowania, będących podstawą różnych technik tworzenia testów. Zanim jednak je omówimy, w rozdziale 5 zdefiniujemy dwa podstawowe modele: przepływu sterowania oraz przepływu danych. Opiszemy je wcześniej, ponieważ będą nam potrzebne również w rozdziale 6, dotyczącym technik testowania statycznego. Ponadto, większość technik w rozdziale 9 jest bezpośrednio opartych na kodzie, który modeluje się właśnie grafem przepływu sterowania lub przepływu danych. W rozdziale 7 opiszemy przykłady technik analizy dynamicznej, której podstawową cechą jest analiza programu w trakcie jego działania. Rozdziały 8 i 9 zawierają obszerny i szczegółowy przegląd technik testowania opartych na specyfikacji oraz na strukturze, czyli tzw. testowanie biało- i czarnoskrzynkowe. Każda technika jest zilustrowana przykładem opisującym proces tworzenia testów zgodny z normą ISO/IEC/IEEE 29119. W szczególności dla każdej z nich omówimy cztery pierwsze kroki procesu. W rozdziale 10 opiszemy dwie ostatnie techniki: bazującą na defektach oraz najmniej
sformalizowaną,
ale
zaskakująco
skuteczną
technikę
opartą
na
doświadczeniu. Rozdział
11
zawiera
rozważania
projektowania testów. W rozdziale
dotyczące 12
wyboru
omówimy
odpowiednich
natomiast
techniki
technik oceny
priorytetyzacji przypadków testowych. technika projektowania testów, technika testowa, technika specyfikacji testowej, technika projektowania przypadków testowych (ang. test design technique, test technique, test specification technique, test case design technique) – procedura wywodzenia i/lub wybierania przypadków testowych technika wykonywania testu (ang. test execution technique) – metoda użyta do wykonania konkretnego testu, zarówno ręcznie, jak i automatycznie
5. Testowanie oparte na modelu
Oprogramowanie ma zazwyczaj dosyć skomplikowaną strukturę i przetestowanie wszystkich możliwych aspektów jego działania nie jest możliwe. Dlatego do projektowania testów wykorzystuje się modele. Większość technik projektowania testów omówionych w rozdziałach 8 i 9 to techniki oparte na modelu. Model może opisywać sam program, wymagania lub logikę biznesową realizowaną przez aplikację. Nawet w mniej sformalizowanych podejściach, takich jak techniki oparte na doświadczeniu opisane w rozdziale 10, da się odnaleźć elementy podejścia bazującego na modelu. testowanie oparte na modelu (ang. model-based testing) – testowanie oparte na modelu działania modułu lub systemu podlegającego testom, np. na modelu wzrostu niezawodności, profilu operacyjnym lub modelach zachowania takich jak tablica decyzyjna czy diagram stanów
5.1. Cechy dobrego modelu Model obiektu (np. programu) to formalny opis tego obiektu na pewnym ustalonym poziomie abstrakcji. Przykłady takich modeli poznaliśmy w podrozdziale 4.1, w odniesieniu do cyklu życia oprogramowania. Abstrakcja pozwala nam opuścić w opisie nieistotne w danej chwili szczegóły obiektu i skupić się tylko na tych cechach, które nas z różnych względów interesują. Weźmy przykład programu komputerowego. Najbardziej dokładnym modelem tego programu będzie on sam – skompilowany (lub interpretowany) zbiór linii kodu. Załóżmy, że program ten składa się z wielu modułów, które komunikują się ze sobą, np. przez wywoływanie jednego modułu przez inny. Może się zdarzyć, że
w działaniu programu nie będzie nas interesował przepływ sterowania między poszczególnymi liniami kodu, ale to, w jaki sposób moduły wzajemnie się wywołują. Abstrahujemy od programu jako zbioru linii kodu i wychodzimy na wyższy, bardziej ogólny poziom: patrzymy na program jak na zbiór modułów. Wewnętrzna struktura modułu nas nie interesuje. Model tak postrzeganego programu może składać się np. z modułów i opisu relacji między nimi (przykładem może być diagram klas w UML). Jeśli interesuje nas sekwencja wywołań funkcji w zbiorze klas, to wtedy modelem działania programu może być diagram sekwencji; jeśli skupiamy się na definiowaniu i używaniu danych w programie, to modelem może być diagram przepływu danych, i tak dalej. Dobry model systemu musi charakteryzować się kilkoma cechami: formalizm – model musi być opisany w ścisły, formalny sposób, precyzyjnym językiem; niesprzeczność – z modelu nie mogą wynikać dwa sprzeczne ze sobą zachowania systemu; abstrakcja – model powinien skupiać się tylko na tych aspektach i charakterystykach systemu, które nas w danej chwili interesują; jednoznaczność – dwie osoby używające modelu w taki sam sposób powinny otrzymać dokładnie takie same wyniki (chyba że model jest niedeterministyczny). Po co w ogóle tworzy się modele? Czasami wynika to po prostu z powodów finansowych lub związanych z bezpieczeństwem. Jeśli NASA chce wystrzelić rakietę na orbitę okołoziemską, to nie będzie tworzyła setek prototypów rakiet, wystrzeliwała ich z różnymi parametrami lotu i patrzyła, co się stanie (wyleci na orbitę? poleci w kosmos? a może spadnie na ziemię, powodując przy tym śmierć i zniszczenie?). Raczej użyty zostanie matematyczny model lotu rakiety umożliwiający szybkie, wielokrotne, tanie i bezpieczne uruchamianie (w komputerze), pozwalające na dokładne dostrojenie wszystkich niezbędnych parametrów. W testowaniu oprogramowania powód stosowania modeli jest nieco inny. Każda technika projektowania testów jest nakierowana na odkrywanie w oprogramowaniu innego rodzaju błędów. Wybór techniki oparty jest na czymś, co Boris Beizer nazywa „hipotezą błędu” ([62], [14]). Hipoteza błędu wyraża nasze przekonanie
o
rodzajach
błędów,
jakie
mogą
istnieć
w
testowanym
oprogramowaniu. Dysponując taką hipotezą, możemy wybrać odpowiednią technikę projektowania testów. Jeśli hipoteza jest słuszna, to nasze podejście będzie efektywne. Technika projektowania jest oparta na modelu błędu, czyli na ścisłym, formalnym opisie niezgodności rzeczywistego działania programu z oczekiwanym. Wykorzystując odpowiedni model działania programu, uwzględniający określony model błędu, możemy efektywnie generować przypadki testowe pozwalające wykrywać błędy danego typu. Przypadki są tak generowane, aby pokryć zadane wymagania testowe. Wymagania testowe mogą być opisane na poziomie modelu. Jeśli na przykład wykorzystujemy technikę projektowania opartą na kryterium pokrycia gałęzi, to naszym modelem jest graf przepływu sterowania, a wymaganiem testowym będzie zbiór wszystkich krawędzi tego grafu. Kwestie związane z pokrywaniem wymagań testowych omówimy dokładnie w dalszych rozdziałach. Ważną korzyścią ze stosowania modeli jest to, że generacja przypadków testowych na podstawie modelu jest zwykle szybka i automatyczna. Nieco bardziej problematyczne i czasochłonne może być dostarczenie konkretnych danych testowych dla wygenerowanych przypadków testowych. Na przykład znalezienie wartości wejściowych, które pozwolą na uzyskanie określonej ścieżki przepływu sterowania może być bardzo trudne. Istnieją wyspecjalizowane narzędzia, które są w stanie, przez symboliczne wykonywanie kodu, dostarczyć odpowiednie przypadki testowe (np. [63]). Inną zaletą stosowania podejścia opartego na modelu jest możliwość wczesnego wykrywania błędów oraz rozpoczęcia czynności testowych przed fazą kodowania. Zanim nastąpi implementacja kodu, jest tworzony projekt programu, który możemy traktować jak model (np. diagramy UML). Model może zostać poddany procesowi przeglądu (patrz podrozdział 6.1), dzięki czemu jest możliwe wykrycie i naprawa usterek. Takie podejście chroni nas przed popełnieniem tych błędów na etapie implementacji. Część modeli ma właściwość wyroczni. Oznacza to, że potrafią na podstawie określonego wejścia opisać pożądane zachowanie czy odpowiedź systemu. Ta właściwość pozwala na automatyzację nie tylko projektowania i wykonania testów, lecz także na automatyzację weryfikacji tego, czy test jest zdany, czy nie. Podsumowując, zalety testowania opartego na modelu są następujące [64]: formalizm modelu pozwala na systematyczność w tworzeniu przypadków testowych, przez co zmniejsza się ryzyko pominięcia
w testowaniu niektórych aspektów systemu; można ustalać odpowiedni poziom precyzji testów, co pozwala na stosowanie różnych kryteriów pokrycia; są niezależne od psychologii, tzn. osobowość użytkownika modelu nie wpływa na jego działanie podczas testowania; testy weryfikują poprawność modeli; testy są generowane automatyczne; często oczekiwane odpowiedzi można wygenerować automatycznie z modelu. Testowanie oparte na modelu ma również pewne wady: testy nie będą bardziej dokładne niż sam model; testy dotyczą tylko jednego aspektu działania systemu, opisanego przez model; wiele modeli jest bezużytecznych, skomplikowanych, trudnych w użyciu lub wymagających dużej pracochłonności; niewłaściwe podejście do takiego testowania daje fałszywe poczucie bezpieczeństwa; tak stworzone testy nie uwzględniają doświadczenia i intuicji ludzi.
5.2. Taksonomia modeli Modele można charakteryzować pod wieloma różnymi kątami. Na rysunku 5.1 przedstawiono taksonomię zaproponowaną w [65]. Taksonomia ta wyróżnia siedem wymiarów modeli, zgrupowanych w trzy kategorie: opis modelu, generacja testów oraz wykonanie testów. Przedmiot modelu – modelować można zarówno testowany system (ang. System Under Test, SUT), jak i środowisko. Zwykle modeluje się jedno i drugie. W przypadku SUT model działa jak wyrocznia i jest w stanie dostarczyć, dla zadanego wejścia, oczekiwane wyjście (zachowanie) systemu. Ponadto, jego struktura może zostać wykorzystana do generacji testów. Model środowiska narzuca z kolei ograniczenia na możliwe wejścia.
Redundancja – model, poza generacją przypadków testowych może również generować kod źródłowy tych przypadków. Charakterystyki – model może dopuszczać niedeterministyczne zachowanie, uwzględniać następstwo zdarzeń (modele uwarunkowane czasowo) oraz pracować na danych dyskretnych (np. poszczególne chwile czasu jako kroki, skończona liczba stanów), ciągłych (czas bądź zbiór możliwych wartości jest ciągły), jak i hybrydowych (ciągłodyskretnych).
Rysunek 5.1. Taksonomia modeli
Paradygmat – opisuje paradygmat i notację zapisu modelu. Można wyróżnić pięć głównych paradygmatów: pre–post (wykorzystywane np. w językach modelowania takich, jak notacja Z [66], notacja B [67] czy język OCL [68]), oparty na przejściach (wykorzystujący np. sieci Petriego, diagramy stanów UML czy maszyny stanowe omówione w podrozdz. 8.5), oparty na historii, funkcjonalny oraz operacyjny (symulujący oczekiwane użycie systemu na podstawie rozkładu prawdopodobieństwa). Kryteria wyboru przypadków testowych – opisują sposób, w jaki będą wybierane testy. Wszystkie te kryteria omówione zostaną w rozdziałach 8, 9 i 10. Technologia – opisuje sposób generowania przypadków testowych. Wykonywanie testów – w podejściu online generacja testów następuje na podstawie historii zachowania się systemu. Podejście to wykorzystuje się zwłaszcza w modelach niedeterministycznych, gdzie model musi podążać za nieznaną z góry ścieżką wykonania programu. W podejściu offline wszystkie testy są generowane przed ich uruchomieniem.
5.3. Przykład wykorzystania modelu Skoro omówiliśmy teoretyczne podstawy modelowania, czas na praktykę. Przedstawimy konkretny przykład rzeczywistego systemu i na jego przykładzie pokażemy, jak opisane w poprzednim podrozdziale korzyści ze stosowania modelu uwidaczniają się w kontekście testowania. Przykład dotyczy pewnej transformacji słów kodu Graya. Kod Graya długości n to uporządkowana sekwencja wszystkich 2n słów binarnych o długości n, w której każde dwa kolejne słowa różnią się tylko na jednym bicie. Rozszerzenie kodu Graya o jeden bit odbywa się według dwóch prostych zasad: 1) mając kod o długości n, dopisz jego odbicie lustrzane; 2) do oryginalnego kodu dopisz na początku zero, a do elementów w odbiciu lustrzanym jedynkę. Na rysunku 5.2 pokazano przykład tworzenia kodu Graya o długości 3 z kodu o długości 2. Z prawej strony rysunku widzimy numerację (idącą od zera) kolejnych elementów ośmioelementowego kodu Graya o długości 3. Na przykład
element 000 jest pierwszy (pozycja nr 0), element 001 drugi (pozycja nr 1), element 011 trzeci (pozycja nr 2) itd. Numeracja jest podana w systemie dwójkowym.
Rysunek 5.2. Generacja kodu Graya o długości 3 z kodu o długości 2 System Gray2Order to układ kombinacyjny1 zamieniający element kodu Graya w binarną sekwencję, opisującą pozycję tego elementu w kodzie. Testowany przez nas system będzie działał na kodzie Graya o długości 8, złożonym z 28 = 256 elementów. System ma postać układu scalonego, z 8 wejściami i 8 wyjściami. Nie znamy jego wewnętrznej struktury, ale dysponujemy modelem w postaci schematu elektronicznego. Jest on przedstawiony na rys. 5.3. Wejścia są oznaczone symbolami od I1 do I8, przy czym I8 opisuje bit najbardziej znaczący, I1 – najmniej znaczący. Wyjścia są oznaczone przez O1 do O8, z taką samą interpretacją bitów znaczących. Model składa się z siedmiu bramek logicznych XOR (alternatywa rozłączna, ang. XOR, od exclusive OR), połączonych w odpowiedni sposób. Każda bramka XOR ma dwa wejścia i jedno wyjście. Jeśli wejścia są różne (0 i 1 lub 1 i 0), to na wyjściu pojawia się sygnał 1. Jeśli takie same (dwa zera lub dwie jedynki), to na wyjściu bramki pojawi się 0. Wyjście z bramki jest zarazem jednym z wejść do bramki następnej.
Rysunek 5.3. Model 8-bitowego konwertera Gray2Order Nasz model ma wszystkie cechy dobrego modelu opisane w podrozdziale 5.1. Jest formalny, bo używa precyzyjnej notacji opisu układów elektronicznych. Jest niesprzeczny, bo bramki działają w sposób deterministyczny i nie da się uzyskać dwóch różnych wyjść na podstawie tego samego wejścia. Jest jednoznaczny, bo reguły działania bramek logicznych są ściśle określone i każdy użytkownik modelu dostanie, dla zadanego wejścia, taki sam wynik jak pozostali użytkownicy. Jest wreszcie abstrakcyjny – fizyczna budowa układu (ścieżki, tranzystory itp.) została zamodelowana za pomocą abstrakcyjnych elementów, takich jak bramki logiczne. Nie interesuje nas wewnętrzna budowa bramek logicznych, ale wyłącznie ich działanie oraz sposób połączenia elementów. Zauważmy ponadto, że model może podać oczekiwaną odpowiedź dla dowolnego zadanego wejścia. Można również wykorzystać ten model do automatycznego projektowania testów dla różnych kryteriów pokrycia, np.: dla każdej błędnie liczącej bramki powinien istnieć przynajmniej jeden test, w którym wystąpi to złe obliczenie; albo: dla każdej bramki powinny istnieć testy, które przetestują wszystkie cztery możliwe wejścia do tej bramki, itd. Jak przetestować system Gray2Order? Załóżmy, że dysponujemy środowiskiem testowym, w którym możemy zainstalować nasz układ, pobudzać go odpowiednimi sygnałami na wejściu i analizować otrzymane wyjście. Każde podanie 8 sygnałów wejściowych oczywiście zajmuje czas i chcielibyśmy przetestować system za pomocą możliwie najmniejszej liczby przypadków
testowych, ale jednocześnie możliwie jak najpełniej. Pojęcie „jak najpełniej” jest trudne do dokładnego zdefiniowania. Oczywiście, w trywialnym przypadku możemy przetestować układ, podając mu wszystkie możliwe kombinacje sygnałów wejściowych. Dla systemu 8-bitowego takich kombinacji jest 256, co jeszcze nie jest dużą liczbą. Ale w przypadku układu 64-bitowego musielibyśmy wykonać 264 = 18,446,744,073,709,551,616 testów, co jest już niewykonalne. Jak zatem sensownie przetestować nasz układ? Zastanówmy się najpierw, jak mogłaby wyglądać hipoteza błędu. Innymi słowy – co może spowodować nieprawidłowy wynik działania układu? Nasz model wykorzystuje bramki logiczne, które fizycznie są realizowane przez odpowiednio połączone tranzystory. Na skutek różnych defektów (uszkodzenia mechaniczne, błąd przy drukowaniu płytki itp.) któraś z bramek logicznych może źle działać, tzn. dawać błędny wynik działania XOR. Pomysł polega więc na tym, aby skonstruować takie przypadki testowe, które wymuszą – dla każdej bramki – przyjęcie przynajmniej raz każdej możliwej kombinacji wejść (00, 01, 10, 11). Jeśli bowiem jedna z bramek działa nieprawidłowo dla jakiejś kombinacji wejść, to przynajmniej jeden z testów powinien to wykryć. Wymaganiem testowym jest więc zbiór wszystkich możliwych kombinacji wejść dla każdej bramki logicznej. Test pokryje wymaganie „wejścia 01 dla bramki U2”, jeśli podane na wejściu sygnały spowodują podanie bramce U2 wejść 0 i 1. Kryterium pokrycia zostanie spełnione, jeśli każda bramka przynajmniej raz otrzyma na wejściu każdą możliwą kombinację sygnałów 0 i 1. Proponowane przypadki testowe (PT) pokazano w tabeli 5.1. Pierwsza kolumna zawiera numer przypadku testowego. W kolejnych 8 kolumnach umieszczone są sygnały podawane na 8 wejść układu. Kolumny U1 do U7 zawierają opis kombinacji wejść do wszystkich siedmiu bramek logicznych pod wpływem podania sygnałów wejściowych I8 I7 … I1. Ostatnia kolumna zawiera oczekiwane wyjście układu, dzięki czemu możemy porównać wynik oczekiwany z rzeczywistym. Tabela 5.1. Przypadki testowe dla konwertera Gray2Order
test
W ejścia do bramek logicznych
Oczekiwane wyjście I8 I7 I6 I5 I4 I3 I2 I1 U1 U2 U3 U4 U5 U6 U7 (O8...O1) Sygnały wejściowe
PT1 0 0 0 0 0 0 0 0 00 00 00 00 00 00 00 00000000 PT2 1 1 1 1 1 1 1 1 11 01 11 01 11 01 11 10101010 PT3 0 1 1 1 1 1 1 1 01 11 01 11 01 11 01 01010101 PT4 1 0 0 0 0 0 0 0 10 10 10 10 10 10 10 11111111 Zauważmy, że zarówno dla naszego 8-bitowego układu, jak i w ogólnym przypadku, dla dowolnej liczby bitów wejściowych, wystarczą tylko 4 testy do spełnienia naszego kryterium pokrycia. W każdej kolumnie od U1 do U7 występują wszystkie cztery kombinacje wejść do danej bramki. Na przykład realizacja wejść 01 do bramki U5 nastąpi przy wykonaniu testu nr 3. Dzięki odpowiednio sformułowanej hipotezie błędu oraz modelowi układu byliśmy w stanie stworzyć rozsądny zestaw przypadków testowych. Gdyby któraś z bramek była zepsuta, to istniałaby duża szansa, że któryś z testów to wykryje. Nie możemy jednak być tego pewni w stu procentach. Na przykład na skutek błędów w drukowaniu płytki jedna z bramek może dać złe wyjście przy pewnej szczególnej kombinacji dwóch lub więcej wejść. Nasze testy nie wykryją tego, jeśli taka kombinacja wejść nie wystąpi w żadnym teście. Może też wystąpić tzw. zjawisko maskowania błędów: błędny sygnał, powstały w jednym miejscu, może zostać zamieniony ponownie, na skutek innego błędu, na poprawny w innym miejscu. maskowanie usterek, maskowanie defektów, maskowanie błędów (ang. fault masking, defect masking, failure masking) – sytuacja, w której występowanie jednego defektu lub błędu uniemożliwia wykrycie innego [7] Widać więc, że żadna metoda testowania (za wyjątkiem testowania gruntownego które, jak już wspomnieliśmy, zwykle jest niewykonalne) nie da nigdy stuprocentowego przekonania o poprawności systemu. Zastosowanie modelu pozwala jednak zredukować liczbę przypadków do rozsądnej liczby. Ponadto, przypadki te są dosyć silne, tzn. z dużym prawdopodobieństwem wykryją istniejący w układzie błąd. Zauważmy też, że nasz zestaw przypadków testowych spełnia również inne, ciekawe kryteria pokrycia, na przykład pokrycie wszystkich możliwych wartości dla każdego bitu wejściowego czy pokrycie wszystkich możliwych wartości dla każdego bitu wyjściowego. Gdyby na
przykład żaden test nie powodował wystąpienia sygnału 1 na bicie O5, nigdy nie wykrylibyśmy błędu powodowanego tym zdarzeniem.
5.4. Modele działania oprogramowania Pokazaliśmy przykład zastosowania modelu w testowaniu układu elektronicznego. Jak jednak mogą wyglądać modele opisujące działanie oprogramowania? W ciągu ostatnich kilkudziesięciu lat powstało bardzo wiele takich metod formalnego opisu. Niektóre z nich są wymienione w tabeli 5.2. Większość z tych modeli będzie nam służyła w następnych rozdziałach do projektowania efektywnych przypadków testowych. Przedstawimy teraz dwa podstawowe modele działania programu, należące do grupy modeli grafowych: graf przepływu sterowania oraz graf przepływu danych. Omawiamy je wcześniej niż pozostałe modele, ponieważ będziemy je wykorzystywać nie tylko w rozdziałach 8 i 9 dotyczących technik białoi czarnoskrzynkowych, lecz także podczas opisywania analizy statycznej w podrozdziale 6.2. Tabela 5.2. Modele działania oprogramowania
Aspekt systemu
Przykładowe modele
Zastosowanie w testowaniu
przepływ s terowania, złożonoś ć kodu
tes towanie g raf przepływu s terowania białos krzynkowe, analiza s tatyczna
przepływ danych, złożonoś ć komunikacji
g raf przepływu danych, g raf wywołań
tes towanie białos krzynkowe, analiza s tatyczna tes towanie mutacyjne, tes towanie interfejs ów,
s kładnia
model s kładniowy
tes towanie s ekwencji zdarzeń akcje, s tany prog ramu
model mas zyny s tanowej
tes towanie czarnos krzynkowe
ws półbieżnoś ć
s ieci Petrieg o, alg ebra proces ów
tes towanie czarnos krzynkowe
kombinacje warunków
tablica decyzyjna, g rafy przyczynowo-s kutkowe, drzewa klas yfikacji, tes towanie par
tes towanie czarnos krzynkowe
poprawnoś ć
model aks jomatyczny, alg ebra proces ów, log iki temporalne, notacja Z, język B, OCL
dowodzenie poprawnoś ci prog ramu
proces y biznes owe
diag ram przypadków użycia, model przepływu zdarzeń
tes towanie czarnos krzynkowe, tes towanie eks ploracyjne
przepływ sterowania (ang. control flow) – sekwencja zdarzeń, w postaci ścieżki, występująca podczas pracy modułu lub systemu przepływ danych (ang. data flow) – abstrakcyjna reprezentacja sekwencji i możliwych zmian stanu obiektu danych, gdzie dostępne stany obiektu to utworzenie, użycie lub usunięcie [14] punkt startu (ang. entry point) – pierwsze wyrażenie wykonywane wewnątrz modułu
punkt wyjścia (ang. exit point) – ostatnie wyrażenie wykonywane wewnątrz modułu
5.4.1. Graf przepływu sterowania Graf przepływu sterowania (ang. Control Flow Graph, CFG) jest abstrakcyjnym modelem opisującym wszystkie możliwe sekwencje wykonań instrukcji programu. Składa się z wierzchołków połączonych skierowanymi krawędziami. Każdy wierzchołek opisuje jedną instrukcję programu lub sekwencję instrukcji I1, I2, …, In takich, że instrukcje te wykonają się zawsze albo wszystkie po kolei, albo nie wykona się żadna z nich. Takie sekwencje nazywamy blokami podstawowymi. Formalnie, sekwencja I1, I2, …, In jest blokiem podstawowym, kiedy są spełnione dwa warunki: gdy sterowanie przejdzie do początku sekwencji, wtedy zawsze nastąpi wykonanie wszystkich instrukcji I1, I2, I3 …, In dokładnie w takiej kolejności (czyli w obrębie sekwencji nie ma rozgałęzień),
każdy skok spoza tej sekwencji w jej obręb może nastąpić tylko do instrukcji I1, tzn. do początku bloku. instrukcja, instrukcja kodu źródłowego (ang. statement, source statement) – element kodu zapisanego w określonym języku programowania, stanowiący najmniejszą niepodzielną jednostkę wykonania lub traktowany w ten sposób instrukcja wykonywalna (ang. executable statement) – wyrażenie, które w trakcie kompilacji jest tłumaczone na kod binarny i które będzie wykonywane proceduralnie podczas działania programu; może ono wykonywać akcje na danych programu blok podstawowy (ang. basic block) – ciąg składający się z jednej lub wielu następujących po sobie instrukcji bez rozgałęzień, przy czym każdy skok spoza bloku w jego obręb – jeśli istnieje – to musi nastąpić do pierwszej instrukcji bloku
DD-ścieżka (ang. DD-path od decision-to-decision) – ścieżka w CFG będąca w jednej z poniższych postaci: – pojedynczy wierzchołek o
stopniu2
wchodzącym
=
0
(wierzchołek
początkowy); – pojedynczy wierzchołek o stopniu wychodzącym = 0 (wierzchołek końcowy); – pojedynczy wierzchołek o stopniu wchodzącym ≥ 2 lub stopniu wychodzącym ≥ 2 (wierzchołek decyzyjny lub tzw. punkt łączenia); – maksymalny łańcuch, tzn. ciąg wierzchołków (v1, v2, …, vn) taki, że stopnie wchodzące oraz wychodzące wszystkich (v1, v2, …, vn) są równe 1 i nie da się tej ścieżki rozszerzyć tak, aby właściwość ta nadal zachodziła W grafach przepływu sterowania pojedyncze wierzchołki często reprezentują bloki podstawowe. Dzięki tej reprezentacji rozmiar grafu (w sensie liczby jego wierzchołków) jest mniejszy niż gdybyśmy chcieli reprezentować każdą pojedynczą instrukcję osobnym węzłem. Grupowanie instrukcji w jeden blok nie ma żadnego wpływu na analizę przepływu sterowania programu, ze względu na właściwość bloku: jego instrukcje zawsze wykonają się w ten sam sposób. Bloki podstawowe reprezentują liniowe sekwencje instrukcji. Czasami zamiast bloków podstawowych wykorzystuje się tzw. DD-ścieżki3 . Na rysunku 5.4 przedstawiono podstawowe elementy modelu przepływu sterowania.
Rysunek 5.4. Podstawowe elementy grafu przepływu sterowania Wśród wierzchołków wyróżniamy początkowy, czyli ten, od którego rozpoczyna się przepływ sterowania oraz końcowy, w którym następuje koniec działania programu. Krawędź od wierzchołka X do wierzchołka Y reprezentuje
przepływ sterowania z bloku X do bloku Y. Instrukcje, w których jest podejmowana decyzja co do wyboru dalszego sterowania są reprezentowane przez wierzchołki z więcej niż jedną krawędzią wychodzącą. W przypadku instrukcji if-then lub if-then-else będą to dwie krawędzie wychodzące. W przypadku instrukcji switch-case może ich być więcej. Wierzchołek może też mieć więcej niż jedną krawędź wchodzącą, jeśli może do niego nastąpić skok z kilku miejsc w programie. Formalnie, graf przepływu sterowania to czwórka G = (V, E, v0, VT), gdzie: V – jest niepustym, skończonym zbiorem wierzchołków; E ⊂ V × V – jest zbiorem krawędzi skierowanych łączących pary wierzchołków; v0 ∈ V – jest wierzchołkiem początkowym; VT ⊂ V – jest niepustym zbiorem wierzchołków (terminalnych). Zauważmy, że zgodnie z powyższą
definicją
wierzchołek
końcowych
początkowy
w szczególności może być jednocześnie wierzchołkiem końcowym. Graf przepływu sterowania jest budowany na podstawie kodu źródłowego. Istnieją narzędzia do automatycznej budowy takich grafów. Proces ten jest bardzo prosty i polega na podziale kodu na bloki podstawowe, a następnie opisaniu relacji następstw tych bloków. Na rysunku 5.5 przedstawiono metodę transformacji dla podstawowych konstrukcji algorytmicznych. diagram przepływu sterowania (ang. control flow graph) – abstrakcyjna prezentacja możliwych sekwencji zdarzeń (w postaci ścieżek) występujących podczas uruchomienia modułu lub systemu Sekwencja
następujących
po
sobie
instrukcji
(blok
podstawowy)
jest
reprezentowana pojedynczym wierzchołkiem. Instrukcja warunkowa if-then składa się z wierzchołka zawierającego warunek (B1) oraz wierzchołka zawierającego ciało instrukcji warunkowej (B2). Jeżeli warunek jest spełniony, to sterowanie przechodzi lewą krawędzią z B1 do B2, a następnie, po wykonaniu instrukcji w B2 przechodzi do pierwszej instrukcji następującej po końcu instrukcji warunkowej (B3). Jeżeli warunek nie jest spełniony, to sterowanie przechodzi od razu prawą krawędzią do B1. Na podobnej zasadzie działa transformacja do grafu w przypadku instrukcji if-then-else, przy czym niespełnienie
Rysunek 5.5. Transformacja konstrukcji algorytmicznych na grafy przepływu sterowania warunku oznacza przejście prawą krawędzią do początku bloku następującego po konstrukcji if-then-else (B4). Transformacja dla instrukcji switch-case jest analogiczna, gdyż może być rozumiana jako wielopoziomowa instrukcja if-thenelse.
Instrukcja for składa się z inicjalizacji, warunku, inkrementacji oraz instrukcji wewnątrz pętli. Dlatego wejściowym wierzchołkiem dla konstrukcji for jest B1 zawierający instrukcję inicjalizacji. Na przykład, dla instrukcji for (i:=0; i0) mogą być oznaczone odpowiednio i > 0 oraz i -1 && pat[k+1] != pat[i]) k = pi[k]; if (pat[i] == pat[k+1])
11 12 13 14
k++; pi[i] = k; } return pi; }
Listing 5.1. Kod w C obliczający tablicę prefiksów dla algorytmu Knutha–Morrisa– Pratta Pierwszy krok to identyfikacja bloków podstawowych. Bloki te są pokazane z lewej strony na rysunku 5.6. Instrukcja i++ w pętli for przynależy do bloku B9 i jest wykonywana jako jego ostatnia instrukcja. Drugi krok polega na określeniu następstw między blokami, których graficzną reprezentacją jest graf przepływu sterowania pokazany na rysunku 5.6 z prawej strony.
Rysunek 5.6. Podział kodu na bloki podstawowe i odpowiadający mu graf przepływu sterowania
5.4.2. Ograniczenia w stosowaniu grafu przepływu sterowania Rozwój technik programowania powoduje, że ścisły zapis przepływu sterowania niektórych programów jest trudny lub wręcz niemożliwy. Rozważmy trzy następujące przykłady: obsługa wyjątków – różne typy wyjątków mogą wystąpić w wielu miejscach programu. W programie na rysunku 5.7a wyjątki mogą nastąpić podczas zarówno otwierania pliku, czytania z pliku, jak i wykonywania operacji arytmetycznej. W sytuacji wystąpienia wyjątku możemy je rozróżniać tak, jak to zostało pokazane na rysunku, ale wciąż nie możemy reprezentować poprawnie sterowania, gdyż B1 tak naprawdę nie jest blokiem – pod wpływem rzucenia wyjątku może
nastąpić skok ze środka bloku do B2 lub B3. Częściowym rozwiązaniem jest reprezentowanie każdej linii programu osobnym wierzchołkiem, jednak to znacznie komplikuje postać grafu;
Rysunek 5.7. Problematyczne programy dla modelowania przepływu sterowania rekurencja modelować
– w przypadku stosowania rekurencji trudno jest działanie programu za pomocą grafów przepływu
sterowania. Na rysunku 5.7b przerywanymi strzałkami zaznaczono możliwe skoki w przypadku stosowania rekurencji. Jeśli wywołano rekurencyjnie funkcję f, to następuje skok B3 → B1. Jeśli zakończono rekurencyjne wywołanie funkcji w B2 i to nastąpił powrót do funkcji wywołującej, mamy skok B2 → B3, przy czym skok może nastąpić do „środka” instrukcji return, między jednym a drugim wywołaniem f; dynamiczne wiązanie – w przykładzie z listingu 5.2 w momencie wykonywania metody dodaj na obiekcie s klasy Student nie zawsze możemy przewidzieć, która metoda dodaj zostanie użyta – może to zależeć od dynamicznego typu obiektu b, np. książka może być niedostępna dla studentów spoza danej uczelni. Grafy przepływu sterowania są zbyt słabym narzędziem do modelowania takich sytuacji.
... wypożycz(Book b, Student s) { if (b.dostępna()) { s.dodaj(b) }
} ... Listing 5.2. Fragment kodu z dynamicznym wiązaniem
5.4.3. Graf przepływu danych Graf przepływu danych (ang. data flow graph) to graf przepływu sterowania wzbogacony o informacje dotyczące użycia i definicji zmiennych. Graf ten umożliwia śledzenie przepływu danych od momentu ich inicjalizacji do momentu ich użycia. Model ten pozwala na testowanie (zarówno statyczne, jak i dynamiczne), które skupia się na przepływach wartości danych. analiza przepływu danych (ang. data flow analysis) – analiza statyczna na podstawie definiowania i użycia zmiennych Definicja zmiennej to miejsce, w którym wartość zmiennej jest zapisywana do pamięci (przypisanie, wejście itp.). Użycie zmiennej to miejsce, w którym wartość zmiennej jest odczytywana z pamięci w celu wykorzystania (prawa strona instrukcji przypisania, warunek logiczny itp.).
Rysunek 5.8. Graf przepływu danych dla funkcji compute_prefix Rozważmy przykład opisanej wcześniej funkcji compute_prefix. W bloku B3 następuje tam definicja zmiennej pi (w instrukcji pi[0]=k), a po przejściu B3 → B4 → B5 → B6 w bloku B6 następuje jej użycie (w instrukcji k=pi[k]). Graf przepływu danych dla funkcji compute_prefix jest przedstawiony na rysunku 5.8. Jest to graf przepływu sterowania z rysunku 5.6 z dodanymi informacjami dotyczącymi definicji i użyć zmiennych. W celu uproszczenia pominęliśmy etykiety krawędzi opisujące warunki logiczne. Niektórzy autorzy (np. [27]) przypisują definicje i użycia także do krawędzi (właśnie w przypadku tych warunków), jednak my będziemy stosować konwencję odwołującą się bezpośrednio do bloków. Krawędzie reprezentują bowiem abstrakcyjne pojęcie przepływu sterowania, natomiast zmienne są umieszczane w konkretnych miejscach (blokach) programu.
5.4.4. Ścieżki, ścieżki testowe i ścieżki nieosiągalne
Należy odróżnić od siebie dwa pojęcia związane z grafowymi modelami działania programu: ścieżkę oraz ścieżkę testową. Ścieżką w grafie G = (V, E, v0, VT)
o długości n nazywamy dowolny ciąg wierzchołków v1, v2, …, vn+1 ∈ V takich, że (v1, vn+1) ∈ E dla każdego i = 1, … n. Ścieżka testowa jest natomiast ścieżką opisującą realizację działania programu dla określonych danych wejściowych. Musi zatem zawsze zaczynać się w wierzchołku początkowym, a kończyć w wierzchołku końcowym grafu przepływu sterowania. Rozróżnienie to jest istotne w przypadku grafowych kryteriów pokrycia omówionych w rozdziale 9: o ile warunkami testowymi i elementami pokrycia dla metod grafowych mogą być dowolne ścieżki (np. pojedyncze wierzchołki4 lub krawędzie), o tyle fizycznymi elementami pokrywającymi te elementy będą zawsze ścieżki testowe. ścieżka testowa, ścieżka przepływu sterowania (ang. test path, control flow path) – sekwencja wydarzeń, np. wykonywalnych wyrażeń, w ramach modułu lub systemu począwszy od punktu wejścia do punktu wyjścia Zgodnie z powyższą definicją pojęcie ścieżki testowej można rozszerzyć na sekwencje „wydarzeń”, którymi mogą być np. stany w maszynie stanowej, wywołane funkcje w grafie wywołań, wywołane podmoduły. Nie każda ścieżka występująca w grafie przepływu sterowania musi być osiągalna. Może nie istnieć zestaw danych, który spowodowałby odpowiadające jej rzeczywiste wykonanie programu. Takie ścieżki nazywa się nieosiągalnymi. Rozważmy przykład prostego kodu z rysunku 5.9.
Rysunek 5.9. Kod oraz odpowiadający mu CFG z nieosiągalną ścieżką Zauważmy, że gdy warunek w bloku B1 jest spełniony, nastąpi podstawienie 1 do zmiennej z, co automatycznie spowoduje spełnienie warunku B3. Dlatego ścieżka B1 → B2 → B3 → B5, choć syntaktycznie poprawna, w praktyce nigdy nie będzie mogła wystąpić. Jest więc ścieżką nieosiągalną. Jeśli powyższy przykład wydaje się nieco sztuczny, to Czytelnik może przeanalizować kod z listingu 9.9 ze s. 336. Jest to rzeczywisty nieosiągalnych.
program,
w
którym
występuje
wiele
ścieżek
Wiele grafowych kryteriów pokryć opisanych w rozdziale 9 wymaga pokrycia testami określonych zbiorów ścieżek. Jeśli okaże się, że występują wśród nich ścieżki nieosiągalne, to można przyjąć jedno z następujących podejść: zignorować ścieżki nieosiągalne (tzn. usunąć je ze zbioru wymaganych elementów pokrycia), osłabić kryterium (tzn. przyjąć inne, słabsze kryterium, które nie będzie generować ścieżek nieosiągalnych), pokryć ścieżki „podobne” do ścieżek nieosiągalnych, stosując objazdy (ang. detour) lub ścieżki poboczne (ang. sidetrip) omówione w podrozdziale 9.14. ścieżka nieosiągalna, ścieżka niewykonalna (ang. unreachable path, infeasible path) – ścieżka, dla której nie istnieje zestaw danych wejściowych, przy których przejście tej ścieżki jest możliwe
1 Układ kombinacyjny to elektroniczny układ cyfrowy, w którym stan wyjścia zależy wyłącznie od stanu jego wejścia. 2 W grafie skierowanym (takim jak CFG) stopień wchodzący wierzchołka v to liczba krawędzi wchodzących do v, a stopień wychodzący – liczba krawędzi wychodzących z v. Wierzchołek decyzyjny zawsze ma stopień wychodzący co najmniej 2. 3 Istnieje subtelna różnica między definicją bloku podstawowego a DD-ścieżką zdefiniowaną tak, jak w tym rozdziale (patrz p. 9.10.3). 4 Wierzchołek, w świetle definicji ścieżki, jest ścieżką o długości 0 (bez krawędzi).
6. Techniki testowania statycznego
Techniki testowania statycznego to zestaw narzędzi służących do sprawdzania ręcznie lub za pomocą analizy automatycznej kodu lub innych artefaktów procesu wytwórczego (dokumentacja, wymagania) bez uruchamiania kodu. Do grupy technik statycznych zalicza się przeglądy oraz analizę statyczną kodu. Proces testowania przy użyciu tych metod jest nazywany testowaniem statycznym. W podrozdziale 6.1 omówimy przeglądy, w kolejności zgodnej ze stopniem ich sformalizowania. W podrozdziale 6.2 opiszemy metody analizy statycznej kodu oraz architektury. Analiza statyczna zawiera w sobie m.in. analizę przepływu danych. Tę metodę wykorzystamy również w testach białoskrzynkowych (p. 9.12.6). analiza statyczna (ang. static analysis) – analiza artefaktów oprogramowania, np. wymagań lub kodu źródłowego przeprowadzona bez wykonywania tych artefaktów. Analiza statyczna jest na ogół przeprowadzana za pomocą narzędzi testowanie statyczne (ang. static testing) – testowanie artefaktów powstałych podczas tworzenia oprogramowania, np. wymagań, projektu lub kodu, bez wykonywania tych artefaktów, tzn. podczas przeglądów lub analizy statycznej analizator, analizator statyczny, narzędzie do analizy statycznej (ang. analyzer, static analyzer, static analysis tool) – narzędzie wykonujące analizę statyczną analizator kodu, analizator statyczny kodu (ang. code analyzer, static code analyzer) – narzędzie wykonujące analizę statyczną kodu; sprawdza ono kod
źródłowy pod względem pewnych właściwości, takich jak zgodność ze standardami kodowania, metryki jakości lub anomalie przepływu danych
6.1. Przeglądy Przeglądy są – paradoksalnie – najmniej docenianą lecz jedną z najbardziej efektywnych metod testowania. Capers Jones [70] w swoim raporcie na temat jakości oprogramowania zebrał dane z ponad 13 000 projektów i zbadał, m.in., efektywność usuwania defektów podczas przeglądów. Wyniki te zebrane są w tabeli 6.1. Tabela 6.1. Efektywność usuwania defektów dla wybranych statycznych metod testowania
Rodzaj przeglądu
Efektywność usuwania defektów najniższa mediana najwyższa
Przeg ląd nieformalny wymag ań
20%
30%
50%
Przeg ląd nieformalny projektu wys okieg o poziomu
30%
40%
60%
Ins pekcja s zczeg ółoweg o projektu funkcjonalnoś ci
30%
65%
85%
Ins pekcja s zczeg ółoweg o projektu log iczneg o
35%
65%
75%
Ins pekcja kodu lub analiza s tatyczna
35%
60%
90%
Na przykład, podczas inspekcji kodu w połowie projektów wykryto więcej niż 60% całkowitej liczby znalezionych defektów. W niektórych projektach efektywność ta dochodziła nawet do 90%! W porównaniu z przeglądami, klasyczne metody testowania nie są już tak efektywne. Mediana (w zależności od poziomu testu) waha się od 25% w przypadku testów jednostkowych do 50% w przypadku
testów systemowych. Wartości najwyższe dla testowania przyjmują wartości od 50% do 75%. Wyniki te nie oznaczają oczywiście, że w procesie testowym wystarczy ograniczyć się do przeglądów, aby uzyskać dobrą jakość oprogramowania. Po pierwsze, produktem końcowym jest działające oprogramowanie, a błędy w nim można znaleźć jedynie podczas testów dynamicznych. Po drugie, przeprowadzanie inspekcji nie jest tanie. Wymaga również czasu, przygotowania oraz odpowiednio przeszkolonych ludzi. Zwykle jednak inwestycja w przeprowadzenie przeglądów, zwłaszcza dla kluczowych dokumentów lub fragmentów kodu, zwraca się, co jasno pokazują przedstawione dane. Co więcej, przeglądy można przeprowadzać w bardzo wczesnych fazach projektu, dlatego błędy będą również wykrywane wcześniej. Systematyczne przeprowadzanie przeglądów podczas całego cyklu życia powoduje, że liczba błędów wykrywanych za pomocą przeglądów rozkłada się mniej więcej równomiernie w czasie. Bez stosowania przeglądów większość błędów ujawnia się w końcowych fazach projektu, powodując tym samym zwiększenie kosztów i nakładów pracy potrzebnych do ich usunięcia (patrz rys. 6.1 opracowany na podstawie [70]). W rezultacie, końcowe fazy projektu można nazwać strefą chaosu. Przeglądy, tak jak testowanie, mają dwa zasadnicze cele: znajdowanie defektów oraz nabranie zaufania do tworzonego oprogramowania. Jednak istnieją również cele właściwe tylko przeglądom. Pierwszym z nich jest upewnienie się, że wszyscy członkowie
Rysunek 6.1. Porównanie miejsc powstawania i wykrywania defektu bez stosowania przeglądów (u góry) i z przeglądami (na dole) zespołu projektowego rozumieją dany dokument w ten sam sposób, zarówno jeśli chodzi o samą jego treść, jak i znaczenie dla projektu. Drugim celem jest budowanie porozumienia co do treści zawartej w dokumencie poddawanym inspekcji.
przegląd (ang. review) – ocena produktu lub statusu projektu mająca na celu stwierdzenie rozbieżności od planowanych założeń i rekomendację usprawnień [71] Przeglądy formalne to rodzaj przeglądów wykorzystujących ściśle określone procedury i wymóg dokładnego raportowania. Zostały one wprowadzone w latach 70. przez Fagana [72]. W latach późniejszych wprowadzano różnego rodzaju usprawnienia do procesu inspekcji, np. Genuchten [73] zaproponował zwiększenie efektywności inspekcji przez wprowadzenie tzw. Elektronicznego systemu spotkań (ang. Electronic Meeting System). Przykładami przeglądów formalnych są np. inspekcja lub audyt. przegląd formalny (ang. formal review) – przegląd charakteryzujący się udokumentowanymi procedurami i wymaganiami Czasami można również spotkać się z pojęciem przeglądu koleżeńskiego. Jest to forma przeglądu, w której uczestnikami są koledzy autora, np. członkowie tego samego zespołu. Przykładem przeglądu koleżeńskiego może być przegląd nieformalny, przejrzenie a nawet inspekcja. Najczęstszą jednak formą tego przeglądu jest przegląd techniczny. przegląd koleżeński (ang. peer review) – przegląd produktów powstałych podczas wytwarzania oprogramowania przeprowadzany przez kolegów ich twórcy mający na celu wskazanie defektów i możliwości poprawek Jeszcze jedną odmianą przeglądu jest tzw. kontrola przy biurku. Polega ona na indywidualnym przeglądzie fragmentu kodu lub dokumentacji. kontrola przy biurku, sprawdzanie ręczne (ang. desk checking) – testowanie oprogramowania lub specyfikacji przez manualną symulację jego wykonania Więcej informacji o przeglądach można znaleźć w [30] oraz [74].
6.1.1. Proces dla testowania statycznego Do przeglądów można zastosować podstawowy proces testowy omówiony w rozdziale 3. W fazie planowania i kontroli w przypadku przeglądów powinny
nastąpić: zdefiniowanie kryteriów jakości, identyfikacja uczestników spotkań przeglądowych oraz zaplanowanie spotkania przeglądowego. W fazie analizy i projektowania uczestnikom powinny zostać rozesłane materiały, na podstawie których będzie prowadzony przegląd, takie jak obiekt testowania statycznego (np. fragment kodu źródłowego), a także specyfikacje, dokumenty wymagań, listy kontrolne (jeśli spotkanie ma być na nich oparte) itd. Chodzi
o
to,
aby
uczestnicy
spotkania
mieli
czas
na
zapoznanie się
z dokumentami i ich analizę. Dzięki temu dobrze przygotują się do samego spotkania, które będzie przeprowadzone efektywnie. Wraz z przesłaniem dokumentów uczestnicy są informowani o celach spotkania. Faza implementacji i wykonania to samo spotkanie (lub analiza dokumentów dokonywana przez uczestników przeglądu przy własnych biurkach), zebranie wyników spotkania, a także – w przypadku bardziej formalnych przeglądów – zebranie metryk dotyczących przebiegu spotkania oraz jego efektywności (np. liczba zgłoszonych uwag, liczba znalezionych błędów). W fazie ewaluacji kryteriów wyjścia i raportowania uczestnicy przeglądu (lub np. osoba prowadząca przegląd) oceniają wyniki przeglądu pod kątem spełnienia kryteriów wyjścia. Jest również tworzony raport z przeglądu. Faza czynności zamykających polega na upewnieniu się, że wszystko zostało przeprowadzone jak należy i że cała niezbędna dokumentacja została wytworzona i rozesłana odpowiednim adresatom. Jeśli podczas przeglądu zdecydowano o ponownym przeglądzie danego artefaktu, w fazie czynności zamykających, to należy upewnić się, że czas powtórnego przeglądu został już wyznaczony lub – jeśli ma to nastąpić później – że po prostu o tym nie zapomnimy.
6.1.2. Metody sprawdzania oraz możliwe wyniki przeglądu Istnieją trzy główne metody sprawdzania dokumentów poddanych przeglądowi [35]: sprawdzanie zgodności wstecz; sprawdzanie zgodności wewnętrznej; sprawdzanie zgodności w przód.
Sprawdzanie zgodności wstecz to po prostu weryfikacja, czyli upewnienie się, że artefakt poddawany przeglądowi jest zgodny z innym, źródłowym wobec artefaktu dokumentem. Jest to sprawdzanie opierające się na podstawie testów. Na przykład, jeśli przeprowadzamy przegląd kodu jakiegoś modułu, to możemy go sprawdzać pod kątem zgodności ze specyfikacją tego modułu. Sprawdzanie zgodności wewnętrznej można podzielić na dwa rodzaje. Pierwszy z nich polega na tym, że dokument jest sprawdzany pod kątem występowania określonych defektów lub innych kryteriów (np. jakości kodu). Defekty te najczęściej są opisane w postaci listy kontrolnej. Lista taka nie jest dokumentem źródłowym wobec artefaktu poddawanego przeglądowi, ale zewnętrznym, niezależnym dokumentem opisującym po prostu, pod jakim kątem należy sprawdzić dany artefakt. Z drugim rodzajem sprawdzania zgodności wewnętrznej mamy do czynienia wtedy, gdy nie istnieje podstawa testów i musimy sprawdzać dokument sam ze sobą (np. pod kątem występowania niejasności, sprzeczności lub wieloznaczności). Sprawdzanie zgodności w przód polega na upewnieniu się, że przeglądany artefakt będzie przydatny jako podstawa testów w późniejszych fazach cyklu wytwórczego. Wtedy sprawdzanie dokumentu w przyszłości pod kątem zgodności z tym artefaktem, będzie sprawdzaniem zgodności wstecz. Można wyróżnić trzy zasadnicze decyzje, jakie zespół przeglądowy podejmuje wobec sprawdzanego dokumentu: pełna akceptacja – w przypadku nieznalezienia żadnych usterek dokument może być zaakceptowany w takiej formie, w jakiej jest i nie są potrzebne żadne dodatkowe poprawki; akceptacja warunkowa – w przypadku, gdy kontrolowany dokument zawiera defekty, ale nie jest ich zbyt wiele lub nie są zbyt poważne i wiadomo, że da się je bezproblemowo naprawić, dokument może zostać zaakceptowany warunkowo (tzn. jest zaakceptowany pod warunkiem wprowadzenia odpowiednich zmian) i nie jest konieczny powtórny przegląd tego dokumentu; odrzucenie – w przypadku, gdy błędów jest zbyt dużo, jakość dokumentu jest bardzo niska lub znalezione usterki są bardzo poważne, może zostać podjęta decyzja o odrzuceniu. Dokument wraca wtedy do autora, który musi go poprawić. Konieczny jest powtórny przegląd poprawionego dokumentu.
6.1.3. Role Osoby biorące udział w przeglądach mogą odgrywać różne role. Przed przystąpieniem do przeglądu każdy uczestnik musi mieć jasno określoną rolę i wiedzieć, z jakimi obowiązkami się ona wiąże. Różne typy przeglądu wymagają uczestnictwa osób odgrywających różne role. Czasami jedna osoba może odgrywać więcej niż jedną rolę. Standardowe role występujące w przeglądach to: decydent; kierownik, moderator, prowadzący; autor (projektant, programista, architekt, tester, analityk, menedżer itp.); przeglądający; protokolant. Decydent to osoba, dla której jest przeprowadzany przegląd. Ma on uprawnienia do podjęcia decyzji, czy zostały spełnione kryteria wyjścia dla przeglądu, czyli czy cel został osiągnięty. Rola ta występuje w bardziej formalnych przeglądach. Kierownik (moderator, prowadzący) to osoba odpowiedzialna za przeprowadzenie całego procesu przeglądu, zarówno od strony administracyjnej (zebranie ludzi, rozsyłanie dokumentów, zwoływanie spotkania), jak i logistycznej (rezerwacja sali na spotkanie). W szczególności moderator prowadzi samo spotkanie przeglądowe i czuwa nad tym, aby przebiegało ono sprawnie. moderator, prowadzący inspekcję (ang. moderator, inspection leader) – lider i główna osoba odpowiedzialna za prowadzenie inspekcji lub przeglądu Autor to osoba odpowiedzialna za wytworzenie przeglądanego artefaktu. Zwykle jest też referentem, to znaczy osobą przedstawiającą pozostałym członkom zespołu przeglądowego dany dokument. Przeglądający (inspektor) to osoba mająca za zadanie odszukiwać problemy w referowanym dokumencie. Przeglądającym może być zarówno doświadczony tester, jak i niedoświadczony programista. Często na spotkanie przeglądowe zaprasza się osobę z niewielkim doświadczeniem lub spoza projektu, ze względu na jej „świeże spojrzenie”. Uwagi od takiej osoby często nakierowują bardziej doświadczonych członków zespołu na znajdowanie nowych defektów.
przeglądający, inspektor, kontroler (ang. reviewer, inspector, checker) – osoba zaangażowana w przegląd, która identyfikuje i opisuje odstępstwa w przeglądanym produkcie lub projekcie; przeglądający mogą być dobierani tak, aby reprezentować różne punkty widzenia i odgrywać różne role w procesie przeglądu Protokolant jest osobą zapisującą przebieg całego spotkania. Osoba pełniąca tę funkcję nie powinna odgrywać jednocześnie innych ról. Chodzi o to, aby zarówno autor, jak i przeglądający w pełni mogli skupić się na merytorycznej stronie przeglądu i nie rozpraszali swojej uwagi robieniem jakichkolwiek notatek. Przeglądy są spotkaniami bardzo wyczerpującymi, gdyż jego aktywni uczestnicy muszą przez cały czas zachować wysoki poziom koncentracji i skupienia. protokolant, skryba, rejestrator (ang. scribe, recorder) – osoba, która podczas spotkania przeglądowego rejestruje w dzienniku przeglądu każdy zgłoszony defekt lub sugestię odnośnie usprawnienia procesu; protokolant musi zapewnić, że dziennik przeglądu jest czytelny i zrozumiały Czasami można spotkać również role lektora oraz referenta. Zadaniem lektora jest odczytywanie treści analizowanego dokumentu. Praca referenta polega na parafrazowaniu fragmentów przeglądanego dokumentu podczas spotkania.
6.1.4. Aspekt psychologiczny przeglądów Przeglądy wymagają od uczestników nieustannego skupienia i nie pozwalają na analizę zbyt dużej ilości kodu na jednym spotkaniu. Dlatego muszą być przeprowadzane efektywnie, a nadzór nad ich sprawnym przebiegiem sprawuje moderator. Efektywność może być zaburzona w wyniku różnych mechanizmów psychologicznych. W takich sytuacjach moderator powinien reagować i sprowadzać dyskusję na właściwe tory. Oto przykłady niebezpieczeństw zaburzających poprawny przebieg przeglądu. Prawo Parkinsona, które mówi, że spotkanie zajmuje dokładnie tyle czasu, ile na nie przeznaczymy. Oznacza to, że jeśli prostą sprawę dającą się załatwić w kwadrans omawiać się będzie na spotkaniu trwającym godzinę, to załatwianie tej sprawy będzie trwało godzinę. Jeśli zaś
sprawa wymaga długich narad, ale czasu jest niewiele, to sprawa zwykle będzie załatwiona szybko. Dlatego, aby z jednej strony nie marnować czasu, a z drugiej nie zmuszać ludzi do pracy pod jego presją, należy dobrze wymierzyć czas spotkania przeglądowego. Często jest to zadanie trudne; dobra estymacja czasu spotkania wymaga od moderatora sporego doświadczenia. Rozwiązywanie
problemów
podczas
przeglądu
–
uczestnicy
przeglądów, w sytuacji wykrycia defektu w analizowanym kodzie lub innym dokumencie, często mają tendencję do natychmiastowego proponowania rozwiązania tego problemu. Moderator musi w takiej sytuacji szybko reagować i przypominać, że celem spotkania jest znajdowanie błędów, a nie debugowanie. Takie dyskusje są z punktu widzenia celów przeglądu stratą czasu i powodują zmniejszenie efektywności spotkania, gdyż na wykrycie kolejnych defektów pozostaje mniej czasu. Proces naprawy powinien następować po przeglądzie. Wyjątkiem są tu drobne, oczywiste błędy, które można poprawić „od ręki”. Napięcie na linii autor–przeglądający. Referowanie własnego kodu, zwłaszcza przed większą liczbą osób, na formalnym spotkaniu, często jest dużym stresem dla autora. Co więcej, uwagi przeglądających może on odbierać jako atak pod jego adresem. Moderator musi zadbać o to, aby wszyscy uczestnicy przeglądów mieli świadomość wspólnego celu spotkania, jakim jest wykrycie możliwie największej liczby defektów. Autor musi mieć poczucie, że proces ten nie ma nic wspólnego z ocenianiem jego umiejętności jako twórcy przeglądanego dokumentu. Powinien też traktować przegląd jako okazję do zwiększenia swoich umiejętności – przeglądającymi często są osoby z dużym doświadczeniem, których merytoryczne uwagi mogą być cennymi wskazówkami dla autora.
6.1.5. Typy przeglądów Istnieje kilka typów przeglądów. Różnią się one poziomem formalizacji oraz uczestnikami. Chociaż wszystkie mają na celu wykrywanie defektów, mają też inne cele, odmienne dla poszczególnych przeglądów.
Przegląd nieformalny to, jak sama nazwa wskazuje, najmniej sformalizowana forma testów statycznych. Przeglądy nieformalne przeprowadza się bardzo często, a ich uczestnicy mogą nawet nie być świadomi, że to, co robią jest przeglądem. Najczęściej polega on na tym, że np. jeden członek zespołu pyta drugiego o radę, prosi o pomoc w odszukaniu usterki w kodzie czy też o sprawdzenie poprawności gramatycznej jakiegoś tekstu. W przeglądach nieformalnych nie generuje się żadnych dodatkowych dokumentów ani raportów. Uczestnikami są zwykle autor oraz jeden lub kilku uczestników odgrywających rolę przeglądających. Przegląd nieformalny może dotyczyć dowolnego artefaktu, a o czasie jego przeprowadzenia oraz zakończenia decyduje autor. Zwykle przegląd ten odbywa się przy biurku autora. Przeglądy nieformalne są tanie i łatwe do przeprowadzenia. Powodzenie przeglądu zależy od doboru właściwych uczestników, ale trudno przecenić zyski płynące z tej formy testowania statycznego. przegląd nieformalny, przegląd ad hoc (ang. informal review, ad hoc review) – przegląd, który nie jest oparty na formalnej (udokumentowanej) procedurze Przejrzenie to forma przeglądu, w której prezentacja analizowanego dokumentu następuje krok po kroku. Taka forma prezentacji zmusza autora do określonego sposobu przedstawiania dokumentu i często skutkuje tym, że większość błędów jest wykrywanych przez samego autora podczas prezentacji. Głównym celem przejrzenia jest wykrywanie defektów, ale innym, nie mniej ważnym, celem jest ustanowienie wśród uczestników konsensusu co do sposobu rozumienia treści dokumentu. Po udanym przejrzeniu mamy pewność, że wszyscy rozumieją dany dokument w ten sam sposób. Przejrzenie może dotyczyć dowolnego dokumentu, jednak najczęściej jest nim kod lub projekt architektury systemu, gdyż wspólne, jednakowe rozumienie tych artefaktów przez wszystkich członków zespołu jest sprawą kluczową. Przejrzenia nie są zwykle bardzo formalne. Uczestnikami przejrzenia, oprócz autora, są wyłącznie przeglądający (zwykle do 6–7 osób). Przeglądający mogą przed spotkaniem otrzymać analizowany dokument, ale nie ma obowiązku przygotowania się przez zaznajomienie się z tym dokumentem przed spotkaniem. Po spotkaniu wszyscy uczestnicy powinni otrzymać nieformalny raport podsumowujący przejrzenie.
Jedną z form, jakie można przyjąć podczas przejrzenia jest tzw. zabawa w kompilator. Można ją stosować, gdy analizowanym dokumentem jest kod lub jego fragment. Uczestnicy „na sucho” wykonują kod programu, zapisując na tablicy stan wszystkich zmiennych. Takie przejrzenie można na przykład stosować, gdy autor jest świadomy istnienia usterki w kodzie, ale nie może jej znaleźć. Samo spotkanie nie powinno trwać dłużej niż 2 godziny. Jeśli zespół nie jest w stanie w takim czasie przejrzeć całego dokumentu, to należy wybrać jego najbardziej reprezentatywną część lub najistotniejszy fragment. przejrzenie, przejrzenie zorganizowane (ang. walkthrough, structured walkthrough) – przedstawienie przez autora, krok po kroku, dokumentu w celu zebrania informacji i ustalenia wspólnego rozumienia jego zawartości ([71], [75]) Przegląd techniczny jest przeglądem koleżeńskim i skupia się na osiągnięciu zgody, co do tego, jakie należy przedsięwziąć czynności techniczne wobec analizowanego dokumentu. Cechą charakterystyczną przeglądu technicznego jest etap podejmowania decyzji. Przedmiotem przeglądu technicznego może być każdy dokument techniczny (to znaczy niezwiązany bezpośrednio z kwestiami zarządczymi). W przeglądzie technicznym role są zwykle dość precyzyjnie określone. Osobą zarządzającą procesem przeglądu technicznego jest kierownik, którym nie może być autor. Samo spotkanie jest prowadzone przez moderatora. W spotkaniu uczestniczy również autor oraz przeglądający. Kierownik może odgrywać rolę moderatora. Skład osobowy nie powinien przekraczać 10 osób. Ważne jest, aby osoby biorące udział w przeglądzie technicznym zajmowały w organizacji stanowiska mniej więcej na tym samym poziomie. Zaleca się, aby w przeglądach technicznych nie brał udziału menedżer, gdyż ze względów psychologicznych oraz organizacyjnych dotyczących relacji menedżera z podwładnymi może to wpłynąć na niższą efektywność spotkania. Spotkanie jest zwoływane przez kierownika. Moderator przedstawia cel spotkania, po czym dokument jest przeglądany strona po stronie. Uczestnicy mogą zgłaszać uwagi, które powinny być notowane przez protokolanta lub przez samych przeglądających. Uczestnicy pod koniec spotkania powinni osiągnąć porozumienie dotyczące ewentualnych zmian w dokumencie.
O ile podczas przejrzenia autor referuje swój dokument, o tyle podczas przeglądu technicznego nie zabiera głosu. Może odpowiadać na pytania zadawane przez przeglądających, ale nie powinien przyjmować postawy obronnej – służy jako źródło dodatkowych informacji o dokumencie. Efektywność w znajdowaniu defektów w przypadku przeglądu technicznego jest zwykle wyższa niż w przypadku przeglądów nieformalnych czy przejrzeń. Spotkania przeglądowe są znakomitą okazją dla uczestników, aby uczyć się wzajemnie od siebie i dzielić doświadczeniem. przegląd techniczny (ang. technical review) – dyskusja w grupie współpracowników skupiająca się na osiągnięciu porozumienia w zakresie aspektów technicznych ([30], [71]) Przegląd kierowniczy (menedżerski) dotyczy Dokumenty poddawane przeglądowi kierowniczemu to:
kwestii
zarządczych.
plany związane z projektem (dotyczące harmonogramu, kosztorysu, zasobów, jakości, ryzyka itp.); plany odnoszące się do produktu (dotyczące bezpieczeństwa, utrzymywania systemu, instalacji, kopii zapasowych itp.); raporty (dotyczące postępu w pracach, incydentów, wyników innych przeglądów itp.); dokumenty prawne (plany umów, kontraktów, Servce Level Agreement itp.). Efektem przeglądu kierowniczego może być znajdowanie usterek w powyższych dokumentach, ale znacznie ważniejszym celem jest monitorowanie postępów projektu. Podczas spotkania można porównać plany z raportami i w ten sposób ocenić, czy projekt posuwa się naprzód w należytym tempie. Przeglądy menedżerskie zwykle odbywają się w okolicy kamieni milowych projektu. Wynikiem przeglądu kierowniczego są decyzje menedżerskie. Dotyczyć one mogą przydziału dodatkowych zasobów, realokacji środków, modyfikacji planów, dostosowania poziomu ryzyka itd. kamień milowy (ang. milestone) – punkt w czasie realizacji projektu, dla którego zostały określone (pośrednie) produkty oraz wyniki
W przeglądzie kierowniczym musi brać udział osoba uprawniona do podejmowania decyzji (np. menedżer), a także kierownik, przeglądający i
protokolant.
Przeglądającymi
powinni
być
udziałowcy
projektu
(ang.
stakeholders), a także osoby – zarówno z pionu menedżerskiego, jak i technicznego – odpowiedzialne za wprowadzanie podjętych na spotkaniu decyzji w życie. Organizacja przeglądu kierowniczego jest podobna do organizacji przeglądu technicznego i ma podobny stopień sformalizowania. Jedyna różnica polega na tym, że od uczestników przeglądu kierowniczego oczekuje się przygotowania przed spotkaniem tak, aby znali dobrze bieżący status projektu i orientowali się w postępach prac wytwórczych. Po spotkaniu tworzony jest raport opisujący listę rzeczy do zrobienia wraz ze wskazaniem osób odpowiedzialnych za wykonanie poszczególnych czynności. przegląd kierowniczy, przegląd menedżerski (ang. management review) – systematyczna ocena procesu zakupu, dostawy, wytworzenia, działania lub utrzymania oprogramowania wykonywana przez kierownictwo albo w jego imieniu, która monitoruje postępy, określa status planów i harmonogramów, zatwierdza wymagania oraz ich alokację, a także ocenia skuteczność metod zarządzania Inspekcja to bardzo formalny rodzaj przeglądu. Cały proces jej przeprowadzania (w tym określenie ról odgrywanych przez uczestników) musi być dokładnie zdefiniowany. Moderator oraz uczestnicy muszą być przeszkoleni w zakresie przeprowadzania inspekcji. Podstawą tego typu przeglądu jest zawsze konkretny materiał źródłowy, czyli tzw. dokumentacja bazowa. Inspektorzy szukają określonych typów problemów, często z wykorzystaniem tzw. listy kontrolnej na użytek inspekcji. W ciągu całego procesu zbiera się odpowiednie metryki, które są poddawane dalszej analizie. Tak dokładne i formalne zdefiniowanie ról, obowiązków i metryk ma głębokie uzasadnienie, które staje się jasne, gdy spojrzymy na dwa podstawowe cele inspekcji. Pierwszy z nich to znajdowanie defektów w produkcie, a drugi to znajdowanie defektów w procesie. Cele te można również zdefiniować bardziej ogólnie, jako ulepszanie produktu oraz ulepszanie procesu. Dzięki użyciu dobrze zdefiniowanych metryk jesteśmy w stanie ulepszać sam proces wytwarzania oprogramowania oraz kontrolować jego jakość. Lepszy proces wpływa
pozytywnie na jakość końcową produktu oraz w kolejnych projektach. Uczestnicy inspekcji odgrywają następujące role:
produktów
tworzonych
kierownik inspekcji; autor; inspektorzy; moderator; protokolant. Kierownik jest odpowiedzialny za przeprowadzenie całego procesu inspekcji. Do jego zadań należy uformowanie zespołu inspekcyjnego, zaplanowanie czynności, monitorowanie i nadzór procesu inspekcji, podejmowanie działań korygujących, jeśli zajdzie taka potrzeba, to weryfikacja spełnienia kryteriów wyjścia inspekcji, zamykanie inspekcji, informowanie udziałowców o wynikach inspekcji, branie aktywnego udziału w ulepszaniu procesu. Często kierownik odgrywa również rolę moderatora i bierze udział w spotkaniach inspekcyjnych. Kierownik musi być osobą przeszkoloną nie tylko w zakresie samego procesu inspekcji, lecz także w takich obszarach, jak: plan metryk w organizacji, metodyki ulepszania procesów, statystyczne metody kontroli procesu, czy też polityka przeprowadzania inspekcji w danej organizacji. W inspekcji muszą być dobrze zdefiniowane kryteria wejścia i wyjścia, to znaczy musi być wiadome, kiedy można rozpocząć przeprowadzanie inspekcji i kiedy można ten proces uznać za zakończony. Osobą odpowiedzialną za weryfikację spełnienia tych kryteriów jest kierownik inspekcji. Przykładowe kryteria wejścia: dostępność odpowiednich list kontrolnych; dokumenty bazowe przeszły inspekcję i mają dopuszczalny poziom pozostałych defektów; dokument poddawany gramatycznej.
inspekcji
przeszedł
kontrolę poprawności
Przykładowe kryteria wyjścia: wszystkie wykryte poważne defekty zostały usunięte; dokument poddany inspekcji ma dopuszczalny poziom defektów;
wszystkie wymagane raporty odpowiednich osób.
zostały
napisane i
rozesłane do
Inspekcja Fagana składa się z sześciu podstawowych faz przedstawionych na rysunku 6.2. W fazie planowania jest ustalany skład zespołu inspekcyjnego, są przydzielane role oraz jest estymowany czas wymagany na przegląd dokumentu. Dobór właściwych inspektorów jest kluczowy dla powodzenia całego procesu. Muszą oni mieć odpowiednie umiejętności, doświadczenie, znajomość biznesu i być dostępni w czasie trwania inspekcji.
Rysunek 6.2. Podstawowy model inspekcji Fagana W zależności od rozmiaru dokumentu i stopnia jego skomplikowania wylicza się, jak dużo elementów (linie kodu, strony dokumentacji, wymagania itp.) może być poddanych inspekcji na jednostkę czasu. Na podstawie tych obliczeń planuje się odpowiednią ilość czasu na inspekcję, w szczególności na spotkanie inspekcyjne. Można przeprowadzić również analizę ryzyka i zdecydować, czy inspekcji ma być poddawany dokument w całości, czy też tylko jakaś jego część. Jeśli dokument ma duży rozmiar, to można podzielić go na fragmenty i dla każdego z nich zaplanować osobną sesję inspekcyjną. Kierownik musi w fazie planowania określić, jakie metryki będą zbierane podczas inspekcji. W szczególności wśród tych metryk powinny się znaleźć: miary objętości dokumentów; liczba znalezionych defektów, sklasyfikowanych wg stopnia dotkliwości; koszt poprawy;
ich
czas poświęcony na poszczególne czynności. Faza przeglądu obejmuje szkolenie, przypisanie ról inspekcyjnych, dystrybucję materiałów do uczestników oraz ustalenie harmonogramu prac, w szczególności terminu spotkania inspekcyjnego. Kierownik musi mieć pewność, że wszyscy uczestnicy inspekcji będą w tym czasie dostępni oraz że każdy z nich rozumie dobrze swoją rolę w procesie. Szkolenie może dotyczyć samej inspekcji, ale również dokumentów poddawanych inspekcji. Jeśli uczestnicy inspekcji są doświadczeni, to nie ma konieczności organizowania spotkania w fazie przeglądu – przypisanie ról następuje indywidualnie, a materiał jest wysyłany bezpośrednio do inspektorów bez przeprowadzania żadnych szkoleń. Faza przygotowania uczestników jest kluczową fazą inspekcji. To w tej fazie następuje właściwe testowanie dokumentu poddawanego kontroli. Każdy inspektor przegląda indywidualnie dokument, pamiętając o wykonaniu standardowych czynności tej fazy, takich jak: zapisanie czasu rozpoczęcia fazy; zapisanie wszystkich uwag, pytań i wątpliwości co do przeglądanego dokumentu; wykorzystanie czasu przeznaczonego na przygotowanie (nie więcej i nie mniej), aby zachować tempo sprawdzania poszczególnych elementów dokumentu; zliczanie napotkanych problemów. Problemy znalezione w czasie fazy przygotowania powinny być przypisane do jednej z trzech kategorii: poważne; mniej istotne; niejasne. Problem o statusie „niejasny” oznacza, że inspektor nie może do końca przypisać konkretnej kategorii i wymaga to uzyskania dodatkowych informacji przez autora. Koncepcja roli inspektora bierze się z faktu, że ludzie szukają tego, co chcą znaleźć. Jeśli każemy inspektorom skupić się na wykrywaniu błędów określonego typu (stąd powszechne stosowanie list kontrolnych w inspekcjach), to inspektor
będzie analizował dokument tylko pod tym kątem i szanse na znalezienie defektów danego typu się zwiększą. Jeśli inspektor nie będzie miał konkretnych wytycznych, to jego efektywność może być o wiele mniejsza. Hass [35] wymienia następujące typy wytycznych dla roli inspektora: lista kontrolna – inspektor przegląda dokument pod kątem zadań lub problemów opisanych w liście kontrolnej; dokumenty – inspektor sprawdza zgodność między dokumentami; skupienie – inspektor szuka w dokumencie konkretnego problemu, np. związanego z użytecznością, aspektem finansowym, użyciem odpowiedniego słownictwa; perspektywa – inspektor analizuje dokument, wchodząc w rolę przyszłego użytkownika tego dokumentu, np. projektanta, programisty lub testera; ważne jest, aby inspektor w codziennej pracy odgrywał tę samą rolę lub dobrze ją rozumiał; procedura – inspektor sprawdza dokument, podążając za określoną procedurą; scenariusz – inspektor analizuje dokument pod kątem konkretnego procesu dla konkretnej roli; jest to rola bardziej szczegółowa od perspektywy; standard – inspektor sprawdza, czy dokument jest zgodny z danym standardem, np. ISO, IEEE, standardami dziedzinowymi lub wewnętrznymi stosowanymi w organizacji; punkt widzenia
–
inspektor
przegląda
dokument
jako
osoba
odgrywająca inną rolę, np. użytkownik, analityk, tester, projektant; jest to rola podobna do perspektywy, przy czym jako punkt widzenia są wykorzystywane odpowiednie dokumenty bazowe. Celem fazy spotkania inspekcyjnego jest zebranie wszystkich uwag od inspektorów i przedyskutowanie ich. Spotkanie takie może się wydawać niepotrzebne wobec faktu wykrycia defektów w fazie poprzedniej, ale praktyka pokazuje, że na spotkaniach inspekcyjnych wykrywa się dodatkowo 10–20% więcej usterek. Jeśli jednak inspektorzy znaleźli niewielką liczbę problemów, to spotkanie inspekcyjne może zostać pominięte. Rola moderatora na spotkaniu inspekcyjnym jest taka sama jak w każdej innej formie przeglądu. Ma za zadanie trzymać uczestników „w ryzach”, pilnować, aby
dyskusja była merytoryczna, skupiona na problemach, a nie sposobach ich rozwiązywania oraz zapobiegać konfliktom pojawiającym się na skutek obecności osób o tzw. niskich umiejętnościach miękkich. Moderator zbiera od inspektorów wyniki ich prac z fazy przygotowania, w szczególności ich nazwiska oraz sumaryczną liczbę znalezionych problemów. Następnie moderator odczytuje dokument element po elemencie. Inspektorzy opisują jakie znaleźli problemy w danym fragmencie dokumentu. Dyskusja powinna być skoncentrowana na problemach poważnych oraz niejasnych. Problemy mniej istotne mogą być przekazane protokolantowi później, gdyż nie wymagają zwykle tak dużej uwagi jak pozostałe. W przypadku, gdy podczas spotkania zostaną znalezione nowe problemy, muszą być one zaraportowane i opisane. Pozwoli to między innymi na obliczenie pewnego aspektu efektywności inspekcji, polegającego na zdolności do wykrywania nowych błędów. Faza zmiany to etap, w którym następuje naprawa defektów znalezionych w fazach przygotowania oraz spotkania inspekcyjnego. Naprawy dokonuje zwykle autor. Jeśli znalezione błędy dotyczą dokumentów, których zespół nie może zmieniać samodzielnie, to błędy te powinny zostać przekazane osobom mającym takie uprawnienia (np. błąd znaleziony w strategii testowej może być zmieniony tylko przez kierownika testów). Gdy wszystkie cykle inspekcyjne zostaną zakończone, a kryteria wyjścia spełnione, proces przechodzi do fazy kontynuacji (ang. follow-up). W fazie tej kierownik inspekcji zbiera i opracowuje wszystkie pomiary dokonane podczas całego procesu. Może też oszacować na ich podstawie, wykorzystując też dane historyczne, liczbę pozostałych, niewykrytych problemów w kontrolowanym dokumencie. Dokumentuje też wszystkie pomysły na poprawę procesu w przyszłości. Inspekcje są drogie, ale opłacalne. Na rysunku 6.3 przedstawiono porównanie typowych nakładów pracy w dwóch projektach, z których jeden wykorzystuje inspekcje, a drugi nie. Z wykresu widać, że w początkowej fazie nakłady pracy w projekcie z inspekcjami są wyższe. Jest to zrozumiałe, gdyż koszt inspekcji jest zwykle duży: inspekcje wymagają
Rysunek 6.3. Porównanie nakładu pracy w projekcie z inspekcjami i bez nich czasu na przygotowanie, spotkania, tworzenie dokumentacji, być może także szkolenia. Jednak inwestycja ta zwraca się w końcowych fazach projektu. Dzięki stosowaniu inspekcji już we wczesnych fazach następuje wykrycie dużej liczby usterek, przez co nie trzeba tracić czasu na wyszukiwanie i eliminowanie defektów w późniejszym etapach. Ponadto należy pamiętać, że dzięki inspekcji usprawniamy sam proces, co jest inwestycją także w jakość przyszłych projektów. Lista korzyści z przeprowadzania inspekcji jest długa. Sam Fagan [76] wymienia następujące: redukcja błędów (defektów) polowych użytkowników po wydaniu oprogramowania);
(zgłaszanych
przez
zwiększona satysfakcja klienta; poprawa produktywności, zmniejszenie całkowitych nakładów pracy (dostarczenie większej ilości funkcji w danym czasie lub redukcja czasu do wydania); ulepszenie harmonogramów spotkań; wymiana doświadczeń między deweloperami; proces ciągłej poprawy przez usuwanie defektów systemowych, które są źródłem defektów w produkcie;
szybka nauka unikania błędów podczas pisania kodu przez programistów, którzy uczestniczą w inspekcji swojego oraz cudzego kodu; budowanie zgranego zespołu; w niektórych przypadkach inspekcje pozwalają wyeliminować testy jednostkowe. inspekcja (ang. inspection) – rodzaj przeglądu koleżeńskiego polegający na wizualnej weryfikacji dokumentów w celu wykrycia defektów, np. niezgodności ze standardami projektowymi lub dokumentacją wyższego poziomu; jest to najbardziej formalna technika przeglądu, zawsze oparta na udokumentowanej procedurze ([7], [71]) Audyt to najbardziej formalna technika testowania statycznego. Jest przeprowadzany przez audytorów, przeszkolonych w zakresie przeprowadzania tego typu kontroli. Audyt może być zewnętrzny lub wewnętrzny. W tym drugim przypadku audytorem jest pracownik audytowanej organizacji, ale zewnętrzny wobec zespołu wytwórczego. Audyty przeprowadza się zwykle pod koniec cyklu wytwórczego oprogramowania. Audyt powinien stanowić wartość dodaną dla organizacji przez dostarczenie następujących informacji (stanowiących jednocześnie cele audytu) [77]: czy wykorzystywane w organizacji standardy, procesy, systemy i plany są zgodne z polityką firmy, jej wymaganiami oraz celami; czy pracownicy organizacji prawidłowo wykorzystują te dokumenty podczas wykonywania swojej pracy; jaka jest efektywność wykorzystania tych dokumentów przy osiąganiu założonych celów; czy zasoby (ludzie, wykorzystywane;
sprzęt,
infrastruktura
itp.)
są
efektywnie
czy wytwarzane produkty są zgodne z wymaganą specyfikacją i standardami. W audytach można wyróżnić kilka ról: klient – osoba lub organizacja zamawiająca audyt;
kierownictwo audytora – odpowiedzialne za przypisanie głównego audytora (ang. lead auditor) do audytu oraz współpraca z nim w zakresie tworzenia zespołu audytorów; główny audytor (kierownik audytu) – osoba odpowiedzialna za zaplanowanie i przeprowadzenie audytu, tworzenie planów audytu, zarządzanie zespołem audytorów, podejmowanie decyzji dotyczących prac
audytorów,
komunikację
z
klientem,
organizację
z pracownikami organizacji, przegląd wyników koordynację i kontrolę wykonania akcji naprawczych;
spotkań
audytu
oraz
audytorzy (ang. auditors) – osoby przygotowujące i przeprowadzające wspólnie z głównym audytorem audyt, zbierające informacje, oceniające zebrane informacje, uczestniczące w spotkaniach audytowych oraz raportujące do głównego audytora wyniki swojej pracy; kierownictwo organizacji audytowanej (ang. auditee management) – współpracuje z głównym audytorem, informuje pracowników organizacji o mającym być przeprowadzonym audycie, udostępnia audytorom niezbędne dokumenty oraz zasoby (w tym ludzkie), dostarcza informacji niezbędnych audytorom do wykonania swojej pracy, upewnia się, czy akcje naprawcze zostały poprawnie wykonane i dostarcza na to dowodów głównemu audytorowi; audytowani (ang. auditee) – pracownicy audytowanej organizacji, z którymi audytorzy przeprowadzają wywiady, zbierając różnego rodzaju informacje. Audytowani są zobowiązani do dostarczenia wszystkich niezbędnych dla audytorów danych. Audytor w swojej pracy często wykorzystuje tzw. ścieżkę audytu, czyli śledzi wstecz krok po kroku czynności, jakie powinny zostać wykonane w ramach danego procesu i ocenia ich poprawność na każdym etapie. Na przykład rozpoczynając od analizy raportu z testów, audytor może cofnąć się do etapu wykonywania testów, następnie przeanalizować dokumentację projektową testów, aby na samym końcu przejść do analizy wymagań, na podstawie których testy te zostały zaprojektowane. ścieżka audytu (ang. audit trial) – ścieżka wstecz, wzdłuż której śledzi się oryginalne wejście do procesu (np. dane), rozpoczynając od wyjścia procesu
jako punktu startu; ułatwia to analizę defektów i umożliwia przeprowadzenie audytu procesu Wynikiem audytu jest raport, który może zawierać obserwacje, uwagi, wnioski, sugestie poprawy co do audytowanego dokumentu lub procesu. Zawiera także ostateczną decyzję (tak/nie) dotyczącą tego, czy kontrolowany obiekt jest zgodny z odpowiednimi normami bądź przepisami. Wadą audytu jest jego koszt oraz fakt, że mimo najwyższego stopnia formalizacji jest to najmniej efektywna technika statyczna, jeśli chodzi o wykrywanie problemów. Czasami konieczność przeprowadzenia audytu wynika z zapisów kontraktu lub z przepisów prawa. audyt (ang. audit) – niezależna ocena oprogramowania lub procesów w celu ustalenia zgodności ze standardami, wytycznymi, specyfikacjami oraz/lub procedurami, oparta na obiektywnych kryteriach, wliczając dokumenty, które określają: 1) postać lub zawartość produkowanego produktu; 2) proces, według którego produkt powinien być produkowany; 3) sposób pomiaru stosowania się do standardu lub specyfikacji [71]
6.1.6. Biznesowa wartość przeglądów Zysk z przeprowadzenia przeglądów można wyrazić za pomocą konkretnych wartości liczbowych. W tabeli 6.2 jest przedstawiona przykładowa symulacja kosztów testowania i usuwania defektów dla projektu w dwóch wersjach: z inspekcją i bez inspekcji. Tabela 6.2. Prosta analiza kosztów i zysków z przeprowadzenia przeglądu
symulacja (ang. simulation) – odwzorowanie wybranych charakterystycznych zachowań jednego fizycznego lub abstrakcyjnego systemu przez inny system [78] Taka analiza bardziej przemawia do wyobraźni niż omówiony wykres z rysunku 6.3. Rozważmy dwie wersje tego samego projektu i w celu uproszczenia skupmy się na fazie wymagań, traktując pozostałe etapy projektu jako jedną fazę. Projekty różnią się tym, że w pierwszym nie przeprowadzono inspekcji wymagań, a w drugim to uczyniono. Druga kolumna tabeli przedstawia symulacje dla projektu nr 1. Zakładając, że w fazie wymagań wprowadzono łącznie 400 defektów i w tej samej fazie znaleziono ich 280, efektywność usuwania defektów w fazie wymagań wynosi 280/400 = 70%. Koszty inspekcji są zerowe, gdyż jej nie przeprowadzono. Wiedząc, że koszt usunięcia 280 defektów wynosił 15 000 USD, możemy obliczyć koszt usunięcia jednego błędu równy ok. 53,5 USD, dzieląc 15 000 przez 280. W pozostałych fazach znaleziono 100 spośród pozostałych 120 błędów, co daje efektywność usuwania defektów w fazach późniejszych równą 100/200 ≈ 83%. W drugim projekcie przeprowadzono inspekcję wymagań, której koszt wyniósł 400 USD. Efektywność usuwania defektów w inspekcji szacujemy, stosując dane
historyczne lub przemysłowe (np. raport Capersa Jonesa [70]) na 85%. Oznacza to, że w wyniku inspekcji znajdziemy 0,85 ⋅ 400 = 340 błędów. Używając wskaźnika kosztu usuwania pojedynczego błędu dla projektu nr 1, obliczamy koszt usunięcia tych defektów jako 340 ⋅ 53,5 USD = 18 190 USD. Używając wskaźnika efektywności usuwania defektów w pozostałych fazach dla projektu nr 1, obliczamy, że w pozostałych fazach wykrytych zostanie 0,83 ⋅ 60 ≈ 50 defektów, których koszt usunięcia wyniesie 50 ⋅ 400 USD = 18 190 USD. Sumując koszty inspekcji oraz usunięcia defektów w fazie wymagań i w fazach następnych, dostajemy 55 000 USD w przypadku projektu bez inspekcji i 42 190 USD w przypadku projektu, w którym inspekcję przeprowadzono. Koszt w tym ostatnim projekcie jest o 23% mniejszy niż w projekcie 1. Ponadto mamy dwa razy mniej defektów polowych (10 wobec 20 w projekcie nr 1). Efektywność inspekcji widać jeszcze wyraźniej, gdy doliczy się koszty usunięcia błędów polowych, które zwykle są o wiele większe niż koszty wykrywania błędów przed wydaniem oprogramowania. Koszt projektu nr 2 może być nawet o 50% mniejszy niż koszt projektu nr 1.
6.1.7. Wdrażanie przeglądów Przeglądy nie są tanie i kierownictwo nie zawsze jest chętne do przeznaczania cennych zasobów (czas, ludzie) do ich wykonania. Aby skutecznie przeprowadzać przeglądy w organizacji, należy wykonać kilka kroków opisanych poniżej. Dobrze jest również opracować plan przeglądów, zawierający w szczególności informacje o tym, co ma być poddane przeglądowi, kto ma uczestniczyć w przeglądzie i jakie są kryteria wejścia i wyjścia. plan przeglądu (ang. review plan) – dokument opisujący podejście, zasoby i harmonogram zamierzonych czynności związanych z przeglądem; identyfikuje on m.in. dokumenty i kod podlegający przeglądowi, typy przeglądów do wykorzystania, uczestników, kryteria wejścia i wyjścia, które będą stosowane w przeglądach formalnych oraz uzasadnienie ich wyboru; jest zapisem procesu planowania przeglądu Zdobyć wsparcie kierownictwa i zademonstrować korzyści wynikające z przeglądów. Należy uświadomić kierownictwu, że przeprowadzenie przeglądu się opłaci. W tym celu najlepiej przedstawić tzw. business-case, np. w formie
podobnej do tej w punkcie 6.1.6. Tego typu symulacje, oparte na konkretnych liczbach, bardzo mocno przemawiają do wyobraźni i pozwalają lepiej zrozumieć korzyści płynące z inwestycji w przeglądy. Kierownictwo powinno także rozumieć długookresowe korzyści z przeprowadzania przeglądów. Stosowanie metryk i wykorzystanie danych historycznych jest bardzo dobrym sposobem na przedstawienie kierownictwu tego typu analiz. Kierownictwo musi mieć wystarczające i dokładne informacje o kosztach, korzyściach i potencjalnych problemach we wdrażaniu przeglądów. Wybrać i opisać procedury przeprowadzania przeglądów. Aby proces przeprowadzania przeglądów przebiegał płynnie i bezproblemowo, musimy mieć opisane wszystkie procedury postępowania, a także wszystkie dokumenty, formularze i wzory raportów, które będą wykorzystywane podczas przeglądu. Należy również zadbać o odpowiednią infrastrukturę, np. wspólne repozytorium zawierające pomiary metryk, takich jak liczba znalezionych błędów, liczba uczestników poszczególnych spotkań, czas trwania przeglądu, efektywność przeglądu. Dane te będą w przyszłości pełniły niezwykle cenną funkcję w szacowaniu kosztów i zysków kolejnych przeglądów. W przypadku inspekcji dane te będą również służyły do oceny poprawy procesu (np. jeśli zauważymy utrzymującą się na tym samym poziomie efektywność inspekcji przy jednoczesnym uczestnictwie mniejszej liczby osób lub krótszym czasie inspekcji, to możemy wnioskować, że jakość procesu wzrosła). Przeprowadzić niezbędne szkolenia. Szkolenia są istotne w przypadku najbardziej sformalizowanych przeglądów, takich jak inspekcje. Szkolenia powinny obejmować zakres czynności wchodzących w skład danego przeglądu, opis poszczególnych ról, jak i techniki pracy poszczególnych uczestników. Uzyskać wsparcie osób uczestniczących w przeglądach. Często zdarza się, że członkowie zespołu sceptycznie podchodzą do technik testowania statycznego. Niektórzy programiści mogą uważać, że np. przejrzenie lub inspekcja nie ma sensu, skoro kod i tak jest przetwarzany przez kompilator, który wykonuje pewne elementy analizy statycznej. Należy uświadomić sceptykom, że przeglądy mogą wykrywać błędy trudne lub niemożliwe do automatycznego wykrycia, takie jak błędy wymagań. Przeglądy mają też olbrzymi walor edukacyjny, ponieważ każdy uczestnik przeglądu dzieli się swoim doświadczeniem z innymi. Przeprowadzić pilotażowe przeglądy. Jeśli w organizacji nie stosowano do tej pory metod testowania statycznego, to można przeprowadzić próbne pilotażowe
przeglądy. Mają one dwa zasadnicze cele. Pierwszy to zapoznanie członków zespołu z tą formą testowania i przyzwyczajenie ich do określonych czynności, które muszą być wykonywane np. podczas spotkań przeglądowych. Członkowie zespołu mogą wdrożyć się w swoje role i w kolejnych przeglądach wykonywać je efektywnie. Drugi cel to znajdowanie potencjalnych problemów w przeprowadzeniu samego procesu. Może się okazać, że np. spotkania trwają zbyt długo, autor czuje się atakowany, uczestnicy nie poświęcili odpowiednio dużo czasu na przygotowanie się do spotkania. Dzięki przeglądom pilotażowym można te problemy rozpoznać i wyeliminować lub przynajmniej zminimalizować ich oddziaływanie w kolejnych przeglądach. Zastosować przeglądy do najważniejszych dokumentów. Przeglądy powinny być efektywne w sensie znajdowania problemów. Niestety nie są one efektywne w sensie swoich „mocy przerobowych”. Dlatego należy zastosować odpowiednią priorytetyzację artefaktów i stosować przeglądy do dokumentów kluczowych, najważniejszych, takich, w których niewykryte błędy mogą nas w przyszłości dużo kosztować. Przykładami takich artefaktów są: wymagania, umowy, plany testów, kod źródłowy kluczowych bądź krytycznych modułów.
6.1.8. Kryteria sukcesu przeglądów Na sukces przeglądu składa się wiele czynników. Sylabus ISTQB [79] wyróżnia następujące czynniki sukcesu w kontekście przeglądów: Czynniki techniczne sukcesu upewnić się, że proces przeglądu jest stosowany poprawnie, zwłaszcza w przypadku przeglądów formalnych takich jak inspekcje; zapisywać koszty przeglądu oraz osiągane korzyści; przeglądać wczesne wersje robocze lub fragmenty dokumentów, aby zidentyfikować wzorce defektów, zanim zostaną wprowadzone w cały dokument; upewnić się, że dokument poddawany kontroli jest gotowy do przeglądu; używać odpowiednich list kontrolnych dla najczęściej spotykanych defektów (w każdej organizacji i w każdym typie projektu mogą to być inne defekty); używać wielu typów przeglądu, w zależności od założonego celu; poddawać przeglądom najważniejsze dokumenty;
namawiać do znajdowania najważniejszych błędów – skupiać się na treści, a nie na formie; ciągle ulepszać proces prowadzenia przeglądu. Czynniki organizacyjne sukcesu upewnić się, że kierownictwo przydzieliło odpowiednią ilość czasu na czynności związane z przeglądami, nawet pod presją terminów; pamiętać, że czas i koszt przeglądów nie są proporcjonalne do znalezionych defektów; zapewnić odpowiednią ilość czasu na poprawianie znalezionych defektów; nigdy nie używać metryk z przeglądów do oceny indywidualnej efektywności ludzi; zapewnić, aby właściwi ludzie byli przypisani do odpowiednich ról i rodzajów przeglądów; organizować formalnych;
szkolenia,
zwłaszcza
w
przypadku
przeglądów
wspierać forum przeprowadzających przeglądy, dzielić się pomysłami i doświadczeniem; upewnić się, że każdy aktywnie uczestniczy w przeglądach; zapewnić wyważony zespół przeglądających oraz mieć na uwadze, że jego uczestnicy mają różne umiejętności i doświadczenie; wspierać działania zmierzające do usprawniania procesu tak, żeby zająć się problemami systemowymi; pokazywać ulepszenia osiągnięte dzięki przeglądom. Czynniki ludzkie sukcesu nauczyć interesariuszy, aby spodziewali się wykrycia i przeznaczyli czas na ich usunięcie oraz ponowny przegląd;
defektów
zagwarantować, aby przegląd był pozytywnym doświadczeniem dla autora; traktować znajdowanie defektów jako coś pożądanego w atmosferze wolnej od obwiniania; zapewnić konstruktywny, pomocny i obiektywny ton komentarzy, unikać subiektywizmu, być profesjonalnym w komunikacji;
nie przeprowadzać przeglądu, jeśli autor tego nie chce lub nie jest do tego przekonany; zachęcać wszystkich do głębokiego przemyślenia najważniejszych aspektów przeglądanych dokumentów.
6.2. Analiza statyczna Analiza statyczna polega na analizowaniu kodu lub architektury systemu bez uruchamiania testowanej aplikacji. Technika ta jest najczęściej stosowana przy użyciu narzędzi. W obszarze metod analizy statycznej kodu można wyróżnić: analizę przepływu sterowania; analizę poprawności sekwencji operacji; analizę przepływu danych; testowanie zgodności ze standardami programowania; generowanie metryk kodu; formalne dowodzenie poprawności; symboliczne wykonanie kodu. W obszarze metod analizy statycznej architektury można wyróżnić: analizę statyczną strony internetowej; analizę grafów wywołań. Modele służące do przeprowadzania analizy statycznej (np. model przepływu sterowania czy danych) są także wykorzystywane w testach dynamicznych, zwłaszcza przy definiowaniu kryteriów pokryć w technikach białoskrzynkowych omówionych w rozdziale 9. analiza statyczna kodu (ang. static code analysis) – analiza kodu źródłowego przeprowadzona bez wykonywania oprogramowania
6.2.1. Analiza przepływu sterowania Analiza przepływu sterowania to metoda analizy kodu pod kątem kolejności wykonywania instrukcji. Może być przeprowadzana bezpośrednio na kodzie lub
wykorzystywać model, np. graf przepływu sterowania opisany w punkcie 5.4.1. Typowe problemy znajdowane w trakcie analizy przepływu sterowania to: znajdowanie martwego kodu; znajdowanie funkcji, które nie są wywoływane. Fakt istnienia zarówno martwego kodu, jak i niewywoływanych funkcji może być wynikiem błędu programisty, ale najczęściej wynika z wielokrotnego wykorzystywania tego samego kodu czy modułów. Jeśli projekt jest bardzo silnie oparty na reużywalnych komponentach bądź fragmentach kodu, dobrze jest stosować analizę statyczną.
1 function f(x) 2 y:=x+1 3 if (y>x) then 4 y:=y*y 5 else 6 y:=y/2 7 end Listing 6.1. Przykład programu z martwym kodem Rozważmy przykład programu z listingu 6.1. Zauważmy, że warunek w linii 3. będzie zawsze spełniony, w wyniku czego instrukcja w linii 6. nigdy nie będzie wykonana. Linia 6. stanowi więc martwy kod. martwy kod, nieosiągalny kod (ang. dead code, unreachable code) – kod, który nie może być osiągnięty przez żaden przepływ sterowania i dlatego nie może być wykonywany W przypadku prostych sytuacji, podobnych do tej z listingu 6.1, martwy kod może być znaleziony przy użyciu narzędzia, a nawet przez kompilator podczas kompilacji. Do bardziej skomplikowanych programów można użyć wykonania symbolicznego opisanego w punkcie 6.2.8.
6.2.2. Poprawność sekwencji operacji
Modele takie jak graf przepływu sterowania mogą być użyte do sprawdzenia poprawności sekwencji wywołań operacji. Na przykład, wiele obiektów abstrakcyjnych typów danych musi być zainicjalizowanych przed ich użyciem, plik musi być otwarty przed dokonaniem jakiejkolwiek operacji na nim. Rozważmy abstrakcyjną strukturę danych kolejki z ograniczoną pojemnością z następującymi operacjami: CreateQueue(Q, n) – operacja tworząca kolejkę Q o pojemności n elementów; Enqueue(Q, e) – operacja wstawiająca do kolejki Q element e; Dequeue(Q) – operacja pobierająca element z kolejki Q; RemoveQueue(Q) – operacja usuwająca kolejkę Q z pamięci. Dla tak zdefiniowanego ograniczenia:
typu
danych
mamy
następujące
naturalne
1. Enquque, Dequeue i RemoveQueue dla Q nie mogą zostać wykonane, zanim nie zostanie wykonane CreateQueue dla Q; 2. po wykonaniu RemoveQueue(Q) nie można wykonać Enqueue, Dequeue ani RemoveQueue dla Q, chyba że wcześniej ponownie wywołane zostanie CreateQueue dla Q; 3. Dequeue nie powinno być wywoływane na pustej kolejce; 4. Enqueue nie powinno być wywoływane dla kolejki zawierającej maksymalną liczbę elementów.
Rysunek 6.4. Graf przepływu sterowania dla programu wykorzystującego kolejkę z ograniczoną pojemnością Rozważmy graf przepływu sterowania, przedstawiony na rysunku 6.4, dla programu wykorzystującego tak zdefiniowaną kolejkę. Widzimy, że metody CreateQueue, Enqueue, Dequeue oraz RemoveQueue są wywoływane odpowiednio w wierzchołkach 1, 2, 6 i 4. Zakładamy, że wszystkie dotyczą jednej i tej samej kolejki Q. Wykorzystując ten graf, możemy przeprowadzić analizę statyczną pod kątem poprawności wywoływania operacji na kolejce. Zauważmy, że występuje tu kilka błędów lub co najmniej podejrzanych miejsc, wymagających szczegółowego sprawdzenia.
pętla (1, 2, 1) może wykonać się więcej razy niż pojemność kolejki, co naruszy zasadę nr 4; należy sprawdzić, czy warunek pętli zapewnia, że dla pełnej kolejki z wierzchołka 1 można przejść wyłącznie do 3; jeśli sterowanie przejdzie od razu ścieżką (1, 3, 6), to zostanie naruszona zasada nr 3 (pobranie elementu z pustej kolejki); należy się upewnić, że krawędzią (1, 3) możemy przejść wyłącznie w sytuacji, gdy kolejka nie jest pusta; pętla (3, 6, 7, 3) może zostać wykonana więcej razy niż liczba elementów w kolejce naruszając ponownie zasadę nr 3; wykonanie ścieżki (4, 7, 3, 6) naruszy zasady nr 1 i 2.
6.2.3. Analiza przepływu danych Analiza przepływu danych wykorzystuje model przepływu danych opisany w punkcie 5.4.3. Dotyczy wykrywania problemów podobnych do niepoprawnych sekwencji wywołań opisanych w poprzednim podrozdziale, przy czym rozważanymi operacjami są użycie, definicja oraz zabicie zmiennej, zapisywane skrótowo przy użyciu liter „d” (ang. definition), „u” (ang. use) oraz „k” (ang. kill). Analiza taka zawsze dotyczy konkretnej zmiennej. W tabeli 6.3 opisano wszystkie możliwe następstwa operacji d, u, k. Jeśli w grafie przepływu danych występuje niepożądane następstwo operacji (np. dk, dd, -u), to prawdopodobnie w kodzie jest defekt, a co najmniej kod wymaga analizy i być może refaktoryzacji. Tabela 6.3. Możliwe typy następstw operacji na zmiennych
T yp Uwagi -d
pierws za definicja zmiennej, normalna s ytuacja
-u
użycie bez definicji – możliwy defekt (ale może też dotyczyć zmiennej g lobalnej)
-k
zabicie bez definiowania – prawdopodobnie defekt
d-
prawdopodobnie defekt – być może martwa zmienna
u-
os tatnia akcja to użycie – dopus zczalne (choć nie ma zabicia)
k-
os tatnia akcja to zabicie zmiennej – normalna s ytuacja
dd
redefinicja zmiennej bez użycia – prawdopodobnie defekt
du
definicja a potem użycie – normalna s ytuacja
dk
prawdopodobnie defekt – zmienna po zdefiniowaniu nie była użyta
ud
redefinicja zmiennej po użyciu – normalna s ytuacja
uu
dwukrotne użycie zmiennej – normalna s ytuacja
uk
zabicie zmiennej po użyciu – normalna s ytuacja
kd
powtórna definicja po zabiciu – dopus zczalne; niektórzy uznają to za defekt
ku
użycie po zabiciu – poważny defekt
kk
powtórne zabicie zmiennej – defekt
Następstwo operacji dla danej zmiennej zawsze dotyczy wykonywalnej sekwencji instrukcji I1, I2, …, In takich, że pierwsza operacja występuje w I1, druga w In i między nimi nie występuje żadna inna operacja na tej zmiennej. Na przykład, we fragmencie programu z listingu 6.2 dla zmiennej x nie istnieje następstwo dd dla wierzchołków 1 i 4, ponieważ, aby dojść z 1 do 4 musimy przejść przez wierzchołek 3, w którym następuje użycie x. Dla tej zmiennej mamy jedynie następstwo du dla wierzchołków 1 i 3 oraz ud dla wierzchołków 3 i 4.
1 x:=3 2 y:=4 3 if (x 0 jest nam potrzebny do dalszego wnioskowania warunek x ≥ 0). Reguła while (6) opisuje działanie pętli while: formuła ψ jest tu niezmiennikiem
pętli – jest prawdziwa zarówno przed, jak i po wykonaniu pętli. Warunek końcowy zawiera negację A, gdyż jest warunkiem zakończenia pętli. Pokażemy teraz na przykładzie, jak stosuje się logikę Hoare’a do dowodzenia poprawności programów. Rozważmy program z listingu 6.4 mający obliczać silnię4 z liczby naturalnej n. {x = n ∧ n > 0} 1 y:=1 2 while (x>0) do 3
y:=y*x
4 x:=x-1 5 end while {y = n!} Listing 6.4. Program obliczający silnię Program przyjmuje na wejściu zmienną x, a na wyjściu zwraca y, które ma być równe x!. Przed programem i po programie są wypisane warunki: początkowy oraz końcowy. Razem z programem (nazwijmy go P) tworzą formułę Hoare’a: {x = n ∧ n > 0} P {y = n!} Formuła ta mówi, że jeśli zmienna x jest równa n, gdzie n jest liczbą dodatnią, to po wykonaniu programu P zmienna y będzie wynosić n!, czyli dokładnie tyle, ile chcemy. Formuły takie, wplecione w kod, są nazywane niezmiennikami (asercjami), gdyż niezależnie od tego jak przebiega sterowanie programu, jeśli znajdzie się ono w miejscu asercji, to warunek przez nią wskazywany zawsze jest prawdziwy. Jeśli udowodnimy, że powyższa formuła Hoare’a zachodzi, to udowodnimy poprawność programu. Dowód można przeprowadzić przez wstawianie kolejnych asercji między linie kodu, wykorzystując aksjomaty i reguły (1)–(6) podane powyżej. Na listingu 6.5 w kod zostały wplecione asercje (dla większej czytelności podane na szarym tle), które stanowią dowód poprawności. A1 i A9 są asercjami stanowiącymi warunek początkowy i końcowy. Pokażemy, że jeśli zachodzi A1, to po wykonaniu P zajdzie A9. Asercja A2 jest prawdziwa wobec implikacji x = n ∧ n > 0 ⇒ x! ⋅ 1 = n! ∧ x ≥ 0 (stosujemy tu regułę
konsekwencji (5)). Aby uzyskać {A2} y := 1 {A3} używamy aksjomatu przypisania {A3 [1/y]} y := 1 {A3}, bo A3 [1/y] = A2. Postać A4 wynika z reguły while (6), a A5 uzyskujemy przez zastosowanie reguły konsekwencji (5). A6 i A7 dostajemy, używając aksjomatu przypisania {A6 [y ⋅ x/y]} y := y ⋅ x {A6} oraz {A7 [x – 1/x]} x := x – 1 {A7}. Postać A8 wynika z tego, że A3 jest niezmiennikiem pętli, więc po jej zakończeniu
niezmiennik
ten
musi
zachodzić
wraz
z
negacją
asercji
odpowiadającej strażnikowi pętli (czyli wyrażeniu x > 0 w instrukcji while). Negacja wynika stąd, że skoro pętla się skończyła, to jej warunek musiał przestać zachodzić. Z A8 wynika, że x = 0, skąd po podstawieniu do x! ⋅ y = n! bezpośrednio uzyskujemy A9. A1 {x = n ∧ n > 0} A2 ⇒ {x! ⋅ 1 = n! ∧ x ≥ 0} 1 y:=1 A3 2 A4 A5 3 A6 4
{x! ⋅ y = n! ∧ x ≥ 0} while (x>0) do {x! ⋅ y = n! ∧ x ≥ 0 ∧ x > 0} ⇒ {(x – 1)! ⋅ y ⋅ x = n! ∧ (x – 1) ≥ 0} y:=y*x {(x – 1)! ⋅ y = n! ∧ (x – 1) ≥ 0} x:=x-1
A7 {x! ⋅ y = n! ∧ x ≥ 0} 5 end while A8 {x! ⋅ y = n! ∧ x ≥ 0 ∧ ~ (x > 0)} A9 ⇒ {y = n! } Listing 6.5. Program obliczający silnię z wstawionymi asercjami Jak widać, przeprowadzenie dowodu jest dosyć żmudne i wymaga czasami pomysłu na dostarczenie odpowiedniego wzmocnienia lub osłabienia warunku (tak jak np. w A2), aby uzyskać ciąg formuł Hoare’a prowadzący od warunku początkowego do warunku końcowego całego programu P. Dlatego, choć istnieją narzędzia umożliwiające w niektórych przypadkach zautomatyzowanie procesu dowodzenia, metoda ta nie znalazła szerokiego zastosowania w praktyce.
Znacznie efektywniejsze jest przeprowadzanie przeglądów, zwłaszcza inspekcji formalnych.
6.2.8. Symboliczne wykonywanie kodu Symboliczne wykonywanie kodu jest metodą powstałą w latach 70. XX wieku dzięki pionierskim pracom Kinga [87] i Clarke [88]. Polega ona na analizie działania programu, w którym zmienne nie przyjmują konkretnych wartości, ale są traktowane algebraicznie. Symboliczne stany reprezentują zatem zbiory możliwych konkretnych stanów (czyli zbiory możliwych wartości zmiennych). Algorytm symbolicznego wykonywania kodu analizuje możliwe ścieżki wykonania programu i dla każdej z nich oblicza tzw. warunek ścieżki (PC, od ang. path condition), który jest wyrażeniem lub zbiorem wyrażeń algebraicznych opisujących związki między zmiennymi, które muszą zajść, aby program przeszedł daną ścieżką. Dzięki tej metodzie jest możliwe projektowanie przypadków testowych pokrywających określone elementy struktury programu, np. instrukcje lub rozgałęzienia (patrz podrozdz. 9.1–9.2). Aby znaleźć test, który podąży określoną ścieżką, należy znaleźć takie wartości zmiennych, które spełnią warunek tej ścieżki. Zwykle warunki te są dosyć skomplikowane i programy do symbolicznego wykonywania kodu mają specjalne narzędzia do rozwiązywania nieliniowych warunków przy użyciu zaawansowanych metod analizy matematycznej czy metod sztucznej inteligencji, takich jak optymalizacja rojem cząstek (ang. particle swarm optimization). Rozważmy program z listingu 6.6. Zamienia on wartości zmiennych x i y podanych na wejściu. Ponadto, w linii 7. znajduje się martwy kod, który nie powinien wykonać się ze względu na fałszywość predykatu w linii 6. Symboliczne wykonanie kodu pozwala znajdować tego typu problemy.
1 function Zamień(x, y) 2 if (x>y) then 3 x=x+y 4 y=x-y 5 x=x-y 6 if (x>y)
7 print “Martwy kod” 8 end Listing 6.6. Program zamieniający miejscami wartości dwóch zmiennych Na rysunku 6.7 przedstawiono symboliczne wykonanie kodu z listingu 6.6. Każdej linii kodu odpowiada prostokąt zawierający warunek ścieżki (czyli formułę logiczną, która musi być spełniona, aby program poszedł daną ścieżką) oraz symboliczna wersja instrukcji. Dla sygnatury funkcji mamy PC = true (bo początek funkcji zawsze będzie wykonany) oraz przypisujemy zmiennym x i y ich symboliczne wersje X, Y, które będą od tej pory traktowane algebraicznie. Pierwszy if zawsze się wykona, więc również ma PC = true. Symboliczna wersja warunku w tej instrukcji if to X > Y. Jeśli warunek nie zachodzi, to kończymy analizę tej ścieżki. W tym przypadku warunkiem PC dla ścieżki jest to, by X ≤ Y. Jeśli jednak warunek zachodzi, to PC = x > y i wykonane zostanie ciało pierwszej instrukcji if. Symboliczne wykonanie przypisania y:=x–y pozwala nam stwierdzić, że po jej wykonaniu Y := X, ponieważ wcześniej X przyjęło wartość X + Y. Mogliśmy więc podstawić tę sumę pod zmienną X w instrukcji Y := X – Y. Analogicznie, wykorzystując ten sam mechanizm algebraicznych przekształceń, symboliczne wykonanie instrukcji x:=x–y pozwala nam obliczyć, że X := Y. Zatem wykonanie if zamieniło wartości zmiennych x i y ze sobą. Warunek ścieżki dla wszystkich instrukcji if jest taki sam (bo instrukcje te nastąpią zawsze sekwencyjnie, jedna po drugiej). Dochodzimy do drugiego if. Warunek ścieżki to PC = X > Y, ale z wykonania symbolicznego wiemy, że X ma teraz wartość Y i na odwrót. Gdyby warunek drugiego if zachodził, to jednocześnie musiałoby być X > Y i Y > X, co jest sprzecznością. Zatem ta ścieżka nigdy nie będzie wykonana.
Rysunek 6.7. Wykonanie symboliczne kodu zamieniającego wartości zmiennych Obecnie istnieje wiele narzędzi wspomagających symboliczne wykonanie kodu. Z najważniejszych można wymienić KLEE [63] autorstwa naukowców ze Stanforda oraz Java PathFinder [89] dla Javy, stworzony w laboratoriach NASA. W momencie znalezienia defektu programy te oferują możliwość wygenerowania konkretnego przypadku testowego, który tę usterkę ujawni. Znajdowanie konkretnych wartości wejściowych dla takiego testu polega na analizie wszystkich warunków ścieżki leżących na ścieżce, którą chcemy wykonać. W przypadku naszego przykładowego programu może to być bardzo proste: jeśli chcemy, by program wykonał ciało pierwszego if, to musi zachodzić warunek tej ścieżki, czyli X > Y. Wystarczy więc wziąć takie x i y, aby x było większe od y. Jeśli jednak program byłby bardziej skomplikowany, to im „głębiej” sięga pożądana przez nas ścieżka, tym więcej warunków ścieżki musi być spełnionych, a więc tym trudniejsza nasza kontrola nad sterowaniem programem. Niektóre z warunków mogą być nieliniowe. Inne mogą być od siebie zależne (np. wykorzystywać te same
zmienne), co jeszcze bardziej komplikuje sprawę. Dlatego programy realizujące symboliczne wykonanie programu używają skomplikowanych algorytmów do rozwiązywania tego typu warunków w celu odnalezienia konkretnych wartości parametrów wejściowych. Symboliczne wykonanie programu może być użyte pod kątem analizy odporności na zagrożenia (ang. vulnerability). Przykładem takiej aplikacji jest program Kudzu [90] dla języka JavaScript, opracowany ze względu na wzrastającą popularność technologii AJAX.
6.2.9. Analiza statyczna strony internetowej Strony www można testować na wiele sposobów. W szczególności, ponieważ na stronę można patrzeć jak na program, którego kodem źródłowym są pliki html, css, JavScript, php, AJAX itd., strona webowa może podlegać wszystkim technikom i typom testowania, które stosuje się do kodu źródłowego (np. [59]). W kontekście analizy statycznej testowanie strony internetowej można rozumieć jako analizę struktury (architektury). Analiza ta może polegać na sprawdzeniu, czy struktura strony jest zbalansowana i czy do każdego jej elementu można dostać się za pomocą małej liczby operacji (kliknięć). Taka forma analizy może być uznana za statyczne testowanie użyteczności. Użytkownicy stron www opuszczają je przed osiągnięciem zamierzonego celu m.in. z następujących powodów: strony są trudne w użyciu; strony odpowiadają zbyt wolno; strony nie są odpowiednio zabezpieczone; występują błędy, np. „strony nie znaleziono”. Identyfikacja drugiego i trzeciego powodu jest domeną testowania niefunkcjonalnego. Do wykrycia błędów takich jak w powodzie czwartym można użyć zautomatyzowanych testów regresji, które sprawdzają poprawność linków w obrębie całej strony. Testowanie statyczne skupia się na powodzie pierwszym. Rozważmy przykład strony internetowej pokazanej na rysunku 6.8.
Rysunek 6.8. Struktura strony www sklepu internetowego Analiza statyczna może wykazać, że od momentu wejścia na stronę potwierdzenie zamówienia uzyskać można w co najmniej czterech krokach. Być może dałoby się zredukować ich liczbę np. przez umieszczenie wyszukiwarki produktów bezpośrednio na stronie głównej.
Rysunek 6.9. Przykład niezrównoważonej i zrównoważonej struktury strony internetowej
Analiza może też ujawnić wadliwość struktury strony. Na rysunku 6.9 przedstawiono dwie różne hierarchie stron tej samej witryny internetowej. Architektura pokazana po lewej jest niezbilansowana – jedna gałąź jest bardzo głęboka, natomiast strony 3, 4, 5 są ulokowane tuż pod stroną główną. W rezultacie, aby dostać się do stron 12, 13, 14, potrzeba aż pięciu kliknięć. Architektura po prawej stronie jest lepsza – dostęp do każdej strony można uzyskać w co najwyżej dwóch krokach.
6.2.10. Grafy wywołań Większość programów używa innych, zewnętrznych wobec siebie funkcji przez ich wywoływanie. Możemy zdefiniować pewną odmianę metryki złożoności cyklomatycznej, która pozwoli nam ocenić stopień skomplikowania struktury wywołań. Tak jak zwykła złożoność cyklomatyczna określa liczbę testów niezbędnych do pokrycia niezależnych ścieżek w programie, tak jej zmodyfikowana wersja mówi o liczbie testów potrzebnych do przeprowadzenia testów integracji. Model ten jest oparty na wywołaniu jednych modułów przez inne. Jego reprezentacją jest graf, stąd nazwa metody – grafy wywołań. Metryka ta, tak jak złożoność cyklomatyczna, została wprowadzona przez McCabe’a. Metoda ją wykorzystująca to tzw. podejście predykatów projektowych (ang. design predicate approach). Na rysunku 6.10 przedstawiono podstawowe typy wywołań. Jeśli moduł M0 zawsze wywołuje M1 (lub zawsze wywołuje wszystkie moduły M1, M2, … Mn), to mamy do czynienia z wywołaniem bezwarunkowym. Złożoność tego wywołania wynosi 0. Jeśli M0 może wywołać M1, ale nie musi, to mamy do czynienia z wywołaniem warunkowym o złożoności 1. Gdy M0 może wywołać jeden i tylko jeden spośród n modułów, jest to wywołanie wzajemnie wyłączające się o złożoności n – 1. Jeśli istnieje możliwość nie wywołania żadnego z nich, to nie możemy mówić o wzajemnym wyłączaniu i każde z wywołań należy traktować oddzielnie. Gdy M0 wywołuje M1 wielokrotnie podczas swojego działania (ale co najmniej raz), mówimy o wywołaniu iteracyjnym. Ma ono złożoność 1. Jeśli jednak w trakcie iteracji M1 może, ale nie musi być wywołany, mamy do czynienia z warunkowym wywołaniem iteracyjnym o złożoności 2. Po stworzeniu grafu wywołań należy policzyć sumę złożoności wnoszonych przez każdy rodzaj wywołania. Stanowi ona miarę wzrostu złożoności systemu z powodu danego modułu.
Rysunek 6.10. Podstawowe typy wywołań między modułami graf wywołań (ang. call graph) – abstrakcyjna reprezentacja przedstawiająca odwołania między procedurami i funkcjami w programie
#include #include #include int int int int
grep(char*, FILE*, char*); match(char*, char*); matchhere(char*, char*); matchstar(int, char*, char*);
int main(int argc, char *argv[]) { int i, nmatch; FILE *f; setprogname(“grep”); if (argc < 2) {printf(“usage: grep regexp [file ...]”); exit(2)} nmatch = 0; if (argc == 2) { if (grep(argv[1], stdin, NULL)) nmatch++; } else { for (i = 2; i < argc; i++) { f = fopen(argv[i], “r”); if (f == NULL) { printf(“can’t open %s:”, argv[i]); continue; } if (grep(argv[1], f, argc>3 ? argv[i] : NULL) > 0) nmatch++; fclose(f); } } return nmatch == 0; } int grep(char *regexp, FILE *f, char *name) { int n, nmatch; char buf[BUFSIZ]; nmatch = 0; while (fgets(buf, sizeof buf, f) != NULL) { n = strlen(buf); if (n > 0 && buf[n-1] == ‘\n’) buf[n-1] = ‘\0’; if (match(regexp, buf)) { nmatch++; if (name != NULL) printf(“%s:”, name); printf(“%s\n”, buf); }
} return nmatch; } int matchhere(char *regexp, char *text) { if (regexp[0] == ‘\0’) return 1; if (regexp[1] == ‘*’) return matchstar(regexp[0], regexp+2, text); if (regexp[0] == ‘$’ && regexp[1] == ‘\0’) return *text == ‘\0’; if (*text!=’\0’ && (regexp[0]==’.’ || regexp[0]==*text)) return matchhere(regexp+1, text+1); return 0; } int match(char *regexp, char *text) { if (regexp[0] == ‘^’) return matchhere(regexp+1, text); do { if (matchhere(regexp, text)) return 1; } while (*text++ != ‘\0’); return 0; } int matchstar(int c, char *regexp, char *text) { do { if (matchhere(regexp, text)) return 1; } while (*text != ‘\0’ && (*text++ == c || c == ‘.’)); return 0; } Listing 6.7. Kod źródłowy programu grep.c
Rozważmy pokazany na listingu 6.7 napisany w C program grep.c, pochodzący z [91] i nieco zmodyfikowany na nasze potrzeby. Policzmy złożoność grafu wywołań dla funkcji main. Po jej uruchomieniu zawsze nastąpi wywołanie setprogname (jednokrotne). Jest to wywołanie bezwarunkowe i nic nie wnosi do złożoności. Gdy warunek argc < 2 będzie spełniony wykona się jednokrotnie printf, ale gdy warunek ten nie będzie spełniony, możemy wejść do pętli, w której warunkowo wywoływane jest printf. Mamy więc do czynienia z warunkowym wywołaniem iteracyjnym, które wnosi 2 do złożoności. To samo dotyczy funkcji fopen, fclose i grep. Każda z nich albo się nie wywoła (jeśli argc < 2), albo może wywołać się wielokrotnie w pętli for. W funkcji grep warunek pętli wywołujący fgets sprawdzony będzie zawsze co najmniej raz, zatem jest to wywołanie iteracyjne o złożoności 1. Jeśli ten warunek już przy pierwszym sprawdzeniu będzie fałszywy, to grep nie wywoła match, które jest w ciele while. W przeciwnym wypadku match może być wywołane wielokrotnie. Jest to warunkowe wywołanie inkrementacyjne wnoszące 2 do całkowitej złożoności. Funkcja match zawsze wywoła przynajmniej raz matchhere: albo zostanie ona wywołana dokładnie raz w funkcji return, albo wielokrotnie (lecz co najmniej raz) w pętli do. To wywołanie inkrementacyjne wnosi 1 do złożoności. Funkcja matchhere wywoła co najwyżej raz jedną z dwóch funkcji: siebie albo matchstar. Każda daje wywołanie warunkowe i wnosi 1 do złożoności. Zauważmy, że nie jest to wywołanie rozłączne, bo może się zdarzyć, że żadna z tych funkcji nie będzie wywołana, np. wtedy, gdy spełniony będzie warunek regexp[0] == ′\0′. Zauważmy, że mamy tu do czynienia z wywołaniem rekurencyjnym – matchhere może wywołać sam siebie. Funkcja matchstar wywoła co najmniej raz funkcję matchhere, więc jest to wywołanie iteracyjne. Funkcje matchstar i matchhere w grafie wywołań tworzą cykl, co oznacza, że może następować rekurencyjne wywołanie jednej z tych funkcji przez drugą i na odwrót.
Rysunek 6.11. Graf wywołań dla programu grep.c Podliczając całkowitą złożoność grafu wywołań (CGC – ang. call graph complexity) dla programu grep.c, otrzymujemy: CGC = (0 + 2 + 2 + 2 + 2) + (2 + 2 + 1) + 1 + (1 + 1) + 1 = 17. Liczba ta oznacza, że istnieje 17 różnych ścieżek, reprezentujących 17 klas wywołań różniących się liczbą i rodzajem wywoływanych funkcji, które powinniśmy przetestować. Podczas stosowania metody grafów wywołań należy pamiętać o kilku ważnych rzeczach: metoda nie uwzględnia wewnętrznej, funkcjonalnej złożoności wywoływanych modułów, skupia się wyłącznie na stopniu skomplikowania wywoływań modułów; liczby przy typach wywołań nie określają samej złożoności, ale jej wzrost; np. wywołanie warunkowe należy rozumieć nie jako element
o złożoności 1, ale jako element, który podwyższa złożoność całego systemu o 1; grafy wywołań nie są grafami przepływu sterowania i nie można ich interpretować w ten sam sposób; w grafach wywołań po wywołaniu jakiejś funkcji wracamy z powrotem do wierzchołka reprezentującego metodę wywołującą.
1 W języku C (a także innych, takich jak C++ czy Java) jest dopuszczone stosowanie uproszczonego zapisu zwiększania wartości zmiennej o 1. Zamiast pisać i=i+1 można użyć operatora inkrementacji „++” i zapisać tę instrukcję równoważnie jako i++. 2 l-wartość to wartość zmiennej, która – po wykonaniu instrukcji – może być później ponownie wykorzystana. r-wartość to wartość wykorzystywana tylko w danej instrukcji. Na przykład w instrukcji a:=b+3 podczas wyliczania wartości zmiennej a do pamięci tymczasowo zapisywana jest wartość b + 3. Po wykonaniu przypisania zmienna a będzie miała nową wartość, która została zapisana w pamięci, natomiast wartość b + 3 zostanie zapomniana. Dlatego b + 3 jest r-wartością. Natomiast samo b jest l-wartością, bo cały czas zmienna ta przechowywana jest w pamięci. 3 Logika pierwszego rzędu to znana dobrze ze szkoły średniej logika wykorzystująca kwantyfikatory ∀ („dla każdego”) i ∃ („istnieje”), wraz z operatorami negacji ∼, koniunkcji ∧, alternatywy ∨, implikacji ⇒ i równoważności ⇔. 4 Silnia liczby naturalnej n (oznaczana jako n!) to iloczyn wszystkich liczb naturalnych od 1 do n: n! = 1 ⋅ 2 … n.
7. Analiza dynamiczna
Analiza dynamiczna jest przeciwieństwem opisanej wcześniej analizy statycznej w tym sensie, że jej przeprowadzenie odbywa się na działającym kodzie. Metoda ta pozwala na wykrywanie anomalii takich jak wycieki pamięci czy wiszące/dzikie wskaźniki. Służy również do przeprowadzania różnego rodzaju analiz wydajności, statystycznych analiz działania programu, czy też tworzenia grafu wywołań dla działającego programu. analiza dynamiczna (ang. dynamic analysis) – proces oceny systemu lub modułu na podstawie jego zachowania w działaniu, np. zarządzanie pamięcią czy wykorzystaniem procesora Metoda ta sprawdza się bardzo dobrze zwłaszcza w przypadku wymienionych powyżej anomalii, czyli wycieków pamięci oraz dzikich wskaźników. Jeśli na skutek usterki w programie następuje wyciek pamięci lub powstaje dziki wskaźnik, zwykle nie objawia się to od razu żadnym zewnętrznym symptomem, natomiast konsekwencje kumulacji takich błędów w czasie mogą powodować poważne problemy. Analiza dynamiczna pozwala szybko wykrywać istnienie tych anomalii oraz pomaga w znajdowaniu miejsc ich wystąpienia. narzędzie do analizy dynamicznej (ang. dynamic analysis tool) – narzędzie rejestrujące informacje o stanie wykonywanego programu; używane zwykle do znajdowania nieprzypisanych wskaźników, sprawdzania alokacji, użycia i dealokacji pamięci oraz do oznaczania jej wycieków Do
przeprowadzenia
analizy
dynamicznej
zwykle
jest
niezbędna
instrumentacja kodu, czyli dodanie do kodu programu instrukcji służących do zbierania informacji na temat działania programu. Instrumentacja jest
dokonywana albo przez specjalny kompilator, który dodaje odpowiednie linie do kodu programu przed jego kompilacją, albo jest przeprowadzana przez zewnętrzny program działający między oryginalnie wykonywanym kodem a systemem operacyjnym. Oto proste zastosowanie
instrumentacji.
Rozważmy
program
Collatz
przedstawiony na listingu 7.1. Program ten symuluje wykonywanie pewnych operacji na liczbach naturalnych i jest związany ze słynną hipotezą Collatza. Hipoteza ta mówi, że jeśli weźmiemy dowolną liczbę naturalną, a następnie będziemy ją przekształcać według dwóch prostych reguł: jeśli liczba jest parzysta, podziel ją przez 2; jeśli liczba jest nieparzysta, pomnóż ją przez 3 i dodaj 1, to po skończonej liczbie takich operacji zawsze dojdziemy do liczby 1. Na przykład, dla liczby 13 otrzymamy następujący ciąg liczb: 13 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1. Program Collatz realizuje właśnie taki ciąg operacji dla zadanej liczby naturalnej na wejściu.
1 function Collatz(n) 2 while (n>1) do 3 if (n mod 2 == 0) then 4 5 6
n:=n/2 else n:=3*n+1
7 print n 8 end function Listing 7.1. Pseudokod programu Collatz Przypuśćmy, że z jakiegoś powodu interesuje nas, ile razy podczas działania tego programu wykonały się poszczególne instrukcje. W takiej sytuacji możemy dokonać instrumentacji kodu takiej, jak ta z listingu 7.2. Linie wyjściowego programu zachowały oryginalną numerację. Dodane linie nie są numerowane, aby można było lepiej odróżnić kod oryginalny od instrumentacji.
instrumentacja
(ang.
instrumentation)
–
dodanie
specjalnego
kodu
do
oryginalnego programu w celu zbierania informacji o jego zachowaniu podczas wykonania, np. w celu pomiaru pokrycia kodu instrumentator, instrumentator programowy (ang. instrumenter, program instrumenter) – narzędzie programowe wykonujące instrumentację kodu Wprowadziliśmy nową tablicę liczb całkowitych op, mającą tyle elementów, ile linii programu. Po i-tej linii oryginalnego kodu wprowadzamy instrukcję op[i]:=op[i]+1. Dzięki temu wartość op[i] zwiększa się za każdym razem, gdy program wykonuje i-tą
1 function Collatz(n) op[1..7]:=0 op[1]:=op[1]+1 2 while (n>1) do op[2]:=op[2]+1 3 4 5 6
if (n mod 2 == 0) then op[3]:=op[3]+1 n:=n/2 op[4]:=op[4]+1 else op[5]:=op[5]+1 n:=3*n+1
op[6]:=op[6]+1 7 print n op[7]:=op[7]+1 print op[1..7] 8 end function Listing 7.2. Zinstrumentowany program Collatz linię wyjściowego programu. Na końcu możemy wypisać zawartość tablicy op, aby np. zobaczyć, które linie wykonywały się częściej niż inne, a które na przykład nie wykonały się w ogóle.
7.1. Wykrywanie wycieków pamięci Wycieki pamięci występują w programach pisanych w językach umożliwiających dynamiczną alokację pamięci bez tzw. odśmiecacza pamięci (ang. garbage collector). Mechanizm ten istnieje m.in. w Javie, C#, Pythonie czy Rubym, ale nie ma go np. w standardowym C czy C++. Wycieki pamięci objawiają się zwykle dopiero wtedy, gdy nastąpi jej przepełnienie lub nadpisanie pamięci w używanym aktualnie miejscu stosu, co prowadzi zwykle do globalnej awarii systemu. Często symptomem istnienia wycieku pamięci jest coraz wolniejszy czas działania programu. Problemy z pamięcią zwykle są spowodowane pomyłką programisty, ale zdarza się – choć rzadko – że mogą być wynikiem błędnie działającego kompilatora, który podczas kompilacji generuje kod z wyciekami pamięci. wyciek pamięci
(ang.
memory
leak)
–
błąd
mechanizmu
programu
wykonującego dynamiczną alokacje pamięci polegający na niezwalnianiu pamięci po zaprzestaniu jej używania; może on prowadzić do awarii programu spowodowanej brakiem pamięci Wycieki pamięci są najefektywniej wykrywane za pomocą narzędzi dedykowanych do tego celu. Analiza pod kątem problemów z pamięcią zwykle polega na długotrwałym i intensywnym używaniu programu, gdyż nie są to błędy, które pojawiają się błyskawicznie, od razu po uruchomieniu programu. Szansa, że błąd taki zostanie wykryty podczas krótkiej sesji testowej, jest niewielka. Narzędzia monitorujące pamięć analizują ilość pamięci alokowanej i zwalnianej. Gdy dynamicznie zaalokowany blok pamięci „znika” z systemu bez uprzedniego formalnego zwolnienia tej pamięci, narzędzie oznacza miejsce, w którym potencjalnie mogło dojść do wycieku pamięci. W takim przypadku są możliwe dwa scenariusze: albo incydent ten jest logowany i wykonanie programu trwa nadal, albo wykonanie jest natychmiast przerywane. Decyzja o wyborze dalszej drogi zależy oczywiście od testera.
7.2. Wykrywanie dzikich i wiszących wskaźników
Wskaźnik zmiennej x to zmienna wskazująca na adres pamięci, pod którą zmienna x się znajduje. W językach programowania dopuszczających swobodną alokację i dealokację pamięci możliwe jest wystąpienie błędów w postaci wiszących oraz dzikich wskaźników, czyli wskaźników, które nie wskazują na to, na co oryginalnie powinny. Wiszący wskaźnik powstaje, gdy obiekt, na który wskaźnik wskazuje, zostaje usunięty (np. przy użyciu funkcji free w języku C) lub pamięć przeznaczona na ten obiekt jest zwalniana, bez jednoczesnej modyfikacji wartości wskaźnika. Powstanie wiszącego wskaźnika może być spowodowane wyjściem z bloku, w którym jest widoczna wskazywana zmienna. Dziki wskaźnik powstaje, gdy w momencie jego utworzenia nie następuje poprawna inicjalizacja jego wartości. Dobrą praktyką jest inicjalizacja każdego wskaźnika od razu przy jego definiowaniu. wskaźnik (ang. pointer) – zmienna, której wartość określa lokalizację (adres) innej zmiennej [7] wiszący wskaźnik (ang. dangling pointer) – wskaźnik, który nie wskazuje na poprawne obiekty właściwego typu ani nie jest wskaźnikiem pustym (null) w językach, które to umożliwiają dziki wskaźnik (ang. wild pointer) – wskaźnik, który nie został zainicjalizowany przy tworzeniu, przez co jego zawartość jest nieokreślona Na listingu 7.3 przedstawiono fragment programu napisanego w C. W linii 2. następuje deklaracja zmiennej o nazwie w, która ma być wskaźnikiem na zmienną typu int. Deklaracja nie nastąpiła razem z inicjalizacją (np. int *w = NULL), więc w jest dzikim wskaźnikiem. Teoretycznie może wskazywać na cokolwiek. W linii 3. przypisujemy mu wartość null, zatem staje się poprawnym wskaźnikiem, który nie wskazuje na razie na nic. Linie 4.–7. są blokiem, w którego zakresie znajduje się zmienna x. Jest ona niewidoczna poza tym blokiem. W linii 6. każemy wskaźnikowi w wskazywać na adres zmiennej x. W linii 8. chcemy wykorzystać ten wskaźnik, ale linia ta jest poza blokiem, w którym była widoczna zmienna x, dlatego wskaźnik może teraz wskazywać na coś innego. Jeśli między wyjściem z bloku a wykonaniem instrukcji 8. stan pamięci się nie zmienił, to wszystko może
działać poprawnie, ale jeśli ten fragment pamięci został zajęty przez inny proces, to wywołanie linii 8. może spowodować nieprzewidziane zachowanie programu.
1 { 2 int* w;
// w jest dzikim wskaźnikiem
3 4
w = NULL; // teraz w jest poprawnym wskaźnikiem {
5 6 7
int x; w = &x; // wskaźnik w wskazuje na adres zmiennej x } // koniec bloku1 – tracimy widoczność x; w staje się
8
wiszącym wskaźnikiem OperacjaNaWskaźniku(w); // teraz może być problem!
9 } Listing 7.3. Kod z wiszącymi i dzikimi wskaźnikami Konsekwencje stosowania dzikich i wiszących wskaźników mogą być następujące: program może działać zgodnie z oczekiwaniem – tak może być, jeśli zwolniona pamięć, na którą nadal wskazuje wskaźnik, nie została zajęta przez inny obiekt; stan programu jest błędny; program może się zawiesić – jeśli np. użycie wskaźnika spowodowało nadpisanie obszaru pamięci zajmowanego przez system operacyjny; program działa niepoprawnie – bo nie może uzyskać dostępu do swoich obiektów lub używa nieświadomie innych wartości dla tych obiektów; program może spowodować nieprawidłowe działanie innego programu – jeśli użycie wiszącego wskaźnika spowoduje nadpisanie danych, z których korzysta inna aplikacja.
7.3. Błędy API API (ang. Application Programming Interface), czyli interfejs programowy aplikacji, to zestaw funkcji pozwalających na uzyskiwanie zdalnej funkcjonalności aplikacji. Przykładem API może być zestaw funkcji dostarczanych przez serwisy takie jak
Google, Twitter, Facebook, które umożliwiają kontaktowanie się z określonym serwisem i wykonywanie pewnych operacji lub żądanie określonych usług. Błędy mogą pochodzić z niedostatecznej kontroli wywoływanych funkcji API. Jeśli funkcja API zwróci z jakiegoś powodu kod błędu oznaczający, że coś poszło nie tak, a programista wykorzystujący funkcje API nie uwzględni tego w swoim kodzie, może to doprowadzić do nieprzewidzianego zachowania się programu. Narzędzia analizy dynamicznej mogą wykrywać tego typu problemy przez przechwytywanie wywołań API oraz zwracanych wartości i sprawdzać, czy argumenty wywołania są zgodne z oczekiwaną postacią listy parametrów lub czy funkcja nie zwróciła wartości oznaczającej błąd. Użycie takich narzędzi oczywiście znacznie spowalnia wykonanie testowanego programu.
7.4. Analiza wydajności (profiling) Analiza wydajności (czyli tzw. profiling) pozwala określić, ile czasu program przeznacza na wykonywane przez niego operacje, które fragmenty kodu wykonywane są częściej, a które rzadziej oraz jakie funkcje zostały przez niego wywołane. Analiza ta umożliwia odnalezienie fragmentów kodu, które działają wolniej niż się spodziewaliśmy lub np. wywołują podejrzanie dużo razy pewne funkcje. Przeprowadza się ją przy użyciu odpowiednich narzędzi, na programach mających duży kod źródłowy, niemożliwy do całościowej oceny przez przegląd czy inspekcję. Eksperymentalnie pokazano, że dla czasu i obszaru wykonywanego programu zachodzi zasada Pareto w wersji 90/10, tzn. że około 90% swojego czasu program spędza w 10% swojego kodu. To oznacza, że w większości przypadków zasadnicze przyspieszenie działania programu można uzyskać, poprawiając niewielkie fragmenty kodu, a lokalizację tych fragmentów umożliwia właśnie analiza wydajności. Zwykle analiza wydajności składa się z trzech kroków: kompilacja testowanego programu z umożliwieniem profilowania; wykonanie programu skutkujące generacją pliku analizy wykonania; uruchomienie narzędzia analizującego plik analizy wykonania. Załóżmy, że chcemy dokonać analizy wydajności programu mojprogram.c. Na listingu 7.4 pokazano, jak zrobić to przy użyciu programu gprof.
cc –o mojprogram mojprogram.c –g –pg ./mojprogram gprof gmon.out > analiza.txt Listing 7.4. Przeprowadzenie analizy wydajności Pierwsza linia to użycie kompilatora cc w celu skompilowania pliku mojprogram.c. Parametr –pg informuje kompilator o włączeniu opcji profilowania. Druga linia to uruchomienie skompilowanego programu. Program może działać nieco wolniej niż w przypadku nie wykorzystującym profilowania, gdyż w trakcie działania są zbierane informacje o wykonaniu. Informacje te są zapisywane w domyślnym pliku gmon.out. Trzecia linia to wywołanie programu gprof na pliku gmon.out, przy czym wyjście jest przekazane do pliku tekstowego analiza.txt. Wyjście programu gprof składa się z dwóch części. Pierwszą jest tzw. płaski profil (ang. flat profile) pokazujący, ile czasu nasz program spędzał na wywoływaniu poszczególnych funkcji. Druga to znany nam już z punktu 6.2.10 graf wywołań.
Flat profile: Each sample counts as 0.01 seconds. % cumulative self self time seconds seconds calls ms/call 33.34 16.67 16.67 16.67 16.67 0.00 0.00 0.00 0.00 0.00
0.02 0.03 0.04 0.05 0.06 0.06 0.06 0.06 0.06 0.06
0.02 0.01 0.01 0.01 0.01 0.00 0.00 0.00 0.00 0.00
total ms/call
7208 244 8 7
0.00 0.04 1.25 1.43
0.00 0.12 1.25 1.43
236 192 47 45 1
0.00 0.00 0.00 0.00 0.00
0.00 0.00 0.00 0.00 50.00
name open offtime memccpy write mcount tzset tolower strlen strchr main
0.00 0.00
0.06 0.06
0.00 0.00
1 1
0.00 0.00
0.00 10.11
memcpy print
0.00
0.06
0.00
1
0.00 ...
0.06
0.00
1
0.00
0.00
profil
0.00
50.00
report
Listing 7.5. Przykładowy płaski profil programu generowany przez gprof Przykładowy płaski profil wygenerowany przez gprof [92] jest pokazany na listingu 7.5. Funkcje są posortowane według czasu spędzonego na ich wykonaniu. Z raportu wynika, że najwięcej czasu zajmowało wykonywanie otwieranie plików (funkcja open). Funkcje mcount i profil są funkcjami używanymi przez mechanizm profilujący i dlatego są obecne w każdym raporcie. Czasy ich wykonania mogą być użyte do obliczenia narzutu, jaki powstał w wyniku używania narzędzia profilującego. Poszczególne kolumny raportu przedstawiają: % time – procent czasu spędzonego w danej funkcji (powinny sumować się do 100%); cumulative seconds – całkowita liczba sekund spędzonych w danej funkcji oraz wszystkich wymienionych powyżej; self seconds – liczba sekund spędzonych w danej funkcji; calls – liczba wywołań danej funkcji; jeśli nie było wywołań lub nie da się określić ich liczby (np. gdy funkcja ta nie była skompilowana z opcją możliwości profilowania), to pole jest puste; self ms/call – średni czas (w milisekundach) spędzony w funkcji na jedno wywołanie; total ms/call – jw., ale dla danej funkcji oraz wszystkich funkcji wywoływanych przez nią (o ile były profilowane – w przeciwnym razie pole jest puste); name – nazwa funkcji. Jeśli chcemy obliczyć efektywny czas spędzony np. na wykonaniu funkcji open, to musimy odjąć od łącznego czasu czas działania procedur mcount i profil. Okazuje się, że efektywny czas dla funkcji open wynosi nie 33.34, ale 40%: %time(open) = 0.02/(0.06 – 0.01) ⋅ 100% = 0.02/0.05 ⋅ 100% = 40%.
granularity: each sample hit covers 2 byte(s) for 20.00% of 0.05 seconds index % time
self
children
[1]
0.00 0.00
0.05 0.05
100.0
called
1/1
name
start [1] main [2]
0.00 0.00 1/2 on_exit [28] 0.00 0.00 1/1 exit [59] ----------------------------------------------0.00 0.05 1/1 start [1] [2] 100.0 0.00 0.05 1 main [2] 0.00 0.05 1/1 report [3] ----------------------------------------------0.00 0.05 1/1 [3]
100.0
0.00 0.00 0.00
0.05 0.03 0.01
1 8/8 1/1
0.00 0.00
0.01 0.00
9/9 12/34
0.00
0.00
8/8
0.00 0.00 1/1 0.00 0.00 8/8 0.00 0.00 8/16 ----------------------------------------------[4] 59.8 0.01 0.02 8+472 0.01
0.02
244+260
0.00
0.00
236+1
main [2]
report [3] timelocal [6] print [9]
-----------------------------------------------
fgets [12] strncmp [40] lookup [20] fopen [21] chewtime [24] skipspace [44] [4] offtime [7] tzset [26]
Listing 7.6. Graf wywołań wygenerowany przez gprof Na listingu 7.6 jest pokazany graf wywołań. Jest on podzielony na sekcje oddzielone kreskami. Każda sekcja dotyczy jednej funkcji. Funkcje są opisywane numerem (w kwadratowych nawiasach) oraz nazwą. W każdej sekcji linia z numerem w kolumnie „index” dotyczy rozważanej funkcji. Linie nad nią dotyczą funkcji, które ją wywołują, linie pod nią – funkcje przez nią wywoływane. Liczby w postaci x/y dotyczą całkowitej liczby wywołań oraz liczby wywołań nierekursywnych. Cykle są traktowane jak pojedyncze funkcje, aby pojęcie czasu spędzonego w wywoływanych funkcjach miało sens. Jeśli bowiem funkcja A wywołuje funkcję B, a B – funkcję A, to na czas spędzony w A składa się czas spędzony w B, ale ten z kolei zawiera… czas spędzony w A. Dokładny opis struktury obu opisanych raportów można znaleźć w [92]. Pojedyncze wykonanie profilowania może być obarczone dużym błędem losowym. Aby zwiększyć dokładność pomiaru można albo przeprowadzać analizę przez dłuższy czas, albo – jeśli nie jest to możliwe – przeprowadzić kilka analiz. Programy takie jak gprof umożliwiają łączenie ze sobą raportów z plików gmon.out.
1 W języku C klamry { } oznaczają tzw. bloki kodu. Zakres widoczności zmiennej ogranicza się tylko do bloku, w którym została zdefiniowana.
8. Techniki oparte na specyfikacji (czarnoskrzynkowe)
Znajomość technik projektowania testów opartych na specyfikacji jest kluczową umiejętnością analityka testów. Testowanie oparte na specyfikacji traktuje program jak czarną skrzynkę o nieznanej strukturze wewnętrznej. Stąd inna nazwa tej techniki: testowanie czarnoskrzynkowe. Podstawą tworzenia testów w tej technice są dokumenty pierwotne wobec kodu, takie jak wymagania, dokumentacja techniczna, projekt architektury, dokumentacja użytkownika czy przypadki użycia. Zaletą technik opartych na specyfikacji jest możliwość wczesnego projektowania testów, jeszcze przed rozpoczęciem kodowania. Testy można tak naprawdę tworzyć już po zatwierdzeniu dokumentu wymagań. Podczas tworzenia przypadków testowych tester nie ma wglądu do kodu programu, dlatego nie musi mieć doświadczenia w programowaniu. W rozdziale tym dokonamy przeglądu wszystkich najważniejszych technik z tej rodziny. Rozpoczniemy od omówienia metod najbardziej klasycznych: podziału na klasy równoważności, analizy wartości brzegowych, tablic decyzyjnych, grafów przyczynowo-skutkowych oraz maszyny skończenie stanowej (podrozdz. 8.1–8.5). Następnie w podrozdziale 8.6 opiszemy metodę Category-Partition. Jest ona jedną z najstarszych technik testowania opartych na specyfikacji. Drzewa klasyfikacji oraz tzw. techniki kombinacyjne, omówione w podrozdziałach 8.7 i 8.8, można uznać za odmiany tej metody. Podrozdział 8.9 jest poświęcony zagadnieniu testowania dziedziny, czyli rozszerzeniu metody podziału na klasy równoważności dla przypadków wielowymiarowych. W podrozdziałach 8.10, 8.11 i 8.12 opiszemy metody oparte na
perspektywie użytkownika, czyli testowanie na podstawie przypadków użycia, scenariuszy oraz tzw. historyjek użytkownika, stosowanych głównie w projektach opartych na metodykach zwinnych. Podrozdział 8.13 jest poświęcony różnym odmianom testowania losowego, podrozdział 8.14 dotyczy metod opartych na składni, a 8.15 – testowania opartego na cyklu życia danych. Wreszcie, w podrozdziale 8.16 omówimy praktyczne zagadnienia dotyczące łączenia różnych technik czarnoskrzynkowych ze sobą. Dla każdej z omówionych metod opiszemy w szczególności: wymagania testowe dla danej metody – czyli zbiór cech, atrybutów, funkcji lub elementów, na podstawie których będą konstruowane przypadki testowe; elementy pokrycia – czyli zbiór cech, atrybutów, funkcji lub elementów, które należy pokryć testami; kryterium pokrycia – czyli warunek, który musi być spełniony, aby uznać, że w pełni wykorzystano dany model do tworzenia przypadków testowych; stopień pokrycia – czyli wartość z przedziału 0–100% mówiącą w jakim stopniu zostało spełnione kryterium pokrycia dla zadanego zestawu testów; zwykle jest to liczba wymagań testowych (lub elementów pokrycia) pokrytych testami podzielona przez liczbę wszystkich wymagań testowych (lub elementów pokrycia). Należy pamiętać, że dla wielu metod osiągnięcie 100% pokrycia często nie jest możliwe. W takich przypadkach, jeśli kryteria wyjścia uwzględniają stopień pokrycia, należy użyć rozsądnej wartości, która z jednej strony będzie możliwie bliska 100%, ale z drugiej – będzie z dużym prawdopodobieństwem osiągalna. czarnoskrzynkowa technika (projektowania przypadków testowych), projektowanie przypadków testowych na podstawie specyfikacji, technika oparta na specyfikacji (ang. black-box (test design) technique, specification-based test design technique, specification-based technique) – procedura wyprowadzająca i/lub wybierająca przypadki testowe na podstawie analizy specyfikacji (funkcjonalnej lub niefunkcjonalnej) modułu lub systemu bez odniesienia do jego wewnętrznej struktury
Z pojęciem kryterium pokrycia wiąże się inne ważne pojęcie: subsumpcji. Powiemy, że kryterium K 1 subsumuje kryterium K 2 wtedy i tylko wtedy, gdy każda suita testowa, spełniająca K 1 spełnia również K 2.
8.1. Podział na klasy równoważności 8.1.1. Opis metody Metoda podziału na klasy równoważności jest jedną z najstarszych, najprostszych koncepcyjnie i najpowszechniej stosowanych technik czarnoskrzynkowych. Jest ona związana z modelowaniem dziedziny testowanego programu, przy czym modelowaniu mogą podlegać zarówno wejścia, jak i wyjścia programu. dziedzina (ang. domain) – zbiór wszystkich możliwych (dozwolonych) wartości wejścia lub wyjścia z programu dziedzina danych wejściowych (ang. input domain) – zbiór wszystkich możliwych (dozwolonych) wartości wejściowych programu dziedzina danych wyjściowych (ang. output domain) – zbiór wszystkich możliwych (dozwolonych) wartości wyjściowych programu Technika polega na podziale dziedziny przedmiotu testu lub elementu testowego na klasy równoważności, które stają się elementami pokrycia. Podziału dokonuje się za pomocą tzw. charakterystyk, czyli opisów właściwości elementów dziedziny. Charakterystyka powinna zawsze wynikać ze specyfikacji wymagań bądź dokumentów opisujących funkcjonalność programu. Na przykład, jeśli program na wejściu pobiera liczbę a i z jakiegoś powodu jest ważne, czy jest to liczba dodatnia czy ujemna, to charakterystyką może być relacja wartości a do zera. Dzieli ona zbiór wszystkich możliwych wartości a na trzy klasy: wartości mniejsze od zera, zero i wartości większe od zera. Metoda podziału na klasy równoważności może wykorzystywać jedną lub więcej charakterystyk. Mamy wtedy do czynienia z jednym lub kilkoma podziałami dziedziny na klasy równoważności. Z każdej klasy wybiera się przynajmniej jeden element, a następnie tworzy się przypadki testowe dla
wszystkich wybranych elementów. Klasa równoważności powinna być zbiorem wartości wejściowych lub wyjściowych, dla których tester spodziewa się podobnego działania programu. Klasy powinny więc być tworzone tak, że jeśli podanie na wejście (lub wymuszenie na wyjściu) wartości należącej do jednej z nich spowoduje nieprawidłowe działanie programu, to z dużym prawdopodobieństwem powinno to również nastąpić dla każdej innej wartości z tej klasy. klasa równoważności, klasa abstrakcji, podzbiór równoważności
(ang.
equivalence partition, equivalence class) – podzbiór dziedziny danych wejściowych lub wyjściowych, dla którego zakłada się, na podstawie specyfikacji, że zachowanie modułu lub systemu jest takie samo Istotą metody jest dokonanie podziału dziedziny. Jest to czynność kluczowa i – wbrew temu, co może się wydawać – w wielu przypadkach nietrywialna. Podział musi bowiem spełniać pewne warunki, które omówimy w dalszej części tego podrozdziału. Tester może dokonać podziału na klasy równoważności według swojego uznania – nie ma żadnego ścisłego, uniwersalnego przepisu na tę czynność. Rodzaj podziału zależy zawsze od programu, jego funkcjonalności, specyfikacji wymagań, przedmiotu testowania, ale przede wszystkim od intuicji testera. Jedna z fundamentalnych zasad testowania mówi, że testowanie gruntowne jest niewykonalne. Dzięki technice podziału na klasy równoważności możemy zredukować potencjalnie nieskończony zbiór danych testowych do skończonej liczby przypadków równej liczbie klas równoważności. W przypadku, gdy mamy do czynienia z jednym podziałem minimalna liczba przypadków testowych pokrywających wszystkie warunki testowe to liczba klas równoważności tego podziału. Jeżeli stosujemy jednocześnie kilka różnych podziałów P1, P2, …, Pk , to minimalna liczba przypadków testowych potrzebna do spełnienia kryterium pokrycia to maksimum z {|P1|, |P2|, …, |Pk |}, gdzie |Pi| jest liczbą klas równoważności i-tego podziału. W tabeli 8.1 podsumowano technikę podziału na klasy równoważności pod kątem warunków testowych oraz elementów, kryterium i stopnia pokrycia. Należy jednak pamiętać – i uwaga ta odnosi się do wszystkich metod testowania – że fakt osiągnięcia stuprocentowego pokrycia w danej metodzie nie oznacza stuprocentowego przetestowania programu. To, jak już wspomnieliśmy wcześniej,
jest niewykonalne. Osiągnięcie pełnego pokrycia oznacza jedynie, że w pełni wykorzystaliśmy możliwości, jakie oferuje nam konkretna technika testowania i że dany model nie jest w stanie wygenerować więcej interesujących przypadków testowych. podział
na
klasy
równoważności
(ang.
equivalence
partitioning)
–
czarnoskrzynkowa technika projektowania przypadków testowych, w której przypadki te projektowane są tak, aby użyć elementów z klas równoważności; w szczególności przypadki testowe są projektowane tak, aby co najmniej raz pokryć każdą klasę równoważności Tabela 8.1. Podsumowanie metody podziału na klasy równoważności
8.1.2. Formalna definicja podziału Zdefiniujmy teraz formalnie pojęcie podziału. Z matematycznego punktu widzenia podział dziedziny D na klasy równoważności jest wyznaczony przez relację równoważności1 określoną (niekoniecznie skończonym) zbiorem. Zbiory X1, X2,
…, Xn, n > 0 tworzą podział zbioru X, jeśli są spełnione następujące warunki: 1) wszystkie Xi są niepuste: ∀i ∈ {1, …, n} Xi ≠ ∅; 2) suma wszystkich Xi daje cały zbiór X:
3) Xi są parami rozłączne: ∀i, j ∈ {1, …, n} i ≠ j ⇒ Xi ∩ Xj ≠ ∅.
Wyjaśniając jeszcze inaczej, podział zbioru X to rodzina niepustych zbiorów Xi taka, że każdy element X należy do dokładnie jednego z nich. Na rysunku 8.1 przedstawiono poprawny podział dziedziny D pewnego programu. Każdy element
dziedziny jest reprezentowany przez czarną kropkę, a zbiory Xi są reprezentowane przez szare zaokrąglone prostokąty. W tym przykładzie dziedzina programu została podzielona na 7 klas równoważności. Zauważmy, że klasa równoważności może składać się tylko z jednego elementu (w naszym przykładzie X4).
Rysunek 8.1. Podział dziedziny na klasy równoważności Na rysunku 8.2 przedstawiono błędny podział dziedziny D. Występują tu dwa problemy: po pierwsze, zostały zdefiniowane trzy klasy, ale dwie z nich, X1 i X3, mają niepustą część wspólną. Po drugie, niektóre elementy dziedziny nie należą do żadnej klasy.
Rysunek 8.2. Niepoprawny podział dziedziny
8.1.3. Poprawne i niepoprawne klasy równoważności Dziedzina programu zawiera tylko wartości dozwolone, ale w ogólności możemy również rozważać klasy równoważności zawierające dane niepoprawne. Na przykład, jeśli dziedziną wejściową programu jest zbiór liczb naturalnych od 1 do 100, to wartościami niepoprawnymi mogą być liczby ujemne, zero, litery, wartość null, łańcuchy znaków typu string. To, czy dopuszczać w analizie wartości niepoprawne, zależy od testera. Czasami ich uwzględnienie ma sens, np. wtedy, gdy testujemy funkcję API, której możemy jako parametry przekazać dowolne wartości, w tym syntaktycznie niepoprawne lub gdy testujemy program wczytujący parametry z zewnętrznego pliku lub z linii poleceń. Czasami jednak rozważanie wartości niepoprawnych nie jest konieczne, np. wtedy, gdy testujemy formularz mający już kontrolę poprawności wprowadzenia danych lub pole zawierające predefiniowaną listę wszystkich możliwych (poprawnych) wartości. Klasy równoważności zawierające dane niepoprawne są nazywane klasami niepoprawnymi lub klasami niepoprawności, natomiast klasy zawierające wartości poprawne – klasami poprawnymi lub klasami poprawności. Zauważmy, że klasa równoważności nie może jednocześnie zawierać wartości poprawnej i niepoprawnej, bo dla wartości niepoprawnych program z definicji powinien reagować w sposób odmienny niż dla wartości poprawnych.
8.1.4. Procedura tworzenia przypadków testowych Po wyznaczeniu klas równoważności należy przystąpić do tworzenia przypadków testowych. W podrozdziale 3.2 omówiliśmy dwa skrajne podejścia do tworzenia przypadków testowych: jeden do jednego, w którym jeden przypadek testowy spełnia dokładnie jedno wymaganie testowe, oraz podejście minimalizujące, w którym jednym testem staramy się pokryć jak najwięcej wymagań testowych. Zastosowanie pierwszego podejścia może skutkować bardzo dużą liczbą przypadków testowych. Z kolei zastosowanie drugiego może skutkować trudnościami w debagowaniu w przypadku odkrycia usterki. Dlatego w przypadku metody klas równoważności wykorzystuje się podejście zrównoważone: dla klas poprawności stosujemy podejście minimalizujące, a dla klas niepoprawności – podejście jeden-do-jednego. Proces tworzenia przypadków odbywa się zatem zgodnie z następującymi regułami: 1) każdej klasie równoważności należy przypisać unikalny numer pozwalający na jej jednoznaczną identyfikację; 2) należy sukcesywnie tworzyć przypadki testowe, z których każdy pokrywa jak najwięcej niepokrytych jeszcze klas poprawności; proces ten należy kontynuować do momentu, gdy wszystkie klasy poprawności zostaną pokryte (w przypadku, gdy mamy tylko jedną charakterystykę, każdy nowy przypadek pokryje dokładnie jedną klasę równoważności); 3) należy sukcesywnie tworzyć przypadki testowe, z których każdy pokrywa jedną i tylko jedną klasę niepoprawności; proces ten należy kontynuować do momentu pokrycia wszystkich klas niepoprawności. Powodem, dla którego przypadek testowy powinien pokrywać tylko jedną klasę niepoprawności jest chęć wykrycia jak największej liczby nieprawidłowości oraz uniknięcia tzw. maskowania błędów. Może zdarzyć się, że wykrycie jednej nieprawidłowości uniemożliwia wykrycie innej. Może też zdarzyć się, że wystąpienie błędu spowodowane nieprawidłową wartością wejściową zostanie zamaskowane przez podanie nieprawidłowej wartości innej zmiennej wejściowej. Program po wykryciu jednego błędu często zaprzestaje dalszej kontroli błędów. Poniższy przykład ilustruje zjawisko maskowania błędów. Rozważmy program z listingu 8.1 obliczający wynik końcowy przedmiotu, na który składają
się punkty z egzaminu (max. 70) oraz punkty z laboratoriów (max. 30). Załóżmy, że ograniczenia na maksymalne liczby punktów nie są podane w specyfikacji. Wynik końcowy zależy od sumy punktów. Ocena 5.0 jest przyznawana za uzyskanie ponad 90 punktów, 4.0 – za uzyskanie co najmniej 71 i nie więcej niż 90 punktów, 3.0 – za uzyskanie co najmniej 51 i nie więcej niż 70 punktów. Suma mniejsza od 51 skutkuje oceną 2.0.
1
string ObliczOcenę(int pktEgz, int pktLab) if (pktEgz + pktLab > 90) then
2 3 4 5 6 7
return "5.0" else byte pktRazem = pktEgz+pktLab if (pktRazem > 70) then return "4.0 z powodu liczby punktów ="+pktRazem else if (pktRazem > 50) then
8 9
return "3.0 z powodu liczby punktów ="+pktRazem return "2.0 z powodu liczby punktow ="+pktRazem
Listing 8.1. Program obliczający końcową ocenę z przedmiotu Klasą poprawności EGZ1 dla zmiennej pktEgz jest zbiór {0, 1, …, 70}, klasami niepoprawności mogą być: EGZ2 = zbiór liczb całkowitych mniejszych od 0 oraz EGZ3 = zbiór liczb całkowitych większych od 70. Analogiczne klasy (LAB1, LAB2, LAB3) możemy zbudować dla dziedziny zmiennej pktLab. Załóżmy teraz, że chcemy pokryć przypadkami testowymi w szczególności klasy niepoprawności EGZ1 i LAB3. Z odpowiednich klas równoważności wybierzmy wartości –1000 oraz 1095. Rozważmy dwie sytuacje. W pierwszej konstruujemy jeden przypadek testowy PT1 pokrywający obie te klasy niepoprawności (pktEgz = –1000, pktLab = 1095). W drugiej tworzymy dwa przypadki, z których każdy pokrywa dokładnie jedną klasę niepoprawności: PT1′ (pktEgz = –1000, pktLab = 25) oraz PT2′ (pktEgz = 70, pktLab = 1095). W sytuacji jednego przypadku PT1 warunek w linii 1. będzie prawdziwy, bo suma punktów wynosi 1095 – 1000 = 95 i program – zgodnie ze specyfikacją! – zwróci wartość „5.0”. Błędy przekroczenia zakresu obu zmiennych zniosły się i fakt ten nie został wykryty. Rozważmy teraz przypadki PT1′ i PT2′. Po
uruchomieniu pierwszego z nich warunek w linii 1. nie zostanie spełniony. Sterowanie dojdzie do linii 4., w której nastąpi błąd przepełnienia zakresu zmiennej pktRazem, która ma typ byte, a więc jej zakres wynosi od 0 do 255. Dzięki pokrywaniu tylko jednej klasy niepoprawnej w jednym przypadku testowym udało się wykryć defekt. Miarą pokrycia w metodzie podziału na klasy równoważności jest pokrycie klas równoważności. pokrycie klas równoważności (ang. equivalence partition coverage) – odsetek klas równoważności, które zostały sprawdzone przez zestaw testów
8.1.5. Przykład Rozważamy program TypTrójkąta2 pochodzący z [14]. Na wejściu otrzymuje on trzy liczby całkowite a, b, c oznaczające długości odcinków. Na wyjściu (oznaczmy je jako zmienną t) ma określić, jaki typ trójkąta tworzą te trzy odcinki. Możliwe odpowiedzi to: „równoboczny”, „równoramienny”, „różnoboczny” oraz „nie trójkąt”. Ostatnia odpowiedź powinna być zwracana w przypadku, gdy co najmniej jedno z wejść nie jest liczbą całkowitą lub gdy zmienne wejściowe nie spełniają warunku trójkąta, tzn. gdy istnieją wśród tych liczb dwie takie, że ich suma jest mniejsza lub równa trzeciej. Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden przedmiot testowy – program TypTrójkąta. Jest on jednocześnie jedynym elementem testów: ET1: TypTrójkąta. Krok 2. Wyprowadzenie warunków testowych. Warunki testowe wyprowadzimy przez identyfikację charakterystyk programu. Możemy tu posłużyć się specyfikacją wymagań lub innym dokumentem opisującym funkcjonalność programu. Charakterystyki określą klasy równoważności, które w tej metodzie są jednocześnie warunkami testowymi. Intuicja podpowiada nam, że skoro program ma określać typ trójkąta, to najprostszą charakterystyką będzie właśnie ten typ. Jest to przykład charakterystyki opisującej dziedzinę wyjściową programu i odpowiada wartościom zmiennej t:
Charakterystyka „typ trójkąta”: równoboczny, równoramienny, różnoboczny, nie trójkąt. Charakterystyka powinna dokonać podziału dziedziny wejściowej (czyli zbioru wszystkich możliwych trójek liczb naturalnych) na cztery klasy równoważności. Zauważmy jednak, że każdy trójkąt równoboczny jest jednocześnie trójkątem równoramiennym, zatem jedna klasa całkowicie zawiera się w drugiej. Zdefiniowana przez nas charakterystyka nie tworzy poprawnego podziału! Musimy wprowadzić modyfikację polegającą na wyłączeniu trójkątów równobocznych z klasy trójkątów równoramiennych: Charakterystyka Ch1: „typ trójkąta”: „równoboczny”, „równoramienny i nie równoboczny”, „różnoboczny”, „nie trójkąt”. Tak zdefiniowana charakterystyka wyznacza już poprawny podział dziedziny wyjściowej na klasy równoważności. Przy okazji odkryliśmy defekt (niejednoznaczność) w specyfikacji polegający na tym, że trójkątom o trzech równych bokach można przypisać dwa typy: równoboczny i równoramienny. Specyfikacja powinna w wyraźny sposób stwierdzać, że z typu „równoramienny” należy wyłączyć trójkąty równoboczne. Wyjście „równoramienny” traktować więc będziemy jako „równoramienny i nie równoboczny”. Możemy dodatkowo określić klasy niepoprawności dla wyjścia. Nieprawidłowa wartość wyjścia to każda wartość inna od czterech zdefiniowanych powyżej. Jeśli uda się uzyskać wartość nieprawidłową na wyjściu, oznacza to istnienie defektu w przedmiocie testów, podstawie testów lub w obu tych artefaktach. Określanie wyjść nieprawidłowych jest rzeczą subiektywną, zależy od testera i jego znajomości testowanego programu. Możemy np. zdefiniować klasę niepoprawności null, czyli sytuację, w której program nie zwraca żadnego łańcucha znaków na wyjściu. Istnieje 5 klas, dlatego wyznaczają one 5 warunków testowych opisanych w tabeli 8.2. Tabela 8.2. Zestaw warunków testowych dla programu TypTrójkąta na podstawie charakterystyki „typ trójkąta”
Nr Odpowiadający warunku element W arunek testowy testowego testów
WT1
ET1
Trójkąt równoboczny (wymus zony przez a = b = c)
WT2
ET1
Trójkąt równoramienny (wymus zony przez (a = b ∨ a = c ∨ b = c) ∧~(a = b = c))
WT3
ET1
Trójkąt różnoboczny (wymus zony przez a ≠ b ∧ a ≠ c ∧ b ≠ c)
WT4
ET1
Nie trójkąt (wymus zony przez a + b < c ∨ a + c < b ∨ b + c < a lub przez to, że co najmniej jedno wejś cie nie jes t typu int)
WT5
ET1
null
Intuicyjnie czujemy, że np. przypadek testowy (4, 4, 4), pokrywający warunek testowy WT1, powinien spowodować podobne działanie programu jak (6, 6, 6), (11, 11, 11) czy każdy inny przypadek opisujący trzy identyczne liczby całkowite dodatnie na wejściu. Inne podejście do problemu może polegać na analizie dziedziny wejściowej. Skoro specyfikacja mówi, że na wejściu program spodziewa się trzech liczb całkowitych dodatnich, to możemy zdefiniować trzy następujące charakterystyki: relacja a do zera, relacja b do zera oraz relacja c do zera. Dodatkowo, dla każdej zmiennej wprowadźmy dwie klasy niepoprawności „liczba niecałkowita” oraz „nie liczby” (czyli ciągi znaków zawierające litery lub znaki specjalne). Dla każdej charakterystyki otrzymujemy więc następujące podziały: Ch2: a ≤ 0, a ≥ 1, a nie jest całkowite, a nie jest liczbą; Ch3: b ≤ 0, b ≥ 1, b nie jest całkowite, b nie jest liczbą; Ch4: c ≤ 0, c ≥ 1, c nie jest całkowite, c nie jest liczbą. Otrzymaliśmy 3 podziały (każdy z czterema klasami równoważności) dla trzech dziedzin będących zbiorami wartości poszczególnych wejść do programu. W sumie mamy 12 klas równoważności (tab. 8.3).
Tabela 8.3. Zestaw warunków testowych dla programu TypTrójkąta na podstawie charakterystyk Ch2, Ch3 i Ch4
Nr warunku testowego
Odpowiadający przedmiot testów
Charakterystyka
W arunek testowy
WT6
ET1
Ch2
a ≤ 0, a całkowite
WT7
ET1
Ch2
a ≥ 1, a całkowite
WT8
ET1
Ch2
a liczbą niecałkowitą
WT9
ET1
Ch2
a zawiera litery lub znaki
WT10
ET1
Ch3
b ≤ 0, b całkowite
WT11
ET1
Ch3
b ≥ 1, b całkowite
WT12
ET1
Ch3
b liczbą niecałkowitą
WT13
ET1
Ch3
b zawiera litery lub znaki
WT14
ET1
Ch4
c ≤ 0, c całkowite
WT15
ET1
Ch4
c ≥ 1, c całkowite c liczbą
WT16
ET1
Ch4
niecałkowitą
WT17
ET1
Ch4
c zawiera litery lub znaki
Warunki testowe 8, 9, 12, 13, 16, 17 reprezentują klasy niepoprawności. Pozostałe reprezentują klasy poprawności. Krok 3. Wyprowadzenie elementów pokrycia. Elementy pokrycia (klasy równoważności) są jednocześnie warunkami testowymi. Mamy zatem następujących 17 elementów pokrycia: EP1: wyjście „równoboczny” wymuszone przez a = b = c (wyprowadzony z WT1); EP2: wyjście „równoramienny” wymuszone przez (a = b ∨ a = c ∨ b = c) ∧ ~(a = b = c) (z WT2); EP3: wyjście „różnoboczny” wymuszone przez a ≠ b ∧ a ≠ c ∧ b ≠ c (z WT3); EP4: wyjście „nie trójkąt” wymuszone przez a + b < c ∨ a + c < b ∨ b + c < a lub przez to, że co najmniej jedno wejście nie jest typu int (z WT4); EP5: null (z WT5); EP6: a ≤ 0, całkowite (z WT6); EP7: a ≥ 1, całkowite (z WT7); EP8: a liczbą niecałkowitą (z WT8); EP9: a zawiera litery lub znaki specjalne (z WT9); EP10: b ≤ 0, całkowite (z WT10); EP11: b ≥ 1, całkowite (z WT11); EP12: b liczbą niecałkowitą (z WT12); EP13: b zawiera litery lub znaki specjalne (z WT13); EP14: c ≤ 0, całkowite (z WT14); EP15: c ≥ 1, całkowite (z WT15); EP16: c liczbą niecałkowitą (z WT16); EP17: c zawiera litery lub znaki specjalne (z WT17). Krok 4. Zdefiniowanie przypadków testowych. Po zdefiniowaniu warunków testowych i elementów pokrycia przechodzimy do tworzenia przypadków
testowych. Stosując opisaną w punkcie 8.1.1 procedurę, staramy się, aby każdy kolejny przypadek testowy pokrywał jak najwięcej niepokrytych dotąd poprawnych klas równoważności, a następnie – aby każdy kolejny przypadek pokrywał dokładnie jedną klasę niepoprawności. Rezultat tego postępowania jest przedstawiony w tabeli 8.4. Kolejność tworzenia przypadków wyznaczają ich identyfikatory. Pogrubioną czcionką zaznaczono elementy pokryte po raz pierwszy. Tabela 8.4. Przypadki testowe pokrywające klasy równoważności dla programu TypTrójkąta
W ejście T est
Pokryte elementy pokrycia
b
c
dot. klas poprawności
PT1
3
3
3
EP1, EP7, EP11, EP15
Równoboczny
PT2
–3
–3 –3
EP4, EP6, EP10, P14
Nie trójkąt
PT3
5
7
5
EP2, EP7, EP11, EP15
Równoramienny
PT4
4
5
6
EP3, EP7, EP11, EP15
Różnoboczny EP5
null
PT6
6.33 4
0
EP11, EP15
EP8
Nie trójkąt
PT7
1
– 3 3.7
EP7, EP15
EP12
Nie trójkąt
PT8
–1
5
8.94 EP6, EP11
EP16
Nie trójkąt
PT9
xy
4
2
EP9
Nie trójkąt
PT5
EP11, EP15
dot. klas niepoprawn.
Oczekiwane wyjście
a
PT10 –9
B4 4
EP6, EP15
EP13
Nie trójkąt
PT11 0
0
abc EP6, EP10
EP17
Nie trójkąt
Przypadki PT1–PT4 pokrywają wszystkie klasy poprawności wszystkich czterech charakterystyk. Przypadek PT5 pokrywa klasę niepoprawności charakterystyki Ch1, a przypadki PT6–PT11 – klasy niepoprawności charakterystyk Ch2, Ch3 i Ch4. Zauważmy, że przypadki PT6–PT11 pokrywają jednocześnie klasę poprawności jednej charakterystyki i klasę niepoprawności innej charakterystyki. Jest tak dlatego, że charakterystyki te są od siebie niezależne i jedna dotyczy wejścia, a druga wyjścia. Wyjście programu (opisane charakterystyką nr 1) przewiduje możliwość wpisania błędnych danych wejściowych („nie trójkąt”), więc stanowi ona klasę poprawności dla dziedziny wyjściowej. Jednocześnie – zgodnie ze specyfikacją – oczekuje na wejściu liczb całkowitych, więc każde wejście niebędące liczbą całkowitą należy do jednej z dwóch zdefiniowanych przez nas klas niepoprawności. Jedenaście testów zdefiniowanych w tabeli 8.4 pokrywa wszystkie zdefiniowane klasy równoważności (warunki testowe), zatem pokrycie klas równoważności wynosi 100%. Gdybyśmy ograniczyli zbiór testów do dwóch przypadków PT1 i PT2, to pokrycie dla takiego zbioru wyniosłoby 8/17 = 46%, ponieważ przypadki te w sumie pokrywają 8 elementów pokrycia z 17 możliwych. Krok 5. Stworzenie suit testowych. Grupowanie przypadków testowych w suity (zbiory) testowe może zależeć od różnych czynników. Jeśli np. wiemy, że można zautomatyzować weryfikację wyniku rzeczywistego z oczekiwanym tylko w sytuacji, gdy wejście lub wyjście należy do jednej z klas poprawności, to możemy stworzyć dwie suity testowe. Pierwsza złożona będzie z przypadków poddających się automatyzacji, druga – z przypadków wykonywanych manualnie: ST1: Automatyczne testowanie – PT1, PT2, PT3, PT4; ST2: Testy manualne – PT5, PT6, PT7, PT8, PT9, PT10, PT11. Krok 6. Stworzenie procedur testowych. Dla testów automatycznych procedura testowa może wyglądać następująco:
PROC1: Testowanie automatyczne suity testowej ST1 przy użyciu skryptu, zgodnie z kolejnością, w jakiej testy pojawiają się w ST1. Dla testów manualnych procedura testowa może wyglądać następująco: PROC2: Testowanie manualne suity testowej ST2, w kolejności występowania testów w ST2.
8.1.6. Przykład śledzenia artefaktów procesu testowego Każdy artefakt wytworzony podczas opisanego wcześniej procesu może być powiązany z innymi artefaktami. W punkcie 3.1.3 opisaliśmy rolę tzw. obustronnego śledzenia, które zapewnia to powiązanie. Na rysunku 8.3 pokazano obustronne śledzenie między elementami testowymi, warunkami testowymi i elementami pokrycia, a także między przypadkami testowymi a suitami testowymi dla naszego przykładu z punktu 8.1.5. Brakujący element na tym rysunku – śledzenie między elementami pokrycia a przypadkami testowymi – jest pokazany w tabeli 8.5 w formie tzw. macierzy identyfikowalności (ang. traceability matrix). Każdy wiersz macierzy odpowiada jednemu elementowi pokrycia, a każda kolumna – przypadkowi testowemu. Znak „X” na przecięciu wiersza i oraz kolumny j oznacza, że przypadek testowy PTj pokrywa element pokrycia EPi.
Rysunek 8.3. Obustronne śledzenie między artefaktami procesu testowego
Tabela 8.5. Macierz identyfikowalności dla elementów pokrycia i przypadków testowych
PT 1 PT 2 PT 3 PT 4 PT 5 PT 6 PT 7 PT 8 PT 9 PT 10 PT 11 EP1
X
EP2
X
EP3
X
EP4
X
EP5
X
EP6 EP7
X X
X X
X
EP8
X X X
EP11 X
X X
X
X
EP12
X
X
X
EP13 EP14 EP15 X EP16 EP17
X
X
EP9 EP10
X
X X X
X
X
X
X
X
X X
W rozważaniach tych wszystkie charakterystyki były jednowymiarowe, to znaczy dotyczyły jednej zmiennej. Dla przypadków wielowymiarowych oraz
kryteriów kombinacyjnych istnieją osobne, specjalne wersje metody podziału na klasy równoważności. Są one opisane w podrozdziałach 8.8 oraz 8.9.
8.2. Analiza wartości brzegowych 8.2.1. Opis metody Technika analizy wartości brzegowych AWB (ang. Boundary Value Analysis, BVA) jest oparta na metodzie podziału na klasy równoważności. Tak jak w tej ostatniej dziedzinę wejściową lub wyjściową dzieli się na klasy, ale różnica polega na tym, że wybierane do testów elementy leżą na brzegach tych klas. Istnienie brzegów implikuje również, że metoda analizy wartości brzegowych działa tylko dla zmiennych, które da się opisać na skali porządkowej. Innymi słowy, na wartościach tych zmiennych można określić relację porównującą elementy ze sobą. To z kolei ogranicza stosowanie analizy wartości brzegowych do przypadków jednowymiarowych. Przypadki wielowymiarowe opiszemy w podrozdziale 8.9. Ponadto, jeśli klasa równoważności zawiera elementy a i b (a < b), to musi zawierać również wszystkie elementy większe od a i mniejsze od b. Oznacza to, że klasa równoważności musi zawierać zwarty zbiór wartości. Nie może być „przedzielona” inną klasą. Przykłady zbiorów, dla których można przeprowadzić AWB: podzbiór zbioru liczb rzeczywistych (np. typ double); podzbiór zbioru liczb całkowitych (np. typ int); podzbiór znaków (jeśli np. traktujemy znaki jako liczby kodu ASCII, umożliwia to porównanie ich między sobą); zbiór enumeratywny na skali porządkowej (np. poziom zadowolenia na skali typu Likerta: „bardzo niezadowolony”, „niezadowolony”, „obojętny”, „zadowolony”, „bardzo zadowolony”). Hipoteza błędu polega tu na tym, że programista może się pomylić w stosowaniu operatorów porównań i np. zamiast silnej nierówności użyć słabej. Inną możliwością jest np. przetwarzanie tablicy począwszy od elementu o indeksie jeden zamiast zero. Tego typu błędy czasami nazywa się „pomyłkami o jeden”. Podstawowym pojęciem tej techniki jest wartość brzegowa. Wartość brzegowa zawsze dotyczy konkretnego przedziału. Jest to liczba zawarta w tym przedziale
i leżąca na jego granicy. Granicami mogą być także wartości ekstremalne zakresu zmiennej, dla której rozważamy podziały. Słownik ISTQB [6] definiuje wartość brzegową w następujący sposób: „wartość brzegowa – wartość wejścia lub wyjścia, która leży na granicy klas równoważności; również minimalna lub maksymalna wartość zakresu”. Pojęcie „leżenia na granicy klas” jest nie tylko nieprecyzyjne, lecz także wręcz błędne (i może wprowadzać pewne zamieszanie – patrz p. 8.2.3), ponieważ wartość brzegowa zawsze należy do konkretnej klasy. Pamiętajmy, że klasy równoważności pokrywają całą dziedzinę i każdy element należy do dokładnie jednej z nich. Oczywiście dla zmiennej x typu int i warunku x > 5 można zdefiniować granicę jako wartość 5.5, oddzielającą klasę [MININT, 5] od klasy [6, MAXINT]. Jest to jednak dosyć sztuczne, bo: po pierwsze nie ma sensu używać wartości nienależących do dziedziny; po drugie można zapytać, dlaczego akurat wartością tą ma być 5.5, a nie np. 5.89 albo 5.003; po trzecie – jak zobaczymy w punkcie 8.2.2 – nie bardzo wiadomo, z którego z dwóch sąsiadujących z tą wartością przedziałów należy brać wartości graniczne jako elementy pokrycia. Kolejna uwaga, jaką można mieć do powyższej definicji, jest taka, że wartość brzegowa powinna być zdefiniowana jako wartość brzegowa dla konkretnego przedziału, a nie sugerować, że dotyczy ona więcej niż jednej klasy równoważności! Tak postawiona definicja może bowiem wprowadzić testera w pewną konfuzję, o której piszemy w dalszych podrozdziałach. Przyjmiemy następującą, prostą i naturalną definicję wartości brzegowej: wartość brzegowa dla przedziału X (ang. boundary value) – najmniejsza lub największa wartość należąca do X Przedział może być wyznaczony przez klasę równoważności, ale musimy pamiętać, że klasa ta nie może mieć „dziur” – musi być „zwarta”. Jeśli przedział jest ograniczony obustronnie, to ma dwie wartości brzegowe. Na przkład przedział [a, b] ma wartości brzegowe a oraz b. Przedziały jednostronnie ograniczone – najczęściej występujące w predykatach instrukcji warunkowych – mają tylko jedną wartość brzegową, chyba że uwzględnimy maksymalne i minimalne wartości dopuszczane przez reprezentację komputerową. Na przykład przedział opisany warunkiem x > a dla zmiennej x typu int ma wartość brzegową a + 1, ponieważ jest to najmniejsza liczba należąca do przedziału [a + 1,
∞) wyznaczonego tym warunkiem. Biorąc pod uwagę reprezentację liczb w komputerze, możemy przyjąć drugą wartość brzegową jako MAXINT. Przy określaniu wartości brzegowych bardzo ważny jest typ zmiennej występującej w warunku definiującym przedział. Tę kwestię omówimy dokładnie w punkcie 8.2.4. analiza
wartości
brzegowych
(ang.
boundary
value
analysis)
–
czarnoskrzynkowa metoda projektowania przypadków testowych, w której przypadki te są tworzone na podstawie wartości brzegowych W rozdziale przedstawimy metodę AWB na dosyć prostych przykładach. Może to sprawiać wrażenie, że jest ona prostą, banalną techniką, niespecjalnie pomocną w projektowaniu przypadków testowych. Metoda ta jest jednak dużo bardziej potężna, niż to się może wydawać. Niestety wiele podręczników (a także sylabusy i treści pytań egzaminacyjnych ISTQB) trywializuje pojęcie wartości brzegowych. Zaawansowane zastosowanie tej techniki omówimy w podrozdziale 8.9, a w punkcie 20.1.3 pokażemy przykład wymagań prowadzących do nieoczywistych wartości brzegowych, których odkrycie wymaga dalece bardziej wysublimowanej analizy, niż ta stosowana w tym rozdziale.
8.2.2. Metody dwóch oraz trzech wartości granicznych Wartości brzegowe stanowią warunki testowe, z których wyprowadza się elementy pokrycia. Istnieją dwie powszechnie przyjęte metody tego wyprowadzenia: metoda dwóch wartości granicznych; metoda trzech wartości granicznych. Omówimy je na przykładzie. Załóżmy, że chcemy przeprowadzić analizę wartości brzegowych dla następującego wymagania programu ZniżkaMPK przydzielającego różnego typu zniżki na bilety komunikacji miejskiej: „zniżka na bilet okresowy przysługuje osobom do 18 roku życia”, przy czym zmienna określająca wiek jest typu int. Pierwszym krokiem jest określenie klas równoważności. Naturalnymi kandydatami są: klasa
reprezentująca wiek osób uprawnionych do zniżki (liczby całkowite {0, 1, 2, …, 17, 18}) oraz klasa reprezentująca wiek osób nieuprawnionych do zniżki (liczby całkowite od 19 wzwyż). Możemy zdefiniować również klasę niepoprawności, reprezentującą wartości ujemne. W rezultacie dziedzina zmiennej została podzielona na trzy klasy równoważności X1, X2, X3 tak, jak to przedstawiono na rysunku 8.4.
Rysunek 8.4. Klasy równoważności i wartości brzegowe dla programu ZniżkaMPK Klasa X2 reprezentuje przedział całkowity [0, 18]. Jedną z wartości brzegowych klasy X2 jest 18. W metodzie dwóch wartości granicznych jako elementy pokrycia bierze się oryginalną wartość brzegową (w naszym przypadku 18) oraz najbliższą jej wartość leżącą poza klasą X2. Taką wartością jest 19, należące już do X3.
W metodzie trzech wartości granicznych oprócz tych dwóch liczb bierze się dodatkowo wartość najbliższą brzegowej, ale leżącą po jej drugiej stronie. W naszym przypadku jest to wartość 17. Jeśli rozważana klasa równoważności jest złożona z więcej niż jednego elementu, to wartość ta należy do tej samej klasy, co wartość brzegowa. W przeciwnym wypadku należy ona do innej klasy. Podsumowując, dla klasy X2 i wartości brzegowej 18 (dla warunku 0 ≤ wiek ≤ 18) mamy następujące elementy pokrycia:
dla metody dwóch wartości granicznych: 18, 19; dla metody trzech wartości granicznych: 17, 18, 19. Ilustracja graficzna porównania tych dwóch metod jest pokazana na rysunku 8.5. Wartość brzegowa klasy X2 jest zaznaczona kółkiem. Formalnie, dana niech będzie jednowymiarowa dziedzina D i ustalona klasa równoważności X ⊆ D reprezentująca przedział. Wartościami brzegowymi dla X są elementy min{X} i max{X} oznaczające odpowiednio najmniejszy i największy element zbioru X. Zauważmy, że takie elementy zawsze istnieją, nawet dla
zmiennych rzeczywistych, w komputerze.
ze
względu
na
dyskretną
reprezentację
liczb
Rysunek 8.5. Metody dwóch oraz trzech wartości granicznych Elementami pokrycia dla wartości brzegowej min{X} są: {min{X}, max{x: x < min{X}}} dla metody dwóch wartości granicznych; {min{X}, max{x: x < min{X}}}, min{x: x > min{X}}} dla metody trzech wartości granicznych. Analogicznie, elementami pokrycia dla wartości brzegowej max{X} są: {max{X}, min{x: x > max{X}}} dla metody dwóch wartości granicznych; {max{X}, min{x: x > max{X}}, max{x: x < max{X}}} dla metody trzech wartości granicznych. Jak widać, metoda trzech wartości granicznych jest silniejsza od metody dwóch wartości granicznych, gdyż jej elementy pokrycia są nadzbiorem elementów pokrycia tej ostatniej. Można jednak zadać pytanie: jeśli sprawdzamy warunek wiek ≤ 18 dla wartości brzegowej 18, to po co sprawdzać dodatkowo wartość 17? Czy poprawność działania testu dla przypadku 18 nie implikuje poprawności działania testu dla przypadku 17? Niestety nie możemy tego zagwarantować. Programista mógłby warunek wiek ≤ 18 zaimplementować
błędnie jako wiek = 18. Wtedy testy stworzone za pomocą metody dwóch wartości granicznych nie wykryłyby defektu, bo dla wartości 18 i 19 powyższy warunek, choć błędny, dałby poprawne wartości logiczne, tak jak warunek poprawny wiek ≤ 18. Dodanie do testów wartości 17 wykryłoby defekt, ponieważ poprawny warunek dla wiek = 17 powinien zachodzić (bo 17 ≤ 18 jest prawdą), natomiast w błędnej implementacji nie zajdzie (bo 17 = 18 jest fałszem). Niektórzy autorzy (np. Rex Black [93]) uważają, że używanie metody trzech wartości granicznych to strata czasu. O ile można się do pewnego stopnia zgodzić z tezą, że stosowanie tej metody nie zwiększa istotnie prawdopodobieństwa wykrycia błędu, o tyle – w świetle tego przykładu – trudno przychylić się do kategorycznych ocen mówiących o jej bezużyteczności. Stosowanie metody trzech wartości granicznych generuje większą liczbę przypadków testowych i jeśli tylko tester jest w stanie wykonać je w rozsądnym czasie, to może ją stosować. Wybór metody zależy od różnych czynników, w tym od poziomu dopuszczalnego ryzyka. Metoda trzech wartości granicznych sprawia jednak pewne problemy, które dyskutujemy w punkcie 8.2.3. Należy pamiętać, że wartościami brzegowymi są również wartości leżące na granicach zakresu zmiennych. Wartości te zwykle nie są podawane jawnie w specyfikacji. Na przykład specyfikacja dla programu ZniżkaMPK sugeruje, że wiek może być dowolnie duży, jednak zmienna reprezentująca go w implementacji zawsze będzie mieć ograniczony zakres. Jeśli jest to zmienna typu byte, zapisywana na 8 bajtach, to jej maksymalna wartość wynosi 255. Poprawne wartości brzegowe oraz poprawne elementy pokrycia to te, które należą do klas poprawności. Analogicznie, niepoprawne wartości brzegowe oraz niepoprawne elementy pokrycia to te, które należą do klas niepoprawności. Miarą pokrycia w metodzie analizy wartości brzegowej jest liczba pokrytych testami elementów pokrycia w stosunku do wszystkich zidentyfikowanych elementów pokrycia. W tabeli 8.6 przedstawiono podsumowanie metody analizy wartości brzegowych. pokrycie wartości brzegowych – odsetek wyprowadzonych z wartości brzegowych elementów pokrycia, który został pokryty testami Tabela 8.6. Podsumowanie metody analizy wartości brzegowych
Warunki tes towe
Zbiór ws zys tkich wartoś ci brzeg owych, czyli g ranic zidentyfikowanych klas równoważnoś ci
Zbiór ws zys tkich wartoś ci g ranicznych (po 2 dla wartoś ci Elementy brzeg owej w przypadku metody dwóch wartoś ci pokrycia g ranicznych lub po 3 w przypadku metody trzech wartoś ci g ranicznych) Kryterium Dla każdej wartoś ci g ranicznej is tnieje przynajmniej jeden pokrycia tes t, który wykorzys tuje tę wartoś ć Pokrycie
p = (liczba pokrytych tes tami wartoś ci g ranicznych/liczba ws zys tkich wartoś ci g ranicznych) × 100%
8.2.3. Które wartości rozważać jako brzegowe? Rozważmy prosty przykład. Dla warunku x ≤ 6 mamy dwie klasy równoważności: K1: MININT ≤ x ≤ 6 oraz K2: 6 ≤ x ≤ MAXINT. Jedną z wartości brzegowych dla tego warunku jest 6. W tym momencie uważny Czytelnik powinien zauważyć pewną subtelność. Warunek x ≤ 6 jest przecież równoważny warunkowi x < 7, co daje klasy równoważności MININT ≤ x < 7 oraz 7 ≤ x ≤ MAXINT. Są to dokładnie te same klasy równoważności. Dlaczego więc w przypadku klas K1 i K2 jako wartość brzegową wybieramy 6, a nie 7? Odpowiedź jest prosta – wartością brzegową jest wartość leżąca na brzegu konkretnego przedziału, zawarta w tym przedziale. Nasz przedział wyznaczony jest warunkiem x ≤ 6, czyli jest to przedział (–∞, 6], zatem największą wartością należącą do niego jest liczba 6, tak samo, jak dla (identycznego) przedziału wyznaczonego przez warunek x < 7. Jeśli chcemy rozważać wartości brzegowe przedziału dopełniającego, czyli x > 6, to dla niego wartością brzegową jest oczywiście 7. W przypadku metody dwóch wartości granicznych dyskusja ta nie ma żadnego znaczenia, bo niezależnie od tego, czy za wartość brzegową przyjmiemy element graniczny rozważanej klasy (6), czy też element graniczny klasy sąsiedniej (7), będzie to skutkować dokładnie takim samym zbiorem elementów pokrycia, tzn. {6, 7}. Zbiór wszystkich wartości brzegowych wszystkich klas równoważności będzie zawsze równy zbiorowi wszystkich wartości granicznych dla tych wartości brzegowych.
Gdy rozważamy pojedynczą klasę równoważności, wyznaczanie wartości granicznych nie sprawia żadnych problemów. Jednak tester zazwyczaj sprawdza wartości
graniczne
wszystkich
zidentyfikowanych
klas
równoważności,
w szczególności klas sąsiadujących ze sobą. W takiej sytuacji, stosując metodę trzech wartości granicznych stajemy przed pewnym problemem. Jeśli dla warunku x ≤ 6 przyjmiemy 6 za wartość brzegową, to elementami pokrycia będą wartości graniczne 5, 6 i 7. Dla sąsiedniej klasy wartością brzegową jest 7 i elementami pokrycia będą wtedy wartości graniczne 6, 7 i 8. Jeśli przyjmujemy obie te wartości brzegowe jako obowiązujące warunki testowe (bo np. chcemy testować wartości brzegowe wszystkich klas równoważności), to w takim przypadku metoda powinna raczej nazywać się metodą czterech wartości granicznych. Zawsze bowiem da w rezultacie zbiór czterech elementów pokrycia – elementy brzegowe oraz elementy bezpośrednio z nimi sąsiadujące (w naszym przykładzie jest to zbiór {5, 6, 7, 8}). Jeśli wartości brzegowych jest n, to w sumie do pokrycia testami otrzymamy liczbę wartości granicznych rzędu3 2n – ok. dwa razy więcej niż dla metody dwóch wartości granicznych. To zmusza nas do stworzenia większej liczby testów, co nie zawsze jest pożądane. Aby wybrnąć z tego problemu (zostając przy metodzie trzech wartości granicznych i nie generując zbyt dużej liczby przypadków testowych), rozważając wartości brzegowe w kontekście dwóch sąsiadujących przedziałów, za wartość brzegową można przyjąć wartość występującą w specyfikacji. Na przykład jeśli pojawia się w niej sformułowanie: „bezpłatny przejazd przysługuje dzieciom do lat 6”, to mamy do czynienia z przedziałami wyznaczonymi warunkami x ≤ 6, x ≥ 7, ale interesującą nas wartością brzegową będzie 6, czyli wartość brzegowa dla klasy x ≤ 6, gdyż występuje ona jawnie w specyfikacji. Liczby 7 nie rozważa się wtedy jako wartości brzegowej. Stosując metodę trzech wartości granicznych jako elementy pokrycia otrzymamy jednoznacznie wyznaczony zbiór {5, 6, 7}. W ogólności, dla n wartości brzegowych liczba elementów pokrycia będzie rzędu 3n/2, czyli o ok. 25% mniejsza niż dla przypadku 2n. Takie podejście do tworzenia elementów pokrycia nazywać będziemy techniką selektywną. Jeśli hipoteza błędu dotyczy źle zastosowanego operatora relacyjnego, to w kontekście analizy wartości brzegowych lepiej jest definiować wymagania za pomocą nierówności ostrych lub – przyjmując metodę trzech wartości granicznych – stosować technikę selektywną. Pokazuje to następujący przykład. Rozważmy występujący w specyfikacji warunek x ≤ p i równoważną mu postać x
p +1
+
+
+
+
x ≥ p +1
+
+
+
+
W tabeli 8.7 przedstawiono wyniki dla obu wersji implementacji warunku oraz do czterech wymienionych powyżej technik identyfikacji wartości granicznych. Plus oznacza, że w danej sytuacji defekt zostanie wykryty, to znaczy, że któraś z wartości granicznych po podstawieniu do (błędnego) warunku da inną wartość logiczną niż warunek poprawny. Minus oznacza, że defekt pozostanie nieujawniony, tzn. każda wartość graniczna da tą samą wartość logiczną zarówno dla poprawnego, jak i błędnego warunku. Zauważmy, że metoda trzech wartości granicznych z użyciem techniki selektywnej oraz metoda trzech wartości granicznych dla obu wartości brzegowych pozwalają na wykrycie defektu niezależnie od rodzaju popełnionej przez programistę pomyłki oraz od wersji implementacji. Ponadto wszystkie cztery techniki identyfikacji wartości granicznych pozwolą na ujawnienie defektu niezależnie od rodzaju popełnionej pomyłki, jeśli stosujemy warunek z ostrą nierównością (m.in. dlatego w warunkach pętli zaleca się stosowanie właśnie tej konwencji, np. for (i=0; i 0, bo wyrażenie „ostrzeżenie jest większe od braku ostrzeżenia” nie ma sensu. Krok 3. Wyprowadzenie elementów pokrycia. Z każdej wartości brzegowej wyprowadzamy dwa elementy pokrycia (wartości graniczne): jednym jest sama wartość brzegowa, drugim – najbliższa jej wartość należąca do sąsiedniej klasy równoważności. Pamiętajmy, że zmienne s, t oraz v przyjmują wartości rzeczywiste. Droga (w m) jest podana z dokładnością do centymetra, czyli z dokładnością do 1/100 = 0.01 m, zatem εs = 10–2. Czas (w sekundach) jest podany z dokładnością do setnej sekundy. Zatem minimalna wartość, o jaką może zmienić się t to 1/100 = 0.01 s. Możemy więc bezpiecznie przyjąć εt = 10–2. Prędkość v stanowi zmienną typu double, jest wyliczana jako iloraz drogi i czasu, przy czym według specyfikacji czas t jest nie większy niż 1 sekunda. Zmienna v nie powinna zmieniać się o więcej niż iloraz minimalnego skoku drogi i maksymalnego skoku prędkości, czyli 10–2 m/1 s = 10–2 m/s. Możemy więc przyjąć ev = 10–2.
EP1: s = MINDBL (z WT1); EP2: s = –0.01 (z WT2); EP3: s = 0 (z WT2); EP4: s = MAXDBL (z WT3); EP5: t = MINDBL (z WT4); EP6: t = –0.01 (z WT5); EP7: t = 0 (z WT5); EP8: t = 0.01 (z WT5); EP9: t = 1 (z WT6); EP10: t = 1.01 (z WT6); EP11: t = MAXDBL (z WT7); EP12: v = MINDBL (z WT8); EP13: v = –0.01 (z WT9); EP14: v = 0 (z WT9); EP15: v = 0.01 (z WT9); EP16: v = 120 (z WT10); EP17: v = 120.01 (z WT10); EP18: v = MAXDBL (z WT11), Elementy pokrycia EP1–EP11 dotyczą wartości dziedziny wejściowej. Elementy EP12–EP18 dotyczą wartości dziedziny wyjściowej. Niepoprawnymi elementami pokrycia są elementy EP1, EP2, EP5, EP6 i EP10, EP11, EP12, EP13. Pozostałe, ponieważ należą do klas poprawności, są elementami poprawnymi. Krok 4. Zdefiniowanie przypadków testowych. Ze względu na instrumentację kodu opisaną w poprzednim kroku zmienne wewnętrzne total_t i total_s traktujemy jak dane wejściowe. Zastosujemy technikę minimalizującą dla poprawnych wartości granicznych i jeden-do-jednego dla niepoprawnych. Innym podejściem mogłoby być np. wykorzystanie technik kombinacyjnych opisanych w podrozdziale 8.8. Tabela 8.8. Przypadki testowe w analizie wartości brzegowych dla programu CarPanel
Oczekiwane wyjścia
Id
(WARN = 1 gdy v > Pokryte elementy 120) pokrycia
W ejścia
s
t
v = s/t
WARN
PT1
0
0
0
0
EP3, EP7, EP14
PT2
120
1
120
0
EP9, EP16
PT3
MAXDBL 1
MAXDBL
1
EP4, EP18, EP9
PT4
1
0.01
0.01
0
EP8, EP15
PT5
120.01
1
120.01
1
EP17, EP9
PT6
MINDBL
2
MINDBL/2 0
EP1
PT7
–0.01
0.02
–2
0
EP2
PT8
2
MINDBL
2/MINDBL
0
EP5
PT9
–40
0.4
0
EP6
121.2
PT10 120
1.01
1
EP10
PT11 2
MAXDBL 2/MAXDBL 0
EP11
2⋅ PT12 MINDBL
2
MINDBL
0
EP12
PT13 –0.02
2
–0.01
0
EP13
Pogrubione elementy pokrycia w ostatniej kolumnie tabeli oznaczają, że dany element został pokryty po raz pierwszy. Jedynie element EP9 został pokryty trzykrotnie, w PT2, PT3 i PT5. Zestaw opisanych w tabeli 8.8 13 przypadków testowych spełnia w 100% kryterium pokrycia wartości brzegowych. Gdyby np. zbiór testowy ograniczyć tylko do przypadków PT1–PT5, pokrycie wyniosłoby 10/18 ≈ 56%.
Kolejny krok to stworzenie suit i procedur testowych. Można np. stworzyć osobną suitę testową dla testów wykorzystujących nieprawidłowe wartości i osobną dla wszystkich wartości wejściowych poprawnych.
8.3. Tablice decyzyjne 8.3.1. Opis metody Metody: podziału na klasy równoważności oraz analizy wartości brzegowej sprawdzają się dobrze przy dzieleniu zbioru możliwych wartości na klasy i rozważaniu każdej z nich z osobna. Podstawową słabością tych metod jest to, że nie uwzględniają one kombinacji różnych wartości wejściowych. Na przykład działanie programu CarPanel opisanego w poprzednim rozdziale może skutkować awarią, jeśli w jednym przypadku testowym przyjmiemy wartości wejściowe s = MAXDBL, t = 0.01, to wtedy wartość v = s/t przekroczy zakres zmiennej typu double. Tablice decyzyjne (omawiane w tym rozdziale) oraz grafy przyczynowo-skutkowe, techniki kombinacyjne, drzewa decyzyjne i analiza dziedziny opisane w podrozdziałach 8.4, 8.7, 8.8 i 8.9 pozwalają na testowanie programu pod kątem występowania różnych kombinacji wartości wejściowych. Hipoteza błędu w przypadku tablic decyzyjnych mówi, że błędy mogą powstawać na skutek nieprawidłowego obsłużenia sytuacji będącej wynikiem kombinacji różnych czynników. Tablice decyzyjne są techniką przydatną do testowania logiki biznesowej programu. Działanie wielu programów można opisać systemami regułowymi: jeśli wystąpią warunki X i Y, to należy wykonać akcję Z. Tablice decyzyjne pomagają w systematycznym tworzeniu przypadków testowych, z których każdy składa się z zestawu warunków (wejść) oraz akcji (wyjść), które powinny nastąpić, gdy te warunki zajdą. Są więc narzędziem, które pomaga stworzyć zbiór testów pokrywających logiczne związki między wejściami i wyjściami programu. W niektórych przypadkach umożliwiają również redukcję zbioru testów. tablica decyzyjna, przyczynowo-skutkowa tablica decyzyjna (ang. decision table, cause-effect decision table) – tablica opisująca kombinację wejść i/lub czynników (przyczyn) z odpowiadającymi im wyjściami i akcjami (skutkami), pomocna w projektowaniu przypadków testowych
Tablica decyzyjna jest tabelą, której kolumny oznaczają tzw. reguły decyzyjne5, z których tworzy się przypadki testowe. Wiersze są podzielone na dwie grupy. Pierwsza z nich to grupa warunków. Każdy wiersz tej grupy odpowiada jednemu warunkowi, czyli jednemu wejściu. Dalej znajduje się grupa akcji. Każdy wiersz tej części tabeli odpowiada jednej akcji, czyli jednemu wyjściu. Na przecięciu wierszy i kolumn znajdują się wartości poszczególnych warunków i akcji. Najczęściej są to wartości logiczne P (prawda) oraz F (fałsz), choć zarówno w przypadku warunków, jak i akcji mogą to być również inne wartości, np. liczby czy elementy określonego zbioru. Przykładowa postać tablicy decyzyjnej dla trzech warunków i trzech decyzji jest pokazana w tabeli 8.9. W tablicy tej reguła decyzyjna RD1 opisuje następujące zachowanie programu: jeśli zajdą warunki 1 oraz 3 i nie zajdzie warunek 2, to działanie programu powinno skutkować wykonaniem akcji 1 oraz 2 i nie wykonaniem akcji nr 3. Używając notacji logicznej, regułę tę można zapisać następująco: RD1 (warunek 1 ∧ ~warunek 2 ∧ warunek 3) ⇒ (akcja 1 ∧ akcja 2 ∧ ~akcja 3) Tablicę decyzyjną tworzy się zgodnie z następującym algorytmem: 1) na podstawie specyfikacji programu znajdź warunki oraz określ ich możliwe wartości; 2) na podstawie specyfikacji programu znajdź akcje, które są logicznie powiązane z warunkami oraz określ ich możliwe wartości; 3) wygeneruj wszystkie możliwe kombinacje warunków; 4) dla każdej kombinacji warunków określ, które akcje zachodzą, a które nie; 5) jeśli to konieczne, zminimalizuj tablicę. Tabela 8.9. Ogólna postać tablicy decyzyjnej
Reguły decyzyjne →
RD1
RD2
RD3
…
RDn
warunek1
P
P
F
…
F
warunek2
F
F
P
…
F
W arunki
warunek3
P
F
F
…
F
akcja1
P
F
P
…
F
akcja2
P
P
F
…
F
akcja3
F
F
P
…
F
Akcje
Jeśli mamy 3 warunki i każdy z nich może zachodzić (P) lub nie (F), to możliwych jest 23 = 8 kombinacji wartości tych warunków: PPP, PPF, PFP, PFF, FPP, FPF, FFP, FFF. W ogólności, jeśli mamy n warunków i warunek i-ty może przyjmować w i wartości, to liczba możliwych kombinacji wynosi W przypadku trudności z określeniem wszystkich możliwych kombinacji można posłużyć się prostą metodą drzewa. Dziećmi korzenia są wierzchołki odpowiadające wszystkim możliwym wartościom pierwszego warunku. Dziećmi każdego z nich są wierzchołki odpowiadające wszystkim możliwym wartościom drugiego warunku itd. Gdy zbudujemy całe drzewo (mające razem z korzeniem n + 1 poziomów), wartości leżące na każdej gałęzi biegnącej od korzenia do liścia reprezentują jedną kombinację wszystkich warunków. Rozważmy prosty przykład. Dane niech będą trzy warunki, z czego pierwszy i trzeci mogą przyjmować wartości logiczne prawda (P) lub fałsz (F), natomiast drugi może przyjmować wartość 10, 20 lub 30. Drzewo zbudowane według tych reguł jest pokazane na rysunku 8.6. Identyfikujemy wszystkie ścieżki biegnące od korzenia do liści, otrzymując następujący zbiór 2 ⋅ 3 ⋅ 2 = 12 kombinacji wartości trzech warunków: 1. (P, 10, P); 3. (P, 20, P); 5. (P, 30, P); 7. (F, 10, P); 9. (F, 20, P); 11. (F, 30, P); 2. (P, 10, F); 4. (P, 20, F); 6. (P, 30, F); 8. (F, 10, F); 10. (F, 20, F); 12. (F, 30, F).
Rysunek 8.6. Drzewo możliwych wartości wszystkich warunków Warunki oraz akcje tablicy decyzyjnej stanowią warunki testowe. Elementami pokrycia są reguły decyzyjne. Każda kolumna tablicy odpowiada jednej regule decyzyjnej, a więc i jednemu elementowi pokrycia. Kolejne przypadki testowe powstają przez wybór jednej z niepokrytych dotąd reguł decyzyjnych i przez określenie wejść gwarantujących spełnienie odpowiednich warunków oraz oczekiwanych wyjść na podstawie akcji tej reguły. pokrycie tablicy decyzyjnej (ang. decision table coverage) – odsetek kolumn tablicy decyzyjnej, dla których zdefiniowano i wykonano odpowiadające im przypadki testowe Podczas budowy tablicy decyzyjnej powinniśmy móc określić zachodzenie wszystkich akcji dla dowolnej kombinacji warunków. Jeśli istnieje kombinacja warunków, dla której nie możemy powiedzieć, czy dana akcja ma zajść, czy nie, oznacza to, że albo specyfikacja nie jest kompletna i należy ją poprawić, albo taka kombinacja warunków jest nieosiągalna (co również powinno być wyraźnie wyspecyfikowane w dokumentacji projektowej) – tę ostatnią sytuację omówimy dokładniej w następnym podrozdziale. Podsumowanie metody tablicy decyzyjnej jest przedstawione w tabeli 8.10. Tabela 8.10. Podsumowanie metody tablicy decyzyjnej
8.3.2. Wartości nieistotne i minimalizacja tablicy decyzyjnej Wprowadzimy teraz pojęcie wartości nieistotnych, analizując poniższy przykład. System Egzaminator obsługuje proces egzaminacyjny na uczelni. Na wejściu otrzymuje liczbę punktów uzyskaną przez studenta w postaci przedziału, w którym mieści się wynik (możliwe wartości: 0–50, 51–80, 81–100) oraz informację o tym, czy uzyskał on zaliczenie z ćwiczeń. W odpowiedzi system generuje dwie informacje: o zdaniu lub niezdaniu egzaminu oraz o tym, czy student może przystąpić do egzaminu poprawkowego. System działa według następujących reguł: jeśli wynik egzaminu wynosi 50 punktów lub mniej, to egzamin jest niezdany. W takiej sytuacji student może przystąpić do poprawki tylko wtedy, gdy ma zaliczenie. W przypadku braku zaliczenia, niezależnie od wyniku egzaminu, egzamin jest uznany za niezdany i student nie ma prawa przystąpić do poprawki. Jeśli student ma zaliczenie i liczba punktów jest większa od 50, to zdaje egzamin, przy czym jeśli ma co najwyżej 80 punktów, to otrzymuje prawo do poprawki. W systemie wyróżnić możemy 2 warunki: wynik egzaminu – możliwe wartości: 0–50, 51–80, 81–100; czy ma zaliczenie? – możliwe wartości: P, F (prawda, fałsz) oraz dwie akcje: egzamin zdany? – możliwe wartości: P, F; dopuszczony do poprawki? – możliwe wartości: P, F. Tablica decyzyjna dla tego systemu jest pokazana w tabeli 8.11. Tabela 8.11. Tablica decyzyjna dla programu Egzaminator
Reguły decyzyjne →
RD1 RD2
RD3
RD4 RD5
RD6
wynik eg zaminu
0– 50
51– 80
81– 100
0– 50
51– 80
81–100
czy ma zaliczenie?
P
P
P
F
F
F
czy eg zamin zdany?
F
P
P
F
F
F
czy dopus zczony do poprawki?
P
P
–
F
F
F
W arunki
Akcje
Zauważmy, że specyfikacja nic nie mówi o możliwości dopuszczenia do poprawki w przypadku, gdy student otrzymał co najmniej 81 punktów z egzaminu i ma zaliczenie. Taka sytuacja może być czymś zupełnie normalnym (np. wynik jest na tyle dobry, że student nie musi chcieć go poprawiać, bo i tak otrzyma najwyższą ocenę), ale może też być wynikiem defektu w specyfikacji. Jednak z punktu widzenia testera sytuacja musi być jasna – albo akcja zachodzi, albo nie. Tester musi więc dowiedzieć się od projektanta systemu, jak wygląda zachowanie programu w takim przypadku. Budowa tablicy decyzyjnej, dzięki systematycznej metodzie wyprowadzania reguł decyzyjnych, pozwala w bardzo efektywny sposób odkrywać wiele błędów i niejasności w specyfikacji zanim w ogóle zaczniemy testować aplikację. W naszym przykładzie w dalszych rozważaniach założymy, że dla reguły RD3 akcja „dopuszczony do poprawki” zachodzi, tzn. ma wartość P. Kolejna interesująca właściwość omawianej tablicy polega na tym, że w przypadku braku zaliczenia, niezależnie od tego, jaką wartość przyjmie warunek dotyczący wyniku egzaminu, wartości wszystkich akcji są stałe – akcje „egzamin zdany” oraz „czy dopuszczony do poprawki” zawsze mają wartość F. Oznacza to, że w przypadku, gdy student nie ma zaliczenia, informacja o liczbie uzyskanych z egzaminu punktów nie ma znaczenia dla podjęcia obu decyzji – niezależnie od wyniku egzaminu uznany on będzie za niezdany, a student nie będzie dopuszczony do poprawki.
Widzimy, że podczas tworzenia reguł decyzyjnych może się okazać, iż w pewnych sytuacjach niektóre warunki nie mają żadnego znaczenia – są irrelewantne wobec pozostałych warunków i akcji. Takie wartości nazywać będziemy nieistotnymi (ang. „don’t-care” value) i oznaczać w tablicy decyzyjnej symbolem kreski. Wykorzystanie wartości nieistotnych ogranicza liczbę reguł decyzyjnych, bo nie musimy ich tworzyć dla wszystkich możliwych kombinacji wartości uznanych za nieistotne. Jednocześnie należy pamiętać, że podczas tworzenia przypadków testowych warunki nieistotne muszą przyjmować konkretne wartości, abyśmy mogli uruchomić test i porównać wynik uzyskany z oczekiwanym. Możemy zatem reguły decyzyjne RD4, RD5 i RD6 reprezentować pojedynczą regułą, w której wartość warunku „wynik egzaminu” będzie nieistotna. Tak zminimalizowana tablica jest pokazana w tabeli 8.12. Tabela 8.12. Zminimalizowana tablica decyzyjna dla programu Egzaminator
Reguły decyzyjne →
RD1
RD2
RD3
RD4′
W arunki wynik eg zaminu
0–50
51–80
81–100
–
czy ma zaliczenie?
P
P
P
F
czy eg zamin zdany?
F
P
P
F
czy dopus zczony do poprawki?
P
P
P
F
Akcje
Zauważmy, że minimalizacji nie moglibyśmy dokonać, gdyby którykolwiek z pozostałych warunków lub akcji zmieniał swoją wartość w obrębie minimalizowanych reguł. Opiszmy formalnie warunki, pod którymi można dokonać minimalizacji. Dana niech będzie tablica decyzyjna z warunkami w 1, w 2, …, w k i akcjami a 1, a 2, …, a n przyjmującymi wartości odpowiednio ze zbiorów W1, W2, …, Wk , A 1, A 2, …, A n złożona z p reguł decyzyjnych RD = {d 1, …, d p }, gdzie . Każdą regułę reprezentujemy jako wektor o długości k + n wartości warunków oraz akcji
tej reguły:
gdzie
Zdefiniujmy ponadto operator rzutowania p i(d), który zwraca i-ty element wektora d. Na przykład, jeśli d = (w 1, w 2, a 1, a 2), to p 2(d) = w 2, p 3(d) = a 1 itd. Tablicę decyzyjną możemy zminimalizować względem warunku w i, jeśli
istnieją w niej reguły decyzyjne
takie, że
oraz
Reguły te można zastąpić jedną regułą r taką, że p i (r) = ” – ” (wartość nieistotna), a pozostałe wartości warunków i akcji pozostają niezmienione.
Rysunek 8.7. Ilustracja minimalizacji tablicy decyzyjnej Na rysunku 8.7 przedstawiono ideę minimalizacji tablicy decyzyjnej. Sześć decyzyjnych pokazanych z lewej strony rysunku może zostać
reguł
zminimalizowanych do jednej reguły pokazanej z prawej strony, ponieważ wartość warunku 2 nie wpływa na zmianę pozostałych warunków i akcji. Proces minimalizacji można prowadzić dopóty, dopóki istnieją grupy reguł dla których opisane przesłanki mają zastosowanie do jakiegoś warunku. Regułę minimalizowania można jednak nieco osłabić. W przypadku warunku X przyjmującego wartości liczbowe z pewnego przedziału (np. warunek „wynik egzaminu” dla programu Egzaminator) można dokonać minimalizacji względem X w przypadku, gdy pozostałe warunki i akcje nie zmieniają się tylko dla pewnego podzbioru wartości X. Wtedy powstała reguła decyzyjna przyjmuje w warunku X podzbiór możliwych wartości, będący sumą wartości tego warunku z reguł, do których zastosowano minimalizację. Na przykład, w tablicy decyzyjnej 8.12 możemy zminimalizować reguły RD2 i RD3 względem dwóch wartości warunku 1
(51–80 i 81–100), mimo że reguły te nie obejmują wszystkich możliwych wartości tego warunku. Zminimalizowana tablica jest pokazana w tabeli 8.13. Reguła RD2′ odpowiada zminimalizowanym regułom RD2 i RD3. Czasami kombinacja warunków może być nieosiągalna (ang. infeasible). Takie reguły decyzyjne wymagają specjalnego potraktowania. Jeśli konstrukcja programu uniemożliwia ich wystąpienie, to można je pominąć w dalszej analizie. W przeciwnym wypadku należy przeanalizować, czy da się je wymusić. Jeśli tak, to oznacza to defekt albo w oprogramowaniu, albo w specyfikacji, która powinna określać, jak program ma reagować w takiej sytuacji. W każdym razie tester zawsze powinien przynajmniej spróbować wymusić wystąpienie warunków nieosiągalnych. Tabela 8.13. Jeszcze bardziej zminimalizowana tablica decyzyjna programu Egzaminator
Reguły decyzyjne →
RD1
RD2′
RD4′
warunek 1
0–50
51–100
–
warunek 2
P
P
F
akcja 1
F
P
F
akcja 2
P
P
F
W arunki
Akcje
Rozważmy przykład z rysunku 8.8. Przedstawiono na nim fragment okna opcji programu MS Word. Zauważmy, że nie można mieć aktywnej listy rozwijanej „Domyślny styl akapitu” dla odznaczonej opcji „Włącz klikanie i wpisywanie”. Taki warunek może być podany wprost w specyfikacji, jednak tester powinien sprawdzić, czy da się wymusić naruszenie tych reguł przez odznaczenie opcji przy ciągle aktywnej liście rozwijanej.
Rysunek 8.8. Opcje programu Word
8.3.3. Przykład Program Bankier przyznaje kredyt oraz wysyła klientom oferty kredytu i kart kredytowych zgodnie z następującymi regułami. Jeśli dochody klienta przewyższają 5000 USD/mc i klient nie ma jeszcze karty kredytowej, to system wyśle mu ofertę karty oraz kredytu. Oferta kredytu zostanie wysłana także, gdy
klient nie składał wniosku o kredyt. Jeśli klient składał wniosek o kredyt i jego dochody przekraczają 5000 USD/mc, to system decyduje o przyznaniu kredytu. W przypadku, gdy którykolwiek z warunków nie jest spełniony, odpowiednia akcja nie zostanie wykonana. Chcemy przetestować program Bankier za pomocą tablic decyzyjnych. Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden przedmiot testów, program Bankier. ET1: Bankier. Krok 2. Wyprowadzenie warunków testowych. Warunkami testowymi są warunki i akcje. Mamy następujące 3 warunki: WT1: (W1) dochody miesięczne powyżej 5000 USD; WT2: (W2) posiadanie karty kredytowej; WT3: (W3) złożenie wniosku o kredyt oraz następujące 3 akcje: WT4: (A1) wysłanie oferty karty kredytowej; WT5: (A2) wysłanie oferty kredytu; WT6: (A3) przyznanie kredytu. Stanowią one w sumie sześć warunków testowych, wszystkie dla elementu testowego ET1. Krok 3. Wyprowadzenie elementów pokrycia. Tablica decyzyjna umożliwia zdefiniowanie elementów pokrycia jako jej kolumn reprezentujących reguły decyzyjne. W tabeli 8.14 opisano reguły decyzyjne programu Bankier. Tabela 8.14. Tablica decyzyjna dla programu Bankier
Reguły decyzyjne → W arunki
RD1 RD2 RD3 RD4 RD5 RD6 RD7 RD8
W1 dochody powyżej 5000 US D?
P
P
P
P
F
F
F
F
W2 ma kartę kredytową?
P
P
F
F
P
P
F
F
W3 złożył wnios ek o kredyt?
P
F
P
F
P
F
P
F
A1 wys łanie oferty karty kredytowej
F
F
P
P
F
F
F
F
A2 wys łanie oferty kredytu
F
P
P
P
F
P
F
P
A3 decyzja o przyznaniu kredytu
P
F
P
F
F
F
F
F
Akcje
Zauważmy, że można zminimalizować tę tablicę przez połączenie warunków i to na kilka sposobów. Możemy połączyć ze sobą reguły RD2 i RD6 (względem W1) oraz RD5 i RD7 (względem W2). Zamiast RD2 i RD6 możemy wybrać parę RD6 i RD8 (względem W2). Nie możemy jednak wybrać jednocześnie RD2, RD6 i RD8, ponieważ odpowiednie pary tych reguł można minimalizować względem odmiennych warunków. Wybierzmy pierwszy sposób minimalizacji, otrzymując tablicę decyzyjną 8.15. Reguła RD2′ odpowiada zminimalizowanym RD2 i RD6, a RD5′ jest zminimalizowaną regułą dla RD5 i RD7. Tabela 8.15. Zminimalizowana tablica decyzyjna dla programu Bankier
Reguły decyzyjne →
RD1 RD2′ RD3 RD4 RD5′ RD8
W arunki W1 dochody powyżej 5000 US D?
P
–
P
P
F
F
W2 ma kartę kredytową?
P
P
F
F
–
F
W3 złożył wnios ek o kredyt?
P
F
P
F
P
F
Akcje A1 wys łanie oferty karty kredytowej
F
F
P
P
F
F
A2 wys łanie oferty kredytu
F
P
P
P
F
P
A3 decyzja o przyznaniu kredytu
P
F
P
F
F
F
Krok 4. Zdefiniowanie przypadków testowych. W przypadku oryginalnej tablicy osiem elementów pokrycia odpowiada ośmiu kolumnom. W przypadku tablicy zminimalizowanej sześć elementów pokrycia odpowiada sześciu kolumnom. Zaprojektujmy przypadki testowe dla tej drugiej. Są one przedstawione w tabeli 8.16. Zbiór elementów do pokrycia to {RD1, RD2′, RD3, RD4, RD5′, RD8}. Tabela 8.16. Przypadki testowe dla programu Bankier
W ejścia Id
Oczekiwane wyjścia
Po dochody ma kartę złożył oferta oferta przyznanie el po (USD) kredytową? wniosek? karty kredytu kredytu
PT1 6 000
TAK
TAK
NIE
NIE
TAK
RD
PT2 3 500
TAK
NIE
NIE
TAK
NIE
RD
PT3 5 000.01
NIE
TAK
TAK
TAK
TAK
RD
PT4 12 832
NIE
NIE
TAK
TAK
NIE
RD
PT5 2 004.22
TAK
TAK
NIE
NIE
NIE
RD
PT6 0
NIE
NIE
NIE
TAK
NIE
RD
W przypadku PT2 wartość dochodów była nieistotna, ale ponieważ przypadek testowy musi mieć konkretne wartości wejścia, trzeba było wybrać jakąś wartość. Tak samo postąpiliśmy w przypadku PT5 dla wymagania nr 2. Przy okazji PT3 i PT6 wykorzystaliśmy także wartości brzegowe dla warunku na dochód (0 USD i 5000 USD). Pokrycie tabeli decyzyjnej wynosi 100%, ponieważ dla każdego elementu pokrycia (warunku decyzyjnego) stworzyliśmy wykorzystujący
go test. Gdybyśmy np. dysponowali tylko testami PT1, PT2 i PT3, pokrycie wyniosłoby 3/6 = 50%.
8.4. Grafy przyczynowo-skutkowe 8.4.1. Opis metody Grafy przyczynowo-skutkowe (w skrócie grafy P-S) są metodą podobną do tablic decyzyjnych. Opisują logiczne zależności między warunkami (w terminologii grafów nazywanych przyczynami) a akcjami (nazywanymi skutkami), które mogą pojawić się w systemie. W tym sensie są metodą równoważną, jednak występują między nimi pewne różnice. Po pierwsze, grafy P-S pozwalają dodatkowo na określanie zależności między samymi przyczynami lub między samymi skutkami. Takiej możliwości tablice decyzyjne nie dają. Po drugie, w przypadku dużej liczby przyczyn liczba reguł decyzyjnych (a więc i rozmiar tablicy) rośnie wykładniczo, natomiast rozmiar grafu P-S rośnie liniowo względem liczby przyczyn, skutków oraz relacji między nimi. Hipoteza błędu jest natomiast taka sama jak w przypadku tablic decyzyjnych: błędy są powodowane przez wystąpienie określonej kombinacji czynników. Graf P-S może być przekształcony na tablicę decyzyjną i tester może wyprowadzać przypadki testowe na jej podstawie. Jeśli jednak duża liczba warunków sprawia, że rozmiar tablicy staje się problematyczny, to można zastosować tzw. metodę śledzenia wstecznego, która pozwala znacznie zredukować liczbę przypadków testowych (w sensie liczby kolumn odpowiadającej grafowi tablicy decyzyjnej) do niewielkiego podzbioru. Co więcej, procedura ta jest mechaniczna i może być zautomatyzowana. graf przyczynowo-skutkowy (ang. cause-effect graph) – graficzna reprezentacja logicznych zależności między wejściami (przyczynami) a odpowiadającymi im wyjściami (efektami), które to zależności mogą być wykorzystane do zaprojektowania przypadków testowych Grafy P-S składają się z wierzchołków oraz odpowiednio oznaczanych krawędzi reprezentujących logiczne zależności między elementami grafu. Istnieją trzy typy wierzchołków:
wierzchołki reprezentujące przyczyny, wierzchołki reprezentujące skutki, wierzchołki wewnętrzne. Wierzchołki wewnętrzne odgrywają pomocniczą rolę i służą do upraszczania graficznej postaci grafu oraz reprezentowania bardziej złożonych warunków logicznych, np. wykorzystujących kilka rodzajów operatorów logicznych (standardowe oznaczenia krawędzi umożliwiają wykorzystanie tylko pojedynczych operatorów logicznych). Wierzchołki przyjmują wartości logiczne prawdy lub fałszu (reprezentowane stałymi 1 i 0). Prawda oznacza, że dana przyczyna bądź skutek zachodzi, fałsz – że nie zachodzi. Po ustaleniu wartości logicznych dla przyczyn, na podstawie zdefiniowanych relacji oblicza się wartości logiczne wierzchołków wewnętrznych oraz skutków.
Rysunek 8.9. Elementy składowe grafów przyczynowo-skutkowych Na rysunku 8.9 są przedstawione podstawowe elementy, z których można budować grafy P-S. Pierwsza kolumna opisuje zależności logiczne między wierzchołkami, które nie są jednocześnie przyczynami bądź jednocześnie skutkami. Podstawową relacją jest tożsamość b = a, która mówi, że wartość logiczna wierzchołka b jest taka sama, jak wartość logiczna wierzchołka a. Negacja b = ~a zamienia wartość logiczną na przeciwną: jeśli a jest prawdą, to b jest fałszem i na odwrót: jeśli a jest fałszem, to b jest prawdą. Alternatywa c = a ∨ b powoduje, że c jest prawdą, jeśli przynajmniej jeden spośród wierzchołków a i b jest prawdą. W przeciwnym razie c jest fałszem. Koniunkcja c = a ∧ b powoduje prawdziwość c tylko wtedy, gdy zarówno a, jak i b są prawdą. Operator NOR reprezentuje negację alternatywy, c = ~(a ∨ b): c jest prawdą tylko wtedy, gdy zarówno a, jak i b są fałszem. W przeciwnym razie c jest fałszem. Operator NAND to negacja koniunkcji, c = ~(a ∧ b): c jest prawdą, gdy przynajmniej jedno spośród a i b jest fałszem. W przeciwnym razie c jest fałszem. W środkowej kolumnie rysunku przedstawiono operatory ograniczające, czyli nakładające ograniczenia na przyczyny. Operator E (od ang. exclusive) mówi, że co najwyżej jeden wierzchołek nim związany może być prawdą (wszystkie mogą być też jednocześnie fałszem). Operator I (od ang. inclusive) mówi, że przyczyny nim związane nie mogą być jednocześnie fałszem – przynajmniej jedna z nich musi zachodzić. Operator O (od ang. one and only one) mówi, że dokładnie jedna przyczyna nim związana musi zachodzić, a pozostałe muszą być fałszem. Operator R (od ang. requires) jest operatorem „skierowanym”, oznaczanym strzałką. Mówi on, że jeśli przyczyna a zachodzi, to musi również zachodzić b. Jeśli a nie zachodzi, to b może być zarówno prawdą, jak i fałszem. Operatory E, I, O można stosować do dowolnej liczby przyczyn. Operator R jest dwuargumentowy. Na ostatniej kolumnie rysunku przedstawiono operator M maskowania (od ang. masks), który nakłada ograniczenie na skutki. Tak jak operator R jest on dwuargumentowy i mówi, że jeśli skutek a zachodzi, to skutek b nie może zajść (jest maskowany skutkiem a). Jeśli a nie zachodzi, to b może przyjąć dowolną wartość logiczną. Powiedzieliśmy wcześniej, że metoda grafów P-S jest równoważna metodzie tablic decyzyjnych (a nawet, że jest bardziej ogólna). Jak jednak reprezentować w grafie przyczyny bądź skutki, które zamiast wartości logicznych mogą przyjąć
dowolną liczbę innych wartości (jak np. w warunku „wynik egzaminu” z poprzedniego rozdziału, który to skutek mógł przyjmować trzy możliwe wartości: 0–50, 51–80 i 81–100)? Z pomocą przychodzą operatory ograniczające. Rozważmy przykład skutku „wynik egzaminu”. Może on przyjąć dokładnie jedną spośród trzech wartości, zatem można go modelować za pomocą operatora O dla trzech następujących przyczyn: P1: wynik egzaminu jest między 0 a 50? (prawda lub fałsz); P2: wynik egzaminu jest między 51 a 80? (prawda lub fałsz); P3: wynik egzaminu jest między 81 a 100? (prawda lub fałsz). Gdyby wynik egzaminu nie był przyczyną, ale skutkiem, również moglibyśmy zamodelować taką sytuację, używając operatora maskowania. Zakładamy istnienie trzech skutków S1, S2 i S3 (analogicznych do przyczyn P1, P2, P3 podanych powyżej) z których każdy może zachodzić lub nie, ale zajście jednego z nich powoduje, że wszystkie pozostałe zajść nie mogą. Jedynym dostępnym operatorem dla skutków jest operator maskowania. Musimy go zastosować wielokrotnie – dla każdego zachodzącego skutku wobec wszystkich pozostałych, niezachodzących skutków. Odpowiednie fragmenty grafu P-S modelujące wymienione sytuacje, zarówno dla przyczyn, jak i dla skutków, są pokazane na rys. 8.10. Istnienie przyczyn lub skutków przyjmujących więcej niż dwie możliwe wartości powoduje większe skomplikowanie grafu.
Rysunek 8.10. Wykorzystanie operatorów O i M dla reprezentacji przyczyn i skutków o więcej niż dwóch możliwych wartościach tworzenie grafów przyczynowo-skutkowych, analiza przyczynowo-skutkowa (ang. cause-effect graphing, cause-effect analysis) – czarnoskrzynkowa technika projektowania przypadków testowych, w której przypadki te są tworzone na podstawie grafów przyczynowo-skutkowych [36]
8.4.2. Przekształcanie między grafami P-S i tablicami decyzyjnymi Jeśli logika programu nie powoduje wystąpienia kombinacji warunków, która jest nieosiągalna, to tablice decyzyjne są równoważne grafom P-S, tzn. każdą tablicę decyzyjną da się przekształcić na graf P-S i na odwrót. Jeśli istnieją nieosiągalne kombinacje warunków, to musimy wprowadzić dodatkowe oznaczenie tego faktu w tablicy decyzyjnej oraz specjalny, dodatkowy skutek w grafie P-S. Operacji przekształcenia tablicy na graf można dokonać wprost, reprezentując każdą regułę decyzyjną jako kombinację warunków logicznych w grafie P-S. Jeśli jednak chcemy mieć możliwie mały graf, to tablicę można wcześniej zminimalizować. Z kolei przekształcenie grafu na tablicę polega na kolejnym wybieraniu wszystkich możliwych kombinacji przyczyn, obliczania na podstawie grafu skutków i zapisywaniu wyniku jako kolumny tablicy decyzyjnej. Aby
zredukować liczbę reguł decyzyjnych w tablicy, możemy podczas transformacji grafu na tablicę wykorzystać metodę śledzenia wstecznego omówioną w punkcie 8.4.3. Rozważmy przykład tablicy decyzyjnej pokazanej w tabeli 8.17. Wymagania RD6–RD8 są nieosiągalne, co oznaczamy, wprowadzając specjalny, dodatkowy wiersz w części opisującej akcje. Znak X oznacza, że dane wymaganie jest nieosiągalne. Reguły nieosiągalne również mogą podlegać minimalizacji, przy czym nie bierze się tu pod uwagę akcji (bo ich nie ma). Przekształcamy tablicę na graf P-S. Możemy przekształcać ją wprost, reguła po regule. Na przykład dla reguły RD3, opisującej następującą logikę: W1 ∧ ~W2 ∧ W3
⇒ A 1 ∧ ~A 2 otrzymamy fragment grafu P-S pokazany na rysunku 8.11 z lewej strony. Przyczyny P1, P2, P3 odpowiadają warunkom W1, W2, W3, a skutki S1, S2 – akcjom A1, A2. Nieosiągalną regułę RD6 można przekształcić na graf, wprowadzając dodatkowy wierzchołek N reprezentujący wymagania
nieosiągalne. Odpowiedni fragment grafu jest pokazany na rysunku 8.11 z prawej strony. Tabela 8.17. Przykładowa tablica decyzyjna z wymaganiami nieosiągalnymi
Reguły decyzyjne →
RD1 RD2 RD3 RD4 RD5 RD6 RD7 RD8
W arunki W1
P
P
P
P
F
F
F
F
W2
P
P
F
F
P
P
F
F
W3
P
F
P
F
P
F
P
F
A1
P
P
P
P
F
A2
F
F
F
F
P X
X
X
Akcje
(nieos iąg alne)
Rysunek 8.11. Fragment grafu P-S odpowiadający jednej regule tablicy decyzyjnej Jeśli jednak naniesiemy w ten sposób wszystkie reguły na graf, to będzie on bardzo skomplikowany i nieczytelny. Lepiej jest najpierw zminimalizować tablicę decyzyjną. Przy transformowaniu reguły z wartościami nieistotnymi wierzchołki reprezentujące te wartości pomijamy, gdyż ich wartość nie ma wpływu na żaden skutek w tej regule. Zminimalizowana tabela 8.17 pokazana jest w tabeli 8.18. Tabela 8.18. Przykładowa tablica decyzyjna z wymaganiami nieosiągalnymi po zminimalizowaniu
Reguły decyzyjne →
RD1′
RD5
RD6
RD7′
W1
P
F
F
F
W2
–
P
P
F
W3
–
P
F
–
A1
P
F
A2
F
P X
X
W arunki
Akcje
(nieos iąg alne)
Zredukowaliśmy liczbę reguł do czterech. Reguła RD1′ odpowiada wyrażeniu logicznemu W1 ⇒ A 1 ∧ ~A 2. Jak widać, wykorzystujemy tu tylko jedną przyczynę,
bo skutek nie zależy od W2 ani W3. Pełny graf P-S dla zminimalizowanej tablicy jest pokazany na rysunku 8.12. Wierzchołki v, w, x, z są wierzchołkami wewnętrznymi (pomocniczymi).
Rysunek 8.12. Graf P-S stworzony na podstawie tablicy decyzyjnej Przekształćmy teraz ten graf P-S z powrotem na tablicę decyzyjną. Znajdźmy wartości akcji, jeśli wszystkie przyczyny są prawdziwe. Z identyczności P1 ⇒ S1 otrzymujemy, że S1 jest prawdą. Zauważmy, że ten sam wynik osiągnęlibyśmy, wykorzystując węzeł pośredni v, gdyż, podstawiając wartości prawdy za P1, P2, P3 w regule R: ~((~P1 ∧ P2) ∧ P3), otrzymamy wartość logiczną prawdy. Graf został stworzony na podstawie tablicy decyzyjnej, w której akcje są wyznaczone jednoznacznie na podstawie warunków, dlatego wartość logiczna reguły R zawsze będzie taka, jak wartość logiczna P1. Wartość S2 najłatwiej obliczyć z negacji S2 =
~P1. Skoro P1 jest prawdą, to S2 musi być fałszem. Zauważmy, że nie musimy już obliczać wartości dla skutku N, ponieważ reprezentuje on nieosiągalną kombinację warunków. Skoro pozostałe skutki przyjęły wartości logiczne, oznacza to, że kombinacja warunków P1 = P2 = P3 = 1 jest osiągalna. Skutek N jest więc fałszem. Oczywiście możemy to obliczyć wprost na podstawie reguły dla skutku N: N = w ∨ z = (x ∧~ P3) ∨ (~P1 ∧~ P2) = (~P1 ∧ P2 ∧~P3) ∨ (~P1 ∧~P2). Podstawiając wartości P1 = P2 = P3 = 1, otrzymujemy (~1 ∧ 1 ∧~1) ∨ (0 ∧ 0) = 0 ∨ 0 = 0. Postępując podobnie dla wszystkich pozostałych kombinacji przyczyn, otrzymujemy wartości skutków i w ten sposób tworzymy wszystkie kolumny tablicy decyzyjnej. W tabeli 8.19 podsumowano najważniejsze cechy metody grafów P-S. Kryterium pokrycia i pokrycie jest takie samo jak w przypadku tablic decyzyjnych, ze względu na opisany wyżej związek między tymi dwoma podejściami. pokrycie grafu przyczynowo-skutkowego (ang. cause-effect graph coverage) – odsetek pokrytych testami kolumn tablicy decyzyjnej stworzonej na podstawie tego grafu Tabela 8.19. Podsumowanie metody grafów przyczynowo-skutkowych
Warunki tes towe
Zbiór ws zys tkich przyczyn i s kutków wyznaczonych z analizy pods tawy tes tów
Zbiór ws zys tkich reg uł decyzyjnych, tablicy decyzyjnej Elementy s tworzonej na pods tawie g rafu P-S ; tablica może być pokrycia zminimalizowana wpros t lub przez zas tos owanie metody ś ledzenia ws teczneg o Dla każdej reg uły decyzyjnej (kolumny tablicy) is tnieje co Kryterium najmniej jeden tes t, który wymus za s pełnienie pokrycia odpowiednich warunków tej reg uły decyzyjnej Pokrycie
p = (liczba pokrytych tes tami reg uł decyzyjnych/liczba ws zys tkich reg uł decyzyjnych) × 100%
8.4.3. Metoda śledzenia wstecznego – redukcja liczby testów Dla n przyczyn mamy 2n możliwych kombinacji ich wartości logicznych. Każda kombinacja odpowiada jednemu przypadkowi testowemu, więc rozmiar suity testowej rośnie wykładniczo wraz ze wzrostem liczby przyczyn. Gdy zależy nam na zminimalizowaniu tego rozmiaru, możemy wykorzystać tzw. metodę śledzenia wstecznego. Jej idea jest następująca: dla każdej możliwej wartości logicznej każdego możliwego skutku staramy się znaleźć minimalną sensowną liczbę przypadków testowych, które wymuszają ten warunek. Sensowność wyrażona jest przez reguły śledzenia wstecznego6, które opiszemy dalej. Mówiąc bardzo nieformalnie, metoda stara się pomijać te przypadki testowe, w których szansa na wykrycie defektu jest niewielka. Algorytm generowania przypadków testowych w metodzie śledzenia wstecznego jest opisany w listingu 8.2.
ŚledzenieWsteczne wejście: graf P-S zbiór przyczyn P zbiór skutków S={S1, S2, …, Sn} wyjście: zbiór T={T1, …, Tt} przypadków spełniających kryterium pokrycia śledzenia wstecznego 1 T:=Ø // zbiór pusty 2 for (i:=1 to n) do 3 ustaw Si na wartość logiczną 1 4 dokonaj śledzenia wstecznego dla Si=1 5 niech T′:=zbiór przypadków dla tego śledzenia 6 T:=T ∪ T′ 7 for (t:=1 to |T|) do 8 ustaw wartości przyczyn występujących w Tt 9 10 11 12
przypisz wartości wierzchołkom, które można obliczyć z grafu uwzględniając warunki ograniczające uzupełnij przypadek Tt wartościami przyczyn i skutków
nie występujących w Tt uwzględniając warunki ograniczające 13 return T
Listing 8.2. Algorytm redukcji liczby testów dla grafu P-S Algorytm ten zwraca zbiór przypadków testowych w postaci zestawu t kombinacji przyczyn i skutków, które w prosty sposób można przekształcić do postaci t-kolumnowej tablicy decyzyjnej. Metoda śledzenia wstecznego przeprowadzana w linii 4. algorytmu jest dosyć skomplikowana i dlatego zwykle następujących czterech regułach:
jest
automatyzowana.
Opiera
się
na
Reguła 1. Przy śledzeniu wstecznym przyczyn powodujących ustawienie wierzchołka or na 1 nie należy nigdy ustawiać na 1 więcej niż jednego wejścia do tego wierzchołka7. Reguła 2. Przy śledzeniu wstecznym przyczyn powodujących ustawienie wierzchołka or na 0 należy rozpatrzyć wszystkie sytuacje wywołujące wartości 0 w wierzchołkach do niego wchodzących. Reguła 3. Przy śledzeniu wstecznym wejść do wierzchołka and którego wartość ma być 0, należy uwzględnić wszystkie kombinacje wejść wywołujących tę wartość, przy czym dla tych wejść, które mają wartość 1, nie ma potrzeby uwzględniania przyczyn, które tę wartość wywołują. Dla przypadku, gdy wszystkie wejścia do wierzchołka and są zerowe, należy rozpatrzyć tylko jedną kombinację przyczyn wywołującą wartości 0 w tych wierzchołkach8. Reguła 4. Przy śledzeniu wstecznym wejść do wierzchołka and, którego wartość ma być 1, należy rozpatrzyć wszystkie sytuacje wywołujące wartości 1 w wierzchołkach wchodzących. Wierzchołki nand i nor można wyrazić przy użyciu operatorów alternatywy, koniunkcji i negacji9, dlatego nie musimy dla nich tworzyć dodatkowych reguł. Na przykład: p NAND q ≡ (p ∧ q) ≡ ~p ∨ ~q, więc wierzchołek nand jest po prostu wierzchołkiem or z zanegowanymi wszystkimi wejściami.
Rysunek 8.13. Graficzna ilustracja reguł śledzenia wstecznego
Rysunek 8.14. Graf P-S do którego zastosowano śledzenie wsteczne Zobaczmy, jak działa metoda śledzenia wstecznego w praktyce. Rozważmy graf P-S z rysunku 8.14. Skutek S1 wystąpi, gdy z = 0. Wierzchołek z jest typu and, więc, stosując Regułę 3, musimy rozważyć trzy przypadki dla pary (w, y): (0, 0), (0, 1) oraz (1, 0). Dla (w, y) = (0, 0) musimy, zgodnie z Regułą 3, rozważyć tylko jedną kombinację przyczyn powodującą w = y = 0. Możemy to zrobić, przyjmując P1 = 1, P2 = … = P8 = 0. Ta kombinacja stanowi regułę RD1 w wynikowej tabeli decyzyjnej 8.20. Dla (w, y) = (0, 1), zgodnie z Regułą 3, wystarczy rozważyć jedną kombinację dającą y = 1 (co i tak można uzyskać tylko na jeden sposób, przyjmując P4 = … = P8
= 1) i wykonać śledzenie wsteczne dla w = 0. Jest on typu or, zatem zgodnie z Regułą 2 musimy przyjąć P1 = 1 i v = 0, przy czym dla v = 0 musimy z kolei – na podstawie tej samej reguły – rozważyć wszystkie możliwe kombinacje P2 i P3, których koniunkcja da v = 0. Możliwe przypadki dla (P2, P3) to: (0, 0), (1, 0) oraz (1, 1). Zauważmy, że żaden z nich nie narusza warunku ograniczającego R dla pary (P3, P1). Dostajemy zatem trzy kombinacje przyczyn stanowiące reguły RD2–RD4 w wynikowej tabeli 8.20. Gdy (w, y) = (1, 0), musimy rozważyć tylko jedną kombinację przyczyn dającą w = 1, co może nastąpić tylko wtedy, gdy P1 = 0 i P2 = P3 = 1, ale w takim przypadku nastąpiłoby naruszenie warunku ograniczającego (jeśli P3 = 1, to P1 też musi być 1). Nie musimy dalej rozważać tego przypadku, bo wymagana kombinacja przyczyn P1, P2, P3 jest nieosiągalna. Tabela 8.20. Wynikowa tablica decyzyjna po zastosowaniu metody śledzenia wstecznego dla skutku S1
Reguły decyzyjne →
RD1 RD2 RD3 RD4 RD5 RD6 RD7 RD8
W arunki W1=S 1
0
1
1
1
0
0
0
1
W2=S 2
0
0
1
0
0
0
1
1
W3=S 3
0
0
0
1
0
1
0
1
W4=S 4
0
1
1
1
1
1
1
1
W5=S 5
0
1
1
1
1
1
1
1
W6=S 6
0
1
1
1
1
1
1
1
W7=S 7
0
1
1
1
1
1
1
1
W8=S 8
0
1
1
1
1
1
1
1
1
1
1
1
0
0
0
0
Akcje A1=P1
Algorytm śledzenia wstecznego znajduje zbiory przypadków dla poszczególnych skutków ustawianych na 1. Możemy jednak wykonać śledzenie wsteczne również dla wymuszenia tego, że skutek nie zajdzie. Zróbmy to dla naszego przykładu, wymuszając S1 = 0. Będzie tak, jeśli z = 1. Na mocy Reguły 4 musimy rozważyć wszystkie kombinacje dające w = y = 1. Dla y = 1 sytuacja taka nastąpi tylko wtedy, gdy P4 = … = P8 = 1. Dla w = 1 może tak być wtedy, gdy (P1, v) jest postaci (0, 0), (1, 1) lub (0, 1), ale ta ostatnia jest nieosiągalna ze względu na warunek ograniczający. Dla v = 0, zgodnie z Regułą 3, musimy rozważyć (P2, P3) postaci (0, 0), (0, 1) lub (1, 0), co daje 3 kombinacje. Dla v = 1 jedyna możliwość to P2 = P3 = 1. W sumie dostajemy cztery kombinacje (P1, P2, P3): (0, 0, 0,), (0, 0, 1), (0, 1, 0) oraz (1, 1, 1). Stanowią one reguły RD5–RD8 w tablicy 8.20. Zastosowanie metody śledzenia wstecznego dla uzyskania obu możliwych wartości skutku S1 daje tylko 8 reguł decyzyjnych wobec 120 wszystkich możliwych10. Jak widać, śledzenie wsteczne istotnie zmniejsza liczbę reguł, a więc i przypadków testowych.
8.4.4. Przykład Załóżmy, że w testowanym przez nas programie (nazwijmy go JakiśProgram) można wyróżnić 8 przyczyn P1–P8 i 1 skutek S1. Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden element testów, testowany program: ET1: JakiśProgram. Krok 2. Wyprowadzenie warunków testowych. Warunkami testowymi są przyczyny P1–P8 oraz skutek S1, wszystkie dla ET1. WT1: P1; WT2: P2; WT3: P3; WT4: P4; WT5: P5; WT6: P6; WT7: P7;
WT8: P8; WT9: S1. Krok 3. Wyprowadzenie elementów pokrycia. Elementami pokrycia są reguły tablicy decyzyjnej odpowiadającej grafowi. Możemy zdecydować się na zastosowanie metody śledzenia wstecznego, uzyskując tablicę decyzyjną 8.20. Otrzymujemy 8 elementów pokrycia: EP1: RD1; EP2: RD2; EP3: RD3; EP4: RD4; EP5: RD5; EP6: RD6; EP7: RD7; EP8: RD8. Krok 4. Zdefiniowanie przypadków testowych. Podobnie jak w przypadku tablic decyzyjnych, każdy przypadek odpowiada jednej kolumnie tabeli. Pamiętajmy, że wystąpienia określonych przyczyn i skutków mogą być wynikiem przyjęcia określonych wartości przez różne zmienne. Jeśli na przykład wystąpienie przyczyny P1 (tzn. warunku W1 w tablicy decyzyjnej) zależy od tego, że zmienna stanKonta jest większa od 5000 USD, to np. przypadek testowy odpowiadający regule RD1 musi podawać na wejście programu taką wartość tej zmiennej, która nie spowoduje wystąpienia tej przyczyny (bo WT1 dla PT1 w RD1 wynosi 0). Przykładowo, wartość stanKonta może być równa 4500 USD. Ostatnim etapem jest stworzenie suit i procedur testowych. Odbywa się to dokładnie na takiej samej zasadzie, jak w poprzednio omówionych metodach.
8.5. Testowanie przejść między stanami 8.5.1. Opis metody Dotychczas omówione metody koncentrowały się na „statycznym” aspekcie oprogramowania: po otrzymaniu danych na wejściu interesowało nas wyłącznie
to, co program zwrócił na wyjściu. Testowanie przejść między stanami pozwala przetestować dynamikę działania programu, a narzędziem, które to umożliwia, jest maszyna skończenie stanowa (zwana też automatem skończonym), czyli abstrakcyjny model działania programu. Analiza maszyny stanowej pod kątem różnych kryteriów pokrycia pozwala w automatyczny sposób wygenerować zbiór przypadków testowych weryfikujących zachowanie modelowanego systemu. Diagram maszyny stanowej jest jednym ze standardowych diagramów UML [94]. Maszyna skończenie stanowa składa się ze stanów oraz przejść między nimi. Przejścia te mogą być etykietowane dwoma typami etykiet: zdarzeniami oraz akcjami. Wystąpienie zdarzenia wywołuje przejście z jednego stanu do drugiego. Podczas przejścia może być wykonana akcja. Dodatkowo, przy zdarzeniu możemy dopisać warunek. Jest to wyrażenie logiczne, które musi zostać spełnione, aby wystąpienie zdarzenia spowodowało akcję i przejście do kolejnego stanu. Ogólny model stanów oraz przejść między nimi jest pokazany na rysunku 8.15. Jeśli system znajduje się w stanie 1 i nastąpi wtedy zdarzenie, którym jest etykietowana strzałka idąca od stanu 1 do stanu 2, to maszyna – pod wpływem tego zdarzenia – zmieni stan na 2 oraz dokona akcji. Hipoteza błędu w przypadku modelu maszyny stanowej mówi, że błędy powstają w wyniku nieprawidłowej implementacji sekwencji działań programu (np. błędy w logice biznesowej).
Rysunek 8.15. Ogólny model stanów oraz przejść między nimi. Dwie konwencje zapisu Na rysunku 8.15 przedstawiono dwie stosowane w praktyce konwencje zapisu zdarzeń i akcji. W pierwszej nazwę akcji zapisuje się pod nazwą zdarzenia
i oddziela się je poziomą kreską. W drugiej nazwy zdarzenia i akcji zapisuje się w jednej linii, oddzielając je od siebie ukośnikiem. maszyna skończenie stanowa, automat skończony (ang. finite state machine, finite automaton) – model obliczeniowy składający się ze skończonej liczby stanów i przejść między nimi, możliwie z towarzyszącymi im akcjami [7] diagram stanów (ang. state diagram) – diagram przedstawiający stany, jakie moduł lub system może przyjąć oraz zdarzenia lub okoliczności, które powodują zmiany stanów i/lub wynikają z tych zmian [7] zmiana stanu (ang. state transition) – przejście między dwoma stanami systemu lub modułu Niektóre przejścia mogą być etykietowane tylko zdarzeniem, bez akcji. W takim przypadku wystąpienie zdarzenia skutkuje wyłącznie zmianą stanu. Między dwoma stanami może istnieć więcej niż jedno przejście. Zazwyczaj oznacza się je za pomocą jednej strzałki i zestawu par zdarzenie/akcja. Jak odróżnić zdarzenia od stanów i akcji? Zdarzenia nadchodzą spoza modelowanego systemu i ich pojawienie się skutkuje przejściem między stanami. Mogą to być np. akcje użytkownika takie, jak naciśnięcie klawisza, wybór opcji czy akceptacja formularza. Zdarzenie ma zawsze charakter chwilowy – można na nie patrzeć, jak na zjawisko punktowe lub trwające przez bardzo krótki czas. Stan natomiast ma charakter stały, stabilny, rozciągnięty w czasie. Stanem może być np. „oczekiwanie na zapłatę”, tak, jak w maszynie z rysunku 8.16 opisującej działanie automatu biletowego. Dopóki nie nastąpi jakaś nowa akcja, maszyna cały czas będzie oczekiwała w tym samym stanie (np. oczekiwanie na wrzucenie monety). Akcja jest odpowiedzią systemu, która następuje podczas wykonywania przejścia. Tak jak zdarzenie, akcja może być chwilowa lub trwać przez bardzo krótki czas.
Rysunek 8.16. Maszyna stanowa dla automatu biletowego Dlaczego definiuje się pojęcie warunku, skoro teoretycznie może być on reprezentowany jako zdarzenie? Warunek różni się od zdarzenia tym, że najczęściej występują w nim zmienne „wewnętrzne” systemu, zaimplementowane w programie. Odwołuje się więc do wewnętrznej konfiguracji modelowanego obiektu, w przeciwieństwie do zdarzenia, które, jak już powiedzieliśmy wcześniej, ma najczęściej charakter zewnętrzny wobec systemu. Jeśli maszyna, znajdując się w stanie S, przechodzi do stanu T pod wpływem zdarzenia z, to będziemy zapisywać ten fakt jako S → z → T lub S → z[w] → T, gdy oprócz zdarzenia przejście dodatkowo jest etykietowane warunkiem (spełnionym w momencie przejścia). Jeśli będziemy chcieli zaznaczyć również, iż w momencie przejścia nastąpiła akcja a, to będziemy stosować zapis S → z/a → T lub S → z[w]/a → T. Notacji tej używać będziemy także do reprezentowania ścieżek, czyli sekwencji większej liczby przejść. Zapis S1 → z1/a 1 → S2 → z2/a 2 → … → Sn–1 → zn/a n → Sn oznaczać będzie ciąg przejść ze stanu S1 przez stany S2, S3, …, Sn–1 aż do stanu Sn, pod wpływem kolejnych zdarzeń z1, z2, …, zn, podczas którego zostaną wykonane
akcje a 1, a 2, …, a n. Jeśli nie będą interesować nas zdarzenia ani akcje, a jedynie to, przez jakie stany przechodziliśmy, idąc daną ścieżką, będziemy to oznaczać w następujący sposób: S1 S2 S3 … Sn Długość ścieżki to liczba jej przejść. Na przykład ścieżka S1 → S2 → S3 ma długość 2, ponieważ występują w niej 2 przejścia. Ścieżka S1 złożona z pojedynczego stanu ma długość 0. W maszynie stanowej wyróżnić możemy trzy typy stanów, których graficzne reprezentacje są pokazane na rysunku 8.17: stan początkowy – stan, od którego maszyna rozpoczyna swoje działanie; reprezentuje on stan programu w momencie jego uruchomienia; stan końcowy – jeśli maszyna znajdzie się w tym stanie (osiągnie go), to kończy swoje działanie; stan wewnętrzny – stan, który nie jest ani początkowy, ani końcowy.
Rysunek 8.17. Graficzna reprezentacja różnych typów stanów W maszynie stanowej może być tylko jeden stan początkowy i jeden lub więcej stanów końcowych. Powiemy, że maszyna jest syntaktycznie poprawna, jeśli każdy stan da się osiągnąć ze stanu początkowego S0, to znaczy, gdy dla dowolnego stanu S tej maszyny istnieje taka sekwencja zdarzeń z1, z2, …, zn, że zachodzi
Poprawność syntaktyczną należy odróżnić od semantycznej. Może się zdarzyć, że istniejąca sekwencja przejść między stanami maszyny nie jest możliwa do uzyskania w modelowanym systemie. Rozważmy maszynę z rysunku 8.16 modelującą działanie automatu biletowego. Ma on dwie opcje: sprzedaż biletów jednorazowych lub wyświetlenie rozkładu jazdy. Użytkownik w stanie S1 wybiera, czy chce kupić bilet, czy wyświetlić rozkład jazdy. Jeśli wybierze zakup biletu, to program przejdzie do stanu S2 i otworzy się wrzutnik na monety. Po wrzuceniu
odpowiedniej kwoty automat drukuje bilet i użytkownik może dokupić kolejne bilety lub zakończyć korzystanie z automatu. Analogicznie, użytkownik może zażądać wyświetlenia dowolnej liczby rozkładów jazdy przez wybór określonych linii. Załóżmy, że program jest tak skonstruowany, że z chwilą pierwszego wyboru (bilet lub rozkład) kolejne wybory muszą być takie same lub polegać na anulowaniu wyboru bądź zakończeniu korzystania z automatu. Zauważmy, że chociaż według modelu ścieżka S1 → KupBilet → S2 → Zapłać → S1 → WybierzLinię jest syntaktycznie poprawna, to nie będzie się jej dało uzyskać w systemie, bo w ramach jednej sesji można wyłącznie albo kupować bilety, albo przeglądać rozkłady jazdy. Techniki projektowania przypadków testowych dla maszyny stanowej, oparte na kryteriach omówionych w punkcie 8.5.3 pozwalają efektywnie wykrywać tego typu problemy. Sam fakt wystąpienia takiego problemu nie musi oznaczać, że model jest zły. Maszyna stanowa opisuje po prostu, co ma się zdarzyć, jeśli jest w danym stanie i wystąpi określone zdarzenie. Nie uwzględnia ona tego, że w niektórych sytuacjach pewne przejścia, choć istnieją w modelu, nie są możliwe do zrealizowania w rzeczywistym programie. Można temu zaradzić przez wzbogacenie modelu, stosując dodatkowo formalne ograniczenia, wyrażane przez warunki czy też np. za pomocą języka OCL [68].
Rysunek 8.18. Maszyna stanowa z warunkami na niektórych przejściach Warunki można zastosować tak, jak to pokazano na rysunku 8.18. Jest to zmodyfikowana maszyna stanowa dla automatu biletowego z rysunku 8.16. Przed rozpoczęciem działania maszyny zmienna tryb jest ustawiana na pusty łańcuch znaków. Przejście ze stanu S1 do stanu S2 może nastąpić tylko, jeśli wystąpi zdarzenie KupBilet i jednocześnie zmienna tryb będzie pusta (będzie tak przy pierwszym przejściu) lub ustawiona na „b”. Dodatkową akcją, oprócz otwarcia wrzutnika, jest ustawienie zmiennej tryb na „b”. Dzięki temu zapamiętujemy pierwszy wybór użytkownika (kupno biletu) i maszyna nie zezwoli w przyszłości na przejście ze stanu S1 do S3, ponieważ zmienna tryb będzie ustawiona cały czas na „b”. Analogicznie, wybór wyświetlenia rozkładu jako pierwszego żądania użytkownika skutkować będzie ustawieniem zmiennej tryb na „r” i maszyna nie umożliwi przejścia z S1 do S2. Z formalnego punktu widzenia maszyna stanowa to siódemka M = (S, Z, A, f, λ, S0, T), gdzie: S jest skończonym, niepustym zbiorem stanów; Z jest skończonym, niepustym zbiorem zdarzeń;
A jest skończonym zbiorem akcji; f: S × Z → S jest funkcją (częściową) opisującą przejścia między stanami; λ: S × Z → A jest funkcją (częściową) opisującą występowanie akcji przy przejściach; S0 ∈ S jest stanem początkowym; T ⊂ S jest niepustym zbiorem stanów końcowych. Czasami funkcje f i λ reprezentuje się za pomocą jednej funkcji ∆: S × Z → S × A.
8.5.2. Tabelaryczne reprezentacje przejść Maszynę stanową można zdefiniować nie tylko za pomocą reprezentacji graficznej, lecz także przy użyciu tabel. Opiszemy dwie takie metody. Przydadzą się one podczas konstrukcji przypadków testowych dla różnych kryteriów pokrycia omówionych w punkcie 8.5.3. Metoda pierwsza to proste przekształcenie diagramu maszyny na tabelę. Dla maszyny z rysunku 8.16 jej postać tabelaryczna jest pokazana w tabeli 8.21. Tabela 8.21. Tabelaryczna postać maszyny stanowej automatu biletowego
Stan
Zdarzenie
Akcja
Nowy stan
S1
KupBilet
OtwórzWrzutnik
S2
S1
WybierzLinię
Wyś wietlRozkład
S3
S1
Koniec
–
S4
S2
Zapłać
DrukujBilet
S1
S2
Anuluj
ZwróćPieniądze
S4
S3
ZmieńLinię
Czyś ćEkran
S1
S3
Anuluj
–
S4
Tabela składa się z czterech kolumn. W pierwszej zapisujemy stan wyjściowy. Druga opisuje zdarzenie, jakie może nastąpić podczas przebywania w tym stanie. Trzecia i czwarta opisują odpowiednio akcję, jaka zajdzie oraz stan docelowy,
w jakim się znajdziemy, jeśli dane zdarzenie nastąpi. Tabela dla maszyny stanowej zawsze ma tyle wierszy, ile zdefiniowanych przejść w maszynie. Jest ona również tabelarycznym opisem funkcji ∆. Zwróćmy uwagę, że w tabeli opisano wyłącznie przejścia poprawne. Z punktu widzenia testera interesujące jest także sprawdzanie przejść niepoprawnych (tzw. testowanie negatywne). Aby zdefiniować pełną tabelę maszyny stanowej, uwzględniającą zarówno poprawne, jak i niepoprawne przejścia, musimy zidentyfikować najpierw wszystkie możliwe stany i zdarzenia. Mamy: S ={S1, S2, S3, S4} Z = {KupBilet, WybierzLinię, ZmieńLinię, Zapłać, Anuluj, Koniec} Tabela 8.22. Pełna tabela przejść maszyny dla automatu biletowego
Stan
Zdarzenie
Akcja
Nowy stan
S1
KupBilet
OtwórzWrzutnik
S2
S1
WybierzLinię
Wyś wietlRozkład
S3
S1
ZmieńLinię
(niezdefiniowane)
(niezdefiniowany)
S1
Zapłać
(niezdefiniowane)
(niezdefiniowany)
S1
Anuluj
(niezdefiniowane)
(niezdefiniowany)
S1
Koniec
–
S4
S2
KupBilet
(niezdefiniowane)
(niezdefiniowany)
S2
WybierzLinię
(niezdefiniowane)
(niezdefiniowany)
S2
ZmieńLinię
(niezdefiniowane)
(niezdefiniowany)
S2
Zapłać
DrukujBilet
S1
S2
Anuluj
ZwróćPieniądze
S4
S2
Koniec
(niezdefiniowane)
(niezdefiniowany)
S3
KupBilet
(niezdefiniowane)
(niezdefiniowany)
S3
WybierzLinię
(niezdefiniowane)
(niezdefiniowany)
S3
ZmieńLinię
Czyś ćEkran
S1
S3
Zapłać
(niezdefiniowane)
(niezdefiniowany)
S3
Anuluj
–
S4
S3
Koniec
(niezdefiniowane)
(niezdefiniowany)
S4
KupBilet
(niezdefiniowane)
(niezdefiniowany)
S4
WybierzLinię
(niezdefiniowane)
(niezdefiniowany)
S4
ZmieńLinię
(niezdefiniowane)
(niezdefiniowany)
S4
Zapłać
(niezdefiniowane)
(niezdefiniowany)
S4
Anuluj
(niezdefiniowane)
(niezdefiniowany)
S4
Koniec
(niezdefiniowane)
(niezdefiniowany)
Rozważamy teraz wszystkie kombinacje elementów S i Z. Ich liczba to liczba stanów pomnożona przez liczbę zdarzeń. W naszym przypadku będzie 4 × 6 = 24 kombinacji. Pełną tabelę przejść (tzw. tablicę stanów) konstruujemy dokładnie tak samo jak poprzednio, z jedną różnicą – dla kombinacji reprezentujących niepoprawne przejścia w kolumnach opisujących akcje i stany docelowe wpisujemy wartość „niezdefiniowane”. W tabeli 8.22 jest pokazana pełna tabela przejść dla automatu biletowego. tablica stanów (ang. state table) – tablica, która dla każdego stanu zestawia przejścia z tego stanu z każdym możliwym zdarzeniem. Obrazuje zarówno dozwolone, jak i niedozwolone przejścia Z formalnego punktu widzenia, tablica stanów jest równoważna częściowej funkcji przejść f rozszerzonej do funkcji pełnej, tzn. zdefiniowanej dla dowolnej pary (stan, zdarzenie).
8.5.3. Kryteria pokrycia dla maszyny stanowej
W przypadku maszyny stanowej istnieje kilka kryteriów pokryć, gdyż można wyróżnić w niej różne warunki testowe. Zanim je omówimy, zdefiniujmy maszynę stanową, którą wykorzystamy jako przykład ilustrujący te kryteria. Maszyna pokazana na rysunku 8.19 składa się z 6 stanów {P, Q, R, S, T ,U} oraz 10 przejść etykietowanych zdarzeniami {a, b, c, d, e, f, g, h, i, j}. Stan P jest początkowy, a T – końcowy.
Rysunek 8.19. Przykładowa maszyna stanowa Najprostszą metodą pokrycia maszyny stanowej jest wymóg, aby przejść przez każdy stan i wykorzystać każde przejście. Nazywa się je pokryciem 0-przełączeń (ang. 0-switch coverage). W takim przypadku zbiór elementów pokrycia to stany i przejścia. Jest to najprostsze i zarazem najsłabsze kryterium. Zauważmy, że spełnienie tego kryterium oznacza również wykonanie wszystkich możliwych akcji. Gdybyśmy do zbioru elementów pokrycia nie dodali stanów, to dla
maszyny złożonej z 1 stanu i 0 przejść kryterium byłoby spełnione z definicji, w szczególności dla pustego zbioru testów (bo w takiej maszynie nie ma żadnej krawędzi). Stosując kryterium 0-przełączeń dla maszyny z rysunku 8.19, otrzymujemy zbiór elementów pokrycia złożony z 6 stanów i 10 krawędzi: EP={P, Q, R, S, T, U, PR, RQ, QR, QU, UR, UT, RS, SP, ST, SS} Zbiór ten możemy pokryć na przykład następującymi dwoma przypadkami testowymi: PT1: P → a → R → b → Q → c → R → b → Q → d → U → e → R → g → S → j → S → i → T PT2: P → a → R → g → S → h → P → a → R → b → Q → d → U → f → T Pierwszy przypadek powoduje ciąg akcji a, b, c, b, d, e, g, j, i i pokrywa wszystkie stany oraz krawędzie PR, RQ, QR, QU, UR, RS, SS, ST. Drugi powoduje ciąg akcji a, g, h, a, b, d, f i dodatkowo pokrywa krawędzie SP oraz UT. Zauważmy, że aby spełnić kryterium pokrycia stanów i przejść musimy stworzyć co najmniej dwa przypadki, ponieważ stan T jest końcowy (nie da się z niego nigdzie przejść), a prowadzą do niego dwie krawędzie – od stanów S i U. W jednym przypadku testowym możemy ponadto wielokrotnie odwiedzać te same stany i krawędzie. Zwykle im dłuższy ciąg akcji, tym bardziej skomplikowany i trudniejszy do realizacji przypadek. Należy więc znaleźć złoty środek między długością ścieżek a liczbą przypadków testowych, gdyż jedna wartość jest odwrotnie proporcjonalna do drugiej. Zachowanie systemu może zależeć od tego, jaki był poprzedni stan. Drugie kryterium pokrycia, zwane pokryciem 1-przełączeń (ang. 1-switch coverage) bierze to pod uwagę i wymaga, aby pokryć wszystkie możliwe ścieżki o długości co najwyżej 2, to znaczy wszystkie stany, przejścia oraz pary przejść. Stan reprezentuje ścieżkę o długości 0. Forma tego kryterium może wydać się dosyć dziwna – dlaczego ścieżki o długości co najwyżej, a nie dokładnie 2? Chodzi o to, aby formalnie kryterium to subsumowało kryterium 0-przełączeń. Jeśli bowiem wymagalibyśmy wyłącznie pokrycia ścieżek o długości dokładnie 2, to w sytuacji maszyny o dwóch stanach i jednym przejściu między nimi kryterium byłoby spełnione przez pusty zbiór testowy, bo taka maszyna w ogóle nie ma ścieżek o długości większej niż 1. Identyfikacja elementów pokrycia reprezentujących stany oraz ścieżki o długości 1 jest trywialna – są to po prostu wszystkie stany i wszystkie przejścia.
W przypadku identyfikacji ścieżek o długości 2 dobrze jest zastosować systematyczną metodę ich znajdowania, aby uniknąć przeoczenia niektórych ścieżek. Metoda polega najpierw na wypisaniu wszystkich możliwych ścieżek o długości 1 (czyli 0-przełączeń) dla każdego możliwego stanu. W naszym przykładzie z rysunku 8.19 ścieżki te są zebrane w tabeli 8.23. Dla T nie ma żadnych przejść, gdyż jest on stanem końcowym. W sumie ścieżek powinno być dokładnie tyle, ile maszyna ma przejść, czyli 10. W drugim kroku metody bierzemy po kolei te ścieżki, dla każdej z nich patrzymy, jakim stanem X jest zakończona, a następnie tworzymy zbiór ścieżek o długości 2 przez doklejenie na końcu rozważanej ścieżki wszystkich możliwych końców ścieżek o długości 1 rozpoczynających się stanem X. Ścieżki te mamy wypisane w wierszu tabeli odpowiadającym stanowi X. Tabela 8.23. Krawędzie wychodzące z poszczególnych stanów
Stan
Ścieżki o długości 1 wychodzące z danego stanu
P
PR
Q
QR, QU
R
RQ, RS
S
SS, SP, ST
T
Ø
U
UR, UT
Popatrzmy, jak metoda działa w praktyce. Rozważmy ścieżkę PR. Kończy się ona stanem R. W wierszu tabeli odpowiadającym R znajdujemy ścieżki RQ i RS. Doklejamy więc do PR stany Q oraz S, otrzymując dwie ścieżki o długości 2: PRQ i PRS. Tak samo postępujemy dla każdej ścieżki o długości 1. W rezultacie otrzymujemy zbiór ścieżek o długości 2 zebranych w tabeli 8.24 (w ostatniej kolumnie). Tabela 8.24. Obliczanie ścieżek o długości 2
Ścieżka o długości 1
Możliwe kontynuacje
Zbiór wynikowych ścieżek o długości 2
PR
RQ, RS
PRQ, PRS
QR
RQ, RS
QRQ, QRS
QU
UR, UT
QUR, QUT
RQ
QR, QU
RQR, RQU
RS
SS, SP, ST
RSS, RSP, RST
SS
SS, SP, ST
SSS, SSP, SST
SP
PR
SPR
ST
Ø
Ø
UR
RQ, RS
URQ, URS
UT
Ø
Ø
Otrzymaliśmy 17 ścieżek o długości 2. Zauważmy, że w tym przykładzie pokrycie ścieżek o długości 2 zapewni jednocześnie pokrycie ścieżek o długości 1 oraz 0. Nasz zbiór elementów pokrycia może więc zawierać wyłącznie ścieżki o długości 2: EP={PRQ, PRS, QRQ, QRS, QUR, QUT, RQR, RQU, RSS, RSP, RST, SSS, SSP, SST, SPR, URQ, URS} Istnieją 3 ścieżki o długości 2 kończące się w T, dlatego zbiór testów spełniający pokrycie 1-przełączeń musi mieć co najmniej 3 testy. Do ich zaprojektowania można wykorzystać tzw. graf de Brujina11 [95]. Jego wierzchołkami są wszystkie ścieżki o długości 2, a między wierzchołkami X1 X2 X3 oraz Y1 Y2 Y3 krawędź występuje tylko wtedy, gdy ścieżki te „zachodzą” na siebie dwoma elementami, to znaczy gdy X2 = Y1 ∧ X3 = Y2. Graf de Brujina dla naszego automatu jest pokazany na rysunku 8.20.
Rysunek 8.20. Graf de Brujina dla ścieżek długości 2 Każda ścieżka testowa rozpoczyna się w wierzchołku zaczynającym się od stanu początkowego (w naszym przypadku P; wierzchołki te oznaczone są na biało) i kończy w wierzchołku kończącym się stanem końcowym (u nas jest to T; stany te są oznaczone podwójną obwódką). Kryterium 1-przełączeń będzie spełnione, jeśli pokryjemy ścieżkami testowymi wszystkie wierzchołki grafu de Brujina. Na przykład możemy stworzyć następujące 3 przypadki testowe: PRSSSPRSPRST (pokrywa PRS, RSS, SSS, SSP, SPR, RSP, RST); PRQRSPRQURSST (pokrywa dodatkowo PRQ, RQR, QRS, RQU, QUR, URS, SST); PRQRQURQUT (pokrywa dodatkowo QRQ, URQ, QUT). Wykorzystanie grafu de Brujina pozwala na kontrolowanie długości oraz liczby ścieżek. Pokrywając go, możemy manipulować ścieżkami, wydłużać je, skracać lub dodawać w razie potrzeby, tak, aby uzyskać zbiór ścieżek testowych satysfakcjonujący nas pod względem rozmiaru i długości. Kryteria 0- i 1-przełączeń można uogólnić na tzw. kryterium N-przełączeń dla N ≥ 0. Rozważanymi elementami pokrycia są wtedy sekwencje przejść (ścieżki) o długości co najwyżej N + 1. Tak jak poprzednio, sformułowanie „co najwyżej” jest użyte z powodów formalnych, aby kryterium N-przełączeń subsumowało kryterium K-przełączeń dla każdego 0 ≤ K < N.
pokrycie N-przełączeń (ang. N-switch coverage) – odsetek sekwencji przejść o długości co najwyżej N + 1 w maszynie stanowej, które były wykorzystane podczas wykonywania zestawu testowego [96] Metryki pokrycia N-przełączeń są też nazywane metrykami pokrycia Chowa (ang. Chow’s coverage metrics), od nazwiska ich twórcy, prof. Chowa. Ostatnim kryterium pokrycia jest tzw. pokrycie „wszystkich przejść” (ang. alltransitions). Jest to kryterium wymagające, aby przetestować wszystkie przejścia, zarówno poprawne, jak i niepoprawne. Należy zaznaczyć, że pokrycie Nprzełączeń dotyczy wyłącznie sekwencji przejść poprawnych. W tabeli 8.25 podsumowano metodę grafów przyczynowo-skutkowych. Tabela 8.25. Podsumowanie metody grafów przyczynowo-skutkowych
8.5.4. Diagram maszyny stanowej w UML W praktyce testerzy mają najczęściej do czynienia z maszynami stanowymi modelowanymi w języku UML. Notacja ta dopuszcza w budowie maszyny znacznie więcej elementów niż te, które omówiliśmy, w szczególności: stany złożone – jeden stan może reprezentować podmaszynę stanową; rozbudowaną strukturę stanu – oprócz nazwy stan może zawierać sekcję czynności wewnętrznych, sekcję przejść wewnętrznych oraz – w przypadku stanów złożonych – sekcję dekompozycji; obszary współbieżne – w ramach jednej maszyny niektóre ze stanów mogą być aktywowane współbieżnie; bogatą symbolikę opisującą scalanie, rozwidlenie, punkty węzłowe, decyzje, punkty zniszczenia.
Wzbogacona symbolika pozwala opisywać maszynami stanów np. działanie protokołów czy procesów biznesowych, ale należy pamiętać, że im bogatszy opis maszyny, tym z reguły trudniejszy proces testowania. Na przykład jeśli maszyna wykorzystuje obszary współbieżne, to należy podczas wykonywania przypadków testowych monitorować, czy odpowiednie sekwencje rzeczywiście wykonują się współbieżnie, a nie sekwencyjnie. Jeśli maszyna opisuje proces biznesowy, to również pewne czynności mogą odbywać się równolegle. Jeśli przypadek testowy obejmuje przejście przez stan złożony, to należy sprawdzić poprawność działania podmaszyny stanowej w tym stanie itd.
8.5.5. Przykład Redakcja czasopisma internetowego stosuje aplikację Czasopismo wspomagającą proces recenzowania i publikowania artykułów. Autor może wysłać swój artykuł redaktorowi, który dokonuje przeglądu wstępnego i decyduje, czy odrzucić artykuł (jeśli np. nie wpisuje się w profil czasopisma), czy też przekazać go do recenzji. Recenzent może skontaktować się z autorem i zażądać poprawek12. Po ich naniesieniu autor ponownie wysyła artykuł do redaktora. Ten przekazuje go recenzentowi. Recenzent w końcu podejmuje decyzję, czy zaakceptować artykuł do druku i przekazuje ją redaktorowi. Redaktor na podstawie recenzji decyduje o publikacji lub odrzuceniu artykułu. Schemat ten, z punktu widzenia cyklu życia artykułu, jest opisany maszyną stanową na rysunku 8.21. Chcemy przetestować aplikację, wykorzystując ten model.
Rysunek 8.21. Maszyna stanowa dla procesu opisującego pracę redakcji czasopisma Krok 1. Określenie przedmiotu i elementów testów. Mamy jeden przedmiot testów – aplikację Czasopismo: ET1: Czasopismo. Krok 2. Wyprowadzenie warunków testowych. Warunkami testowymi są stany oraz przejścia maszyny stanowej. Zauważmy, że ze stanu S3 do S2 biegną dwa przejścia – jedno wywołane zdarzeniem „odrzuć”, drugie zdarzeniem „akceptuj”. Analogicznie, mamy dwa przejścia ze stanu S2 do S4, wywołane odpowiednio zdarzeniami „publikuj” i „odrzuć”. Należy o tym pamiętać, ponieważ podczas pokrywania przejść trzeba będzie przejść przynajmniej dwa razy między stanem S3 i S2 (za każdym razem innym przejściem) oraz przynajmniej dwa razy między S2 i S4. Poniższa lista obejmuje wszystkie warunki testowe: WT1: S1 (pisanie); WT2: S2 (przegląd); WT3: S3 (recenzja); WT4: S4 (koniec); WT5: S1 → WY → S2 (wysłanie); WT6: S2 → PR → S3 (przekazanie); WT7: S3 → ZP → S1 (żądanie poprawy); WT8: S3 → AK → S2 (akceptuj); WT9: S3 → RN → S2 (recenzja negatywna); WT10: S2 → PU → S4 (publikuj i zakończ); WT11: S2 → OD → S4 (odrzuć i zakończ). Krok 3. Wyprowadzenie elementów pokrycia. Elementy pokrycia zależą od przyjętej metody testowania. Jeśli stosujemy kryterium pokrycia 0-przełączeń, to elementami pokrycia są wprost warunki testowe opisane w poprzednim kroku. Jeśli stosujemy kryterium 1-przełączeń, elementami pokrycia będą wszystkie ścieżki o długości 2 wypisane poniżej. Ścieżki te pokrywają jednocześnie wszystkie
przejścia i stany, więc nie musimy ich explicite dodawać do zbioru elementów pokryć. EP1: S1 → WY → S2 → PR → S3; EP2: S1 → WY → S2 → PU → S4; EP3: S1 → WY → S2 → OD → S4; EP4: S2 → PR → S3 → ZP → S1; EP5: S2 → PR → S3 → RN → S2; EP6: S2 → PR → S3 → AK → S2; EP7: S3 → ZP → S1 → WY → S2; EP8: S3 → RN → S2 → PR → S3; EP9: S3 → RN → S2 → PU → S4; EP10: S3 → RN → S2 → OD → S4; EP11: S3 → AK → S2 → PR → S3; EP12: S3 → AK → S2 → PU → S4; EP13: S3 → AK → S2 → OD → S4. Krok 4. Zdefiniowanie przypadków testowych. Dla kryterium pokrycia 0przełączeń przykładowy zestaw przypadków testowych jest pokazany w tabeli 8.26. Dla tego kryterium elementami pokrycia są wprost warunki testowe WT1– WT11. Pogrubioną czcionką zaznaczono elementy pokryte po raz pierwszy. Tabela 8.26. Przypadki testowe dla kryterium 0-przełączeń dla programu Czasopismo
Id
Ścieżka
Oczekiwany ciąg akcji
PT1
S1 → WY → S2 → PR → S3 → RN → S2 → OD → S4
Udos tRedaktorowi, Udos tRecencentowi, W T 1, W T 2, W T 3, W T 4, Udos tRedaktorowi, W T 5, W T 6, W T 9, W T 11 ZakończProces
S1 → WY → S2 → PR → S3 → ZP → S1 → WY →
Udos tRedaktorowi, Udos tRecenzentowi, WT1, WT2, WT3, WT4, Udos tAutorowi, WT5, WT6, W T 7, W T 8,
Pokryte elementy
PT2 S2 → PR → S3 → AK → S2 → PU → S4
Udos tRedaktorowi, W T 10 Udos tRecenzentowi, Udos tRedaktorowi OpublikujNaS tronie
Dla kryterium pokrycia 1-przełączeń możemy zdefiniować przypadki testowe tak, jak pokazano to w tabeli 8.27. Elementami pokrycia dla tego kryterium są ścieżki o długości 2, a więc elementy EP1–EP13 opisane wcześniej (elementy te w szczególności pokrywają wszystkie przejścia oraz stany, więc nie musimy ich dodawać do zbioru elementów pokryć). Tabela 8.27. Przypadki testowe dla kryterium 1-przełączeń dla programu Czasopismo
Id
Ścieżka
Oczekiwany ciąg akcji
Pokryte elementy
Udos tRedaktorowi, S1 → WY → S2 → PR → Udos tRecencentowi, S3 → PT1 Udos tAutorowi, EP1, EP2, EP4, EP7 ZP → S1 → WY → S2 → Udos tRedaktorowi, PU → S4 OpublikujNaS tronie PT2
S1 → WY → S2 → OD → S4
Udos tRedaktorowi, ZakończProces
EP3
Udos tRedaktorowi, Udos tRecenzentowi S1 → WY → S2 → PR → Udos tRedaktorowi, S3 → Udos tRecenzentowi, RN → S2 → PR → S3 → Udos tRedaktorowi AK → S2 → Udos tRecenzentowi EP1, EP4, EP5, EP6, PT3 R → S3 → ZP → S1 → EP7, EP8, EP9, EP11 Udos tAutorowi, WY → S2 → Udos tRedaktorowi, PR → S3 → RN → S2 → Udos tRecenzentowi, PU → S4
Udos tRedaktorowi, OpublikujNaS tronie Udos tRedaktorowi, S1 → WY → S2 → PR → Udos tRecenzentowi, PT4 S3 → EP1, EP6, EP13 Udos tRedaktorowi, AK → S2 → OD → S4 ZakończProces Udos tRedaktorowi, S1 → WY → S2 → PR → Udos tRecenzentowi, PT5 S3 → EP1, EP6, EP12 Udos tRedaktorowi, AK → S2 → PU → S4 OpublikujNaS tronie Udos tRedaktorowi, S1 → WY → S2 → PR → Udos tRecenzentowi, PT6 S3 → EP1, EP5, EP10 Udos tRedaktorowi, RN → S2 → OD → S4 ZakończProces Tester przy okazji tworzenia przypadków testowych może zwrócić uwagę na pewne problemy w logice biznesowej aplikacji Czytelnik: w PT1 publikacja artykułu następuje przed sprawdzeniem poprawek przez recenzenta (jest to możliwe, ale warto spytać analityka biznesowego, czy w każdej sytuacji jest to dopuszczalne); w PT3 występuje wiele problemów. Przy pokryciu EP8 następuje natychmiastowe odesłanie recenzentowi z powrotem artykułu, który przed chwilą recenzował. Przy pokryciu EP6 recenzent odrzuca artykuł, który wcześniej (przy EP5) zaakceptował. Przy pokryciu EP11 redaktor znów odsyła recenzentowi zaakceptowany już przez niego artykuł. Przy pokryciu EP4 recenzent żąda poprawek od autora, choć wcześniej zaakceptował artykuł. I na końcu ścieżki, przy pokryciu EP9, redaktor publikuje artykuł, choć ostatnia recenzja jest negatywna; w PT4 recenzent akceptuje artykuł, a redaktor go odrzuca (jest to dopuszczalne, choć należy upewnić się, czy każda tego typu sytuacja jest poprawna);
gdyby element pokrycia EP2 sam w sobie stanowił przypadek testowy (tzn. ścieżkę S1 → WY → S2 → PU → S4, oznaczałoby to, że artykuł jest opublikowany bez wysłania go do recenzji. Należy skonsultować się z analitykiem biznesowym, czy taka sytuacja jest dopuszczalna. Sama analiza ścieżek długości 2 może wykryć większość z wymienionych problemów. Część z nich ujawnia się dopiero przy konstrukcji większych (dłuższych) przypadków testowych.
8.6. Kategoria-podział (Category-Partition) 8.6.1. Opis metody Metoda kategoria-podział (K-P) została zaproponowana przez Ostranda i Blacera w 1988 roku [97]. Jest to wartościowa technika testowania opartego na specyfikacji, jeśli idzie o zarówno testy funkcjonalne, jak i testy API. Rozszerza ona znacznie możliwości takich technik, jak podział na klasy równoważności czy analiza wartości brzegowych. Omówione w kolejnych rozdziałach drzewa klasyfikacji oraz metody kombinacyjne można uznać za warianty metody K-P. Technika K-P umożliwia przekształcenie specyfikacji funkcjonalnej programu na specyfikację testów w systematyczny sposób. Polega na identyfikacji elementów wpływających na działanie funkcji programu, a następnie – generowaniu testów przez metodyczne układanie tych elementów w konfiguracje. Hipoteza błędu mówi, że błędy powstają na skutek złego obsługiwania kombinacji danych wejściowych. Metoda składa się z następujących kroków: 1. Zdekomponuj specyfikację funkcjonalną na funkcjonalne jednostki-moduły, które mogą być testowane niezależnie od siebie. 2. Zidentyfikuj parametry (wejścia do modułów) oraz warunki środowiskowe (tzn. stany systemu podczas jego działania), które wpływają na zachowanie się programu. 3. Wyodrębnij kategorie (główne właściwości lub charakterystyki) parametrów i warunków.
4. Podziel każdą kategorię na strefy wyboru, które są rozłączne i w sumie zawierają wszystkie możliwe wartości, jakie dana kategoria może przyjmować. 5. Określ ograniczenia i związki między strefami wyboru kategorii. 6. Napisz specyfikację testów (tzn. listę kategorii, ich podziały na strefy wyborów oraz ograniczenia między nimi), wykorzystując np. język specyfikacji testów (ang. Test Specification Language, TSL). 7. Wyprowadź elementy pokrycia (ręcznie lub np. przy użyciu generatora). Każdy element pokrycia to zbiór stref wyboru, przy czym jedna kategoria może być w nim reprezentowana co najwyżej jedną strefą wyboru. 8. Dla każdego elementu pokrycia stwórz przypadek testowy przez wybór odpowiednich danych wejściowych realizujących zdefiniowane wybory. Oryginalna metoda sugeruje użycie narzędzi automatyzujących generację elementów pokrycia (język typu TSL), ale można ten etap przeprowadzić również ręcznie, jeśli liczba kategorii i wyborów nie jest duża. Przy tworzeniu przypadków testowych można posłużyć się wyrocznią testową do określenia oczekiwanych wyjść. Krok 3 metody jest kluczowy i najbardziej twórczy. Zależnie od doświadczenia testera, jego znajomości obiektu testów oraz formy podstawy testów, wyniki tej fazy mogą się bardzo różnić. Każdy tester może dostarczyć odmienne kategorie, zwracając uwagę na różne aspekty działania systemu. Ważną cechą metody K-P jest stworzenie specyfikacji testów (p. 6) jako obiektu pośredniego między specyfikacją funkcjonalną a wyjściowym zbiorem przypadków testowych. Cel metody to generacja przypadków spełniających pokrycie kombinacji wyborów. Wadą metody jest to, że przy większej liczbie kategorii i wyborów liczba ich możliwych kombinacji (a więc i liczba przypadków testowych) rośnie bardzo szybko. Dlatego ważne jest stosowanie ograniczeń i związków między wyborami. Można również stosować mniej radykalne kryteria pokrycia, niż żądanie przetestowania wszystkich kombinacji. To podejście omówimy dokładniej w podrozdziale 8.8. W tabeli 8.28 podsumowano metodę kategoria-podział. Tabela 8.28. Podsumowanie metody kategoria-podział
8.6.2. Przykład Rozważmy
metodę getDisplayString klasy
timetableFacade
systemu
ELROJ
z Dodatku A. Chcemy ją przetestować, używając metody K-P. Krok 1. Dekompozycja specyfikacji funkcjonalnej na funkcjonalne moduły. Mamy tylko jedną testowaną metodę getDisplayString, która stanowi już osobną, funkcjonalną jednostkę dającą się testować niezależnie od innych elementów systemu. Krok 2. Identyfikacja parametrów i warunków. Bezpośrednimi parametrami, czyli wejściami do modułu (tzn. naszej metody) są zmienne actTimemm i displayLines oznaczające aktualny czas (w minutach) oraz liczbę linii ekranu. Za warunek środowiskowy uznajemy rozkład jazdy zapisany w bazie programu, ponieważ od jego postaci zależeć będzie wartość zwracana przez getDisplayString. Operowanie rozkładem jazdy jako takim nie byłoby zbyt wygodne, dokonajmy dalszej identyfikacji cech tego obiektu. Rozkład może cechować się tym, czy istnieje zdefiniowany w nim komunikat oraz liczbą zdefiniowanych linii autobusowych, co ma znaczenie z punktu widzenia wymagania R06 opisującego relację między ich liczbą a liczbą linii ekranu. Ważna może być liczba pojedynczych kursów (np. jak powinien zachować się program, jeśli w rozkładzie mamy tylko jedną linię i jeden kurs, a ekran ma 10 linii?). Krok 3. Wyodrębnienie kategorii parametrów i warunków. Dla parametru określającego liczbę linii autobusowych kategorią może być jej relacja do liczby linii ekranu oraz do ograniczeń nałożonych na liczbę linii autobusowych przez specyfikację. Dla zmiennej actTimemm kategorią może być jej ograniczenie zadane
przez specyfikację, ale też np. to, czy w rozkładzie istnieje kurs dla tej minuty (jest to
kategoria
odpowiadająca
wartościom
brzegowym).
W
tabeli
8.29
podsumowano wszystkie zidentyfikowane przez nas parametry wraz z ich kategoriami. Tabela 8.29. Parametry i kategorie metody getDisplayString
Parametr
Kategoria
aktualny czas w minutach (actTimemm)
wartoś ci brzeg owe i typowe is tnienie w rozkładzie kurs u o tej minucie
liczba linii ekranu (displayLines)
wartoś ci brzeg owe i typowe
komunikat
przypadki s pecjalne dług oś ć komunikatu
liczba linii autobus owych
wartoś ci brzeg owe i typowe relacja do liczby linii ekranu
liczba kurs ów
łączna liczba kurs ów a liczba linii ekranu wartoś ci brzeg owe i typowe
Krok 4. Podział kategorii na strefy wyboru. Przy podziale kategorii na strefy wyboru możemy wykorzystać techniki podziału na klasy równoważności oraz analizy wartości brzegowych, ale możemy również wprost posłużyć się specyfikacją, która w niektórych przypadkach bardzo ściśle określa pewne zależności między różnymi artefaktami. Podział zidentyfikowanych w poprzednim kroku kategorii na strefy wyboru jest pokazany w tabeli 8.30. Tabela 8.30. Parametry, kategorie i wybory dla metody getDisplayString
Parametr
Kategoria
W ybory
wartoś ci brzeg owe i typowe
actTimemm = 0 actTimemm = n, 1 ≤ n ≤ 58 actTimemm = 59
aktualny czas
liczba linii ekranu
is tnienie kurs u o tej minucie
actTimemm = n i is tnieje kurs o minucie n actTimemm = n i nie is tnieje kurs o minucie n
wartoś ci brzeg owe i typowe
displayLines displayLines displayLines displayLines displayLines
przypadki s pecjalne
brak komunikatu komunikat=null komunikat=” ”
dług oś ć komunikatu
0 1 typowa (między 2 a MAX – 1) MAX
wartoś ci brzeg owe i typowe
0 1 2 ≤ n ≤ 99 100
relacja do liczby linii ekranu
mniej niż linii ekranu – 1 dokładnie tyle ile linii ekranu –1 więcej niż linii ekranu – 1
komunikat
liczba linii autobus owych
łączna liczba kurs ów a l. linii ekranu
=2 =3 = n, 4 ≤ n ≤ 99 = 101 > 101
kurs ów mniej niż linii ekranu –1 kurs ów dokładnie tyle ile linii ekranu – 1
kurs ów więcej niż linii ekranu – 1
liczba kurs ów
wartoś ci brzeg owe i typowe
0 1 2 ≤ n ≤ 5999 6000
Maksymalna liczba kursów dla jednej linii to 60 (w minutach 0, 1, 2, …, 59). Maksymalna liczba linii autobusowych to 100, zatem maksymalna liczba kursów to 6000. Nie mamy górnego ograniczenia na liczbę linii ekranu, zatem nie możemy tu operować wartością maksymalną. Wartość MAX dla długości komunikatu może wynikać np. z typu zmiennej przechowującej go. Dla poszczególnych kategorii z tabeli mamy odpowiednio 3, 2, 5, 3, 4, 4, 3, 3, 4 wyborów. Jeśli chcielibyśmy testować konfiguracje wszystkich możliwych wyborów dla wszystkich możliwych kategorii, to musielibyśmy rozważyć 3 ⋅ 2 ⋅ 5 ⋅ 3 ⋅ 4 ⋅ 4 ⋅ 3 ⋅ 3 ⋅ 4 = 51 840 kombinacji. Jest to stanowczo zbyt dużo. Możemy ograniczyć liczbę testów, rozważając kombinacje nie kategorii, ale parametrów. Dla poszczególnych parametrów mamy odpowiednio 5, 5, 7, 7 i 7 wyborów, zatem liczba wszystkich możliwych kombinacji wyborów wszystkich parametrów wynosi 52 ⋅ 73 = 8575, co nadal jest zbyt dużą liczbą. Zobaczmy, jak liczba ta zredukuje się po zastosowaniu ograniczeń na wybory. Krok 5. Określenie ograniczeń wyborów. W naszej tabeli możemy wskazać kilka ograniczeń między wyborami. Na przykład, dla każdego z wyborów kategorii „przypadki specjalne” parametru komunikat jedynym możliwym wyborem długości komunikatu jest 0. Niestety ten warunek nie wpłynie na zmniejszenie liczby testów, gdy rozważamy kombinacje parametrów, a nie kategorii, ponieważ obie wspomniane kategorie wchodzą w skład tego samego parametru. Inne ograniczenie wiąże ze sobą kategorię „relacja do liczby linii ekranu” parametru „liczba linii autobusowych” oraz kategorię „łączna liczba kursów”. Niech K = liczba linii ekranu – 1. Kursów nie może być więcej niż linii, dlatego jeśli kursów jest mniej niż K, to liczba linii autobusowych musi być również mniejsza niż K. Jeśli kursów jest dokładnie K, to linii może być K lub mniej. To ograniczenie redukuje liczbę przypadków o 525 do 8050. Część ograniczeń wynika z przyjęcia
konkretnych wartości niektórych zmiennych. Na przykład przyjęcie liczby linii autobusowych = 0 implikuje, że liczba kursów musi być 0 lub mniejsza niż liczba linii ekranu – 1. Jeśli nadal mamy zbyt dużo przypadków, to możemy spróbować połączyć kategorie ze sobą. Na przykład jeśli dla parametru aktualny czas połączymy obie kategorie w jedną, to możemy otrzymać tylko 3 możliwe wybory, np.: n = 0 i istnieje taki kurs w rozkładzie, 1 ≤ n ≤ 58 i istnieje taki kurs w rozkładzie, n = 59 i nie istnieje taki kurs w rozkładzie. Gdy to też nie pomoże i nadal mamy zbyt dużo kombinacji, możemy traktować niektóre parametry oddzielnie. Na przykład, możemy testować osobno kombinacje liczby linii ekranu z liczbą linii autobusowych (nie dbając o wartości kategorii innych parametrów), osobno kombinacje komunikatów i liczby kursów oraz osobno aktualny czas. W takim układzie liczba wszystkich kombinacji, nie uwzględniając ograniczeń, wyniesie 5 ⋅ 7 + 7 ⋅ 7 + 5 = 89. To już jest sensowna liczba przypadków testowych. Tego typu ograniczenia omówimy dokładnie w podrozdziale 8.8. Jak widać, tzw. techniki kombinacyjne pozwalają istotnie ograniczyć liczbę możliwych testów. Krok 6. Specyfikacja testów. Specyfikacja będzie wyglądać podobnie do tabeli 8.30, z tym że może być ona wyrażona za pomocą języka specyfikacji testów. Krok 7. Wyprowadź elementy pokrycia. Elementy pokrycia zależą od przyjętego kryterium pokrycia. Jeśli jest nim kombinacja wartości wszystkich możliwych kategorii, to każdy element pokrycia będzie reprezentował jedną kombinację. Jeśli – tak jak w kroku 5 powyżej – osłabiamy kryterium pokrycia, elementów może być mniej, np. część z nich może reprezentować kombinacje wartości jednego podzbioru kategorii, a część innego. Krok 8. Stwórz przypadki testowe. Dla każdego elementu pokrycia należy dostarczyć dane wejściowe dla przypadków testowych tak, by pokryły te elementy. Oto przykładowy przypadek testowy dla metody getDisplayString. Załóżmy, że element pokrycia wygląda następująco: aktualny czas: n = 59 i istnieje kurs o tej minucie; liczba linii ekranu: 3; komunikat: brak komunikatu;
liczba linii autobusowych: dokładnie tyle, ile linii ekranu – 1 (czyli 2); liczba kursów: 2 ≤ n ≤ 5999, np. n = 4. Parametrami wejściowymi dla metody będą w tym przypadku actTimemm = 59 oraz displayLines = 3, ale musimy jeszcze skonfigurować środowisko testowe tak, by postać rozkładu jazdy czyniła zadość opisanym warunkom. Konfigurację możemy przeprowadzić np. w sposób podany na listingu 8.3.
TimetableFacade facade = new TimetableFacade(); facade.addBus(110, 12); facade.addBus(111, 59); facade.addBus(111, 10); facade.addBus(110, 13); Listing 8.3. Konfiguracja środowiska testowego dla wykonania testu metody getDisplayString Jest to fragment kodu pozwalający na stworzenie rozkładu jazdy spełniającego wymagane warunki. Dodaliśmy cztery kursy dla dwóch linii (o numerach 110 i 111). Czas przyjazdu jednego z kursów jest taki sam jak parametr wywołania actTimemm. Pełny opis przypadku testowego wygląda zatem następująco: konfiguracja: rozkład z 2 liniami: 100 (o przyjazdach w minutach 12, 13) i 111 (o przyjazdach w minutach (komunikat=null), data: 1 stycznia 2014;
13,
59),
dane wejściowe: actTimemm = 59, displayLines = 3; oczekiwane wyjście:
01.01.2014 LINIA
Czas przyjazdu
111
0 min
111
11 min
110
13 min
brak
komuniaktu
8.7. Drzewa klasyfikacji 8.7.1. Opis metody Metoda drzew klasyfikacji to kolejna technika czarnoskrzynkowa, będąca z jednej strony szczególnym przypadkiem techniki kategoria-podział, z drugiej zaś jej istotnym rozszerzeniem. Tak jak w przypadku metody kategoria-podział, podczas stosowania drzew klasyfikacji dziedzina wejściowa testowanego obiektu jest dzielona ze względu na różne aspekty istotne z punktu widzenia testowania. Dla każdego aspektu jest tworzona rozłączna i pełna klasyfikacja (tzn. klasy są rozłączne i obejmują wszystkie możliwości). W tym sensie metoda jest szczególnym przypadkiem techniki kategoria-podział, gdyż wymaga rozłączności aspektów, co nie występuje w przypadku kategorii i wyborów (np. w przykładzie z p. 8.6.2 kategorie „wartości brzegowe i typowe” oraz „istnienie kursu o danej minucie” dla parametru „aktualny czas” nie są rozłączne). Drzewa klasyfikacji rozszerzają jednak technikę kategoria-podział, ponieważ dopuszczają dalszą klasyfikację uzyskanych klas. Krokowy proces podziału dziedziny wejściowej ze względu na różne klasyfikacje jest przedstawiany w formie drzewa. Przypadki testowe są tworzone na podstawie wyboru co najwyżej jednej klasy z każdej klasyfikacji. Cały proces pozwala tworzyć przypadki w sposób prosty, systematyczny i formalny. Tak jak w przypadku metody kategoria-podział, w technice drzew klasyfikacji można zdefiniować różnego rodzaju kryteria pokrycia, aby uniknąć wykładniczego wzrostu generowanych przypadków. drzewo klasyfikacji (ang. classification tree) – drzewo obrazujące hierarchiczną zależność klas równoważności używane do projektowania przypadków testowych metodą drzewa klasyfikacji metoda drzewa klasyfikacji (ang. classification tree method) – czarnoskrzynkowa technika projektowania przypadków testowych, w której przypadki te są projektowane do wykonania kombinacji reprezentantów wejść i/lub przetworzonych wyjść [98]
8.7.2. Budowa drzewa klasyfikacji Opis budowy drzewa zilustrujemy za pomocą przykładu pochodzącego z [98]. Rozważmy System Komputerowego Rozpoznawania Obiektów (SKRO), który schematycznie jest pokazany na rysunku 8.22. Składa się on z pasa transmisyjnego oraz kamery podłączonej do komputera. Na pasie znajdują się obiekty różnego kształtu, koloru i rozmiaru. Zadaniem systemu jest rozpoznać typ obiektu. Zbudujemy drzewo klasyfikacji dla tego systemu, opisując przy okazji funkcje jego różnych elementów składowych. Do budowy drzewa klasyfikacji wykorzystuje się cztery typy wierzchołków: korzeń – reprezentuje testowany obiekt (w naszym przypadku SKRO); klasyfikacja – wierzchołek reprezentujący pojedynczy aspekt, w jakim rozważany jest testowany obiekt;
Rysunek 8.22. System komputerowego rozpoznawania obiektów klasa – charakterystyka (element) klasyfikacji; kompozycja – wierzchołek reprezentujący zbiór klasyfikacji.
Dziećmi korzenia, kompozycji i klas mogą być wyłącznie klasyfikacje i kompozycje. Dziećmi klasyfikacji mogą być wyłącznie klasy. Klasy są powiązane z przypadkami testowymi za pomocą tzw. macierzy testów, którą omówimy w dalszej części. Klasy różnych klasyfikacji wchodzących w skład kompozycji nie muszą być parami rozłączne. Na rysunku 8.23 przedstawiono notację wykorzystywaną do oznaczania poszczególnych typów wierzchołków drzewa.
Rysunek 8.23. Typy wierzchołków w drzewie klasyfikacji System SKRO ma rozpoznawać obiekty, dlatego z punktu widzenia testowania istotne mogą się okazać takie aspekty jak kolor czy wymiary. Kolor przyjmiemy jako klasyfikację, natomiast wymiar potraktujemy jak kompozycję dwóch klasyfikacji: rozmiaru oraz kształtu. Klasyfikacje dzielimy na klasy: kształt obiektów (możliwe klasy: kwadrat, koło, trójkąt); kolor obiektów (możliwe klasy: czarny, szary, biały); rozmiar obiektów (możliwe klasy: duży, mały). Możemy również w szczególny sposób potraktować trójkąty i dodatkowo zdefiniować dla tej klasy klasyfikację „typ” dzielącą trójkąty na 3 klasy: równoboczne, równoramienne i różnoboczne. Drzewo klasyfikacji dla systemu SKRO jest pokazane na rysunku 8.24. Pod drzewem klasyfikacji znajduje się tzw. macierz testów. Jej wiersze (oznaczone poziomymi liniami) odpowiadają poszczególnym przypadkom testowym. Kolumny (linie pionowe) odpowiadają wszystkim klasom występującym w drzewie. Czarna kropka na
Rysunek 8.24. Drzewo klasyfikacji dla systemu SKRO wraz z macierzą testową przecięciu przypadku i klasy oznacza, że w danym przypadku testowym musi wystąpić wartość wejściowa odpowiadająca tej klasie. Na przykład, w PT1 testowanym obiektem jest duży, czarny okrąg, w PT2 – mały, biały trójkąt równoramienny itd. Zauważmy, że w PT4 nie dokonaliśmy wyboru klasy dla klasyfikacji „kolor”. Jest to dopuszczalne, jeśli jakaś cecha jest opcjonalna. Możemy na przykład stwierdzić, że kolor jest cechą, która nie zawsze będzie nas interesować i pominąć ją w niektórych przypadkach testowych13. Przypadki testowe zwykle generuje się automatycznie, na podstawie określonego kryterium pokrycia. Automatyzacja pozwala na szybką generację testów oraz na uniknięcie pomyłek polegających na przeoczeniu jakichś kombinacji klas. Najbardziej oczywistym kryterium jest pokrycie wszystkich
kombinacji klas, jednak – tak jak w metodzie kategoria-podział – może to doprowadzić do bardzo dużej liczby przypadków testowych. Dla drzewa pokazanego na rysunku 8.24 mamy trzy niezależne klasyfikacje: rozmiar (2 klasy), kształt (5 klas) i kolor (3 klasy), co daje w sumie 2 ⋅ 5 ⋅ 3 = 30 kombinacji. W przypadku zbyt dużej liczby testów można zastosować techniki kombinacyjne z podrozdziale 8.8.
8.7.3. Asortyment produktów programowych i model cech Asortyment produktów programowych to podejście stosowane w celu ulepszenia ponownego użycia (ang. reusability) komponentów oprogramowania w większej liczbie produktów, które współdzielą ten sam zbiór cech [99], [100]. Przez cechę rozumiemy tu przyrost funkcjonalności oprogramowania. asortyment produktów programowych (ang. Software Product Line, SPL) – zbiór systemów silnie opartych na oprogramowaniu, współdzielących ten sam, zarządzany zbiór cech spełniający określone potrzeby danego segmentu rynku; systemy te są wytwarzane na podstawie wspólnego zbioru zasobów, zgodnie ze ściśle ustalonymi regułami Przy tworzeniu asortymentu produktów programowych często korzysta się z tzw. modeli cech (ang. feature models). Modele cech można traktować jak odmianę drzew klasyfikacji, w których każdy wierzchołek reprezentuje klasyfikację odpowiadającą określonej cesze. To specyficzne zastosowanie drzew klasyfikacji jest okazją do opisania ich rozszerzonej funkcjonalności, mianowicie ograniczeń i zależności między klasami oraz właściwości pojedynczych klasyfikacji. Modele cech służą do specyfikowania elementów asortymentu produktów programowych. Rozważmy na przykład linię produkcyjną samochodów określonej marki. Każdy samochód może mieć różny standard wyposażenia. Niektóre elementy wyposażenia wymagają obecności innych. Inne wzajemnie się wykluczają. Aby przetestować wszystkie konfiguracje cech z uwzględnieniem zależności i ograniczeń między nimi należy zbudować na podstawie modelu cech drzewo klasyfikacji i wygenerować przypadki testowe, uwzględniając te ograniczenia.
Model cech jest podobny do drzewa klasyfikacji, gdyż przedstawia hierarchicznie zorganizowany zbiór cech. Używa jednak dodatkowej notacji, pokazanej na rysunku 8.25.
Rysunek 8.25. Notacja dla modelu cech Oto znaczenie poszczególnych symboli: XOR: dokładnie jedna podcecha musi zostać wybrana; OR: jedna lub więcej podcech może zostać wybranych; obowiązkowe: cecha jest wymagana; opcjonalne: cecha jest opcjonalna; wymaga: wybór cechy A wymusza wybór cechy B; wyklucza: wybór cechy A wymusza pominięcie cechy B. Rozważmy przykład zaczerpnięty z [101]. Jest to asortyment produktu programowego smartfonu z systemem operacyjnym Android. Model cech dla tego asortymentu jest przedstawiony na rysunku 8.26.
Rysunek 8.26. Model cech dla asortymentu produktu programowego smartfonu Cechy „podstawowe funkcje”, „wiadomość”, „połączenie głosowe” i „SMS” są obowiązkowe i każdy model smartfonu musi je mieć. Cecha „MMS” jest opcjonalna. „Komunikacja” i jej podcechy: „WLAN”, „Bluetooth” i „UMTS” również są opcjonalne. Cecha „dodatki” jest obowiązkowa i użycie operatora OR nakazuje wybrać co najmniej jedną spośród cech „MP3” i „aparat”. Oczywiście można wybrać obie w ramach jednego produktu. Jeśli cecha „aparat” jest wybrana, to musimy wybrać dokładnie jedną z cech „3 megapiksele” oraz „8 megapikseli”. Ponadto wybór „MP3” wyklucza istnienie cechy „Bluetooth” i na odwrót. Wybór cechy „MMS” wymaga wyboru cechy „aparat”. Jak widać, istnieje kilka reguł, które muszą być spełnione podczas wyboru cech dla modelu smartfona: cecha w korzeniu musi być zawsze wybrana; wybór cech powinien mieć wartość logiczną true dla formuły opisującej cechy i związki między nimi; wszystkie ograniczenia („wymaga” i „wyklucza”) muszą być spełnione; dla każdej wybranej cechy, która nie jest korzeniem, jej rodzic również musi być wybrany. Dla naszego przykładu istnieje 61 możliwych wyborów zestawów cech spełniających wymienione kryteria, co daje łącznie 61 przypadków testowych. Tak jak poprzednio, możemy zredukować ich liczbę, stosując jedno z kombinatorycznych kryteriów pokrycia. W literaturze (np. [102]) zaproponowano wiele różnych strategii testowania przy użyciu drzew klasyfikacji i asortymentu produktów programowych, takich jak: product-by-product, gdzie każda instancja SPL jest testowana indywidualnie, np. przy użyciu drzew klasyfikacji (niezbyt praktyczna, gdyż zwykle generuje bardzo dużą liczbę przypadków testowych); testowanie inkrementacyjne, gdzie pierwszy produkt jest testowany indywidualnie (np. przy użyciu drzew klasyfikacji), a w każdym następnym – ze względu na dzielenie wspólnego zbioru cech – stosuje się techniki testów regresyjnych (patrz p. 4.4.4); problemem jest to, czy testowaniu podlegać mają wyłącznie cechy nowe i zmodyfikowane oraz
czy mogą być przetestowane w izolacji, czy też należy je testować w kombinacji z jakimiś nie zmienionymi komponentami; reużywalne testy, gdzie za pomocą inżynierii dziedzinowej tworzy się reużywalne przypadki testowe; podział odpowiedzialności, gdzie poszczególne typy testów wykonuje się w różnych fazach wytwórczych (np. w cyklu opisanym modelem V). W tabeli 8.31 podsumowano metodę drzew decyzyjnych. pokrycie drzewa klasyfikacji (ang. classification tree coverage) – odsetek pokrytych testami kombinacji wyborów klas występujących w drzewie, z uwzględnieniem zadanych ograniczeń Tabela 8.31. Podsumowanie metody drzew decyzyjnych
W arunki Klasyfikacje i klasy testowe Elementy Kombinacje wyborów klas (z uwzg lędnieniem og raniczeń) pokrycia Dla każdej zidentyfikowanej kombinacji wyborów is tnieje Kryterium tes t, któreg o dane wejś ciowe realizują te wybory (kryterium pokrycia można os łabić, redukując liczbę możliwych kombinacji – patrz podrozdział 8.8) Pokrycie
p=
liczba pokrytych tes tami kombinacji wyborów liczba ws zys tkich kombinacji wyborów
× 100%
8.7.4. Przykład Przetestujmy system SKRO na podstawie drzewa klasyfikacji z rysunku 8.24. Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden element testów, system SKRO:
ET1: SKRO. Krok
2.
Wyprowadzenie
warunków
testowych.
Warunki
testowe
są
identyfikowane przez wyprowadzenie klasyfikacji i klas dla każdego z parametrów. Załóżmy ponadto, że klasyfikacja „kolor” jest opcjonalna. WT1: rozmiar (mały, duży); WT2: kształt (okrąg, trójkąt, kwadrat); WT3: typ (równoboczny, równoramienny, różnoboczny); WT4: kolor (biały, szary, czarny); WT5: kolor = null. Wszystkie warunki testowe są wyprowadzone dla elementu testowego ET1. Krok 3. Wyprowadzenie elementów pokrycia. Elementy pokrycia określamy na podstawie przyjętego kombinacyjnego kryterium pokrycia, które określa, jakie kombinacje klas oraz których klasyfikacji są wymagane do przetestowania. Przyjmijmy podejście minimalistyczne, tzn. takie, które wymaga, aby każdy nowy element pokrycia pokrywał jak najwięcej elementów drzewa (tzn. liści, które oznaczają klasy). Drzewo klasyfikacji i macierz testów dla tego podejścia są pokazane na rysunku 8.27. EP1 pokrywa WT1, WT2 i WT4; EP2 pokrywa WT1, WT2, WT3 i WT4; EP3 pokrywa WT1, WT2, WT3 i WT4; EP4 pokrywa WT1, WT2, WT3 i WT5; EP5 pokrywa WT1, WT2 i WT5.
Rysunek 8.27. Drzewo klasyfikacji wraz z tabelą kombinacji dla kryterium minimalistycznego Tabela 8.32. Przypadki testowe dla programu SKRO
Id
W ejścia rozmiar kształt
PT1 mały
okrąg
PT2 duży
trójkąt równoboczny
PT3 mały
Oczekiwane wyjście
Pokryte EP
biały
mały biały okrąg
EP1
s zary
duży s zary trójkąt równoboczny
EP2
kolor
mały czarny trójkąt trójkąt równoramienny czarny równoramienny
EP3
PT4 duży
trójkąt różnoboczny
null
duży trójkąt różnoboczny
EP4
PT5 mały
kwadrat
null
mały kwadrat
EP5
Krok 4. Zdefiniowanie przypadków testowych. Każdy przypadek testowy pokrywa dokładnie jeden element pokrycia. Przypadki testowe są tworzone jeden po drugim przez wybór niepokrytego dotąd elementu pokrycia oraz wartości wejściowych pokrywających klasy wyznaczone przez kombinację odpowiadającą temu elementowi pokrycia. Procedura jest powtarzana dopóty, dopóki nie zostanie uzyskany założony poziom pokrycia. Uzyskanie koloru „null” można wyobrazić sobie np. jako zaprezentowanie systemowi obiektu w kolorze innym niż biały, szary i czarny. Zestaw przypadków pokrywających wszystkie elementy pokrycia jest przedstawiony w tabeli 8.32.
8.8. Metody kombinacyjne 8.8.1. Opis metody Celem metod kombinacyjnych jest redukcja rozmiaru suity testowej w przypadku, gdy testujemy jednocześnie kilka aspektów (charakterystyk) systemu i liczba wszystkich kombinacji do przetestowania jest zbyt duża. Są to metody, które można traktować jak niezależne techniki projektowania testów, ale wykorzystuje się je również podczas stosowania innych technik, takich jak podział na klasy równoważności, tablice decyzyjne, kategoria-podział czy drzewa klasyfikacji. Istnieje wiele odmian metody, różniących się tym, jak silne jest kryterium pokrycia. Im silniejsze, tym więcej generuje przypadków testowych. W metodach kombinacyjnych zakładamy, że dziedzina wejściowa, wyjściowa lub ich części może być podzielona na kilka sposobów. Innymi słowy, program może zostać opisany za pomocą wielu różnych charakterystyk. Formalnie, niech D oznacza dziedzinę testowanego programu. Dane są zbiory D1, D2, … Dk ⊆ D oraz relacje równoważności ρ1, ρ2, …, ρk takie, że ρi dzieli zbiór Di na d i klas równoważności. Przypadek testowy ma postać (x1, x2, …, xk ), gdzie xi ∈ Di. Relacje
ρi powodują, że elementy pozostające ze sobą w relacji traktujemy podobnie i tak, jak w przypadku metody podziału na klasy równoważności, aby przetestować klasę wystarczy skonstruować test dla jednej, dowolnej wartości z tej klasy. Mamy do czynienia z wieloma podziałami, dlatego sytuacja się komplikuje. Naszym celem może być przecież nie tylko przetestowanie każdej klasy z osobna, lecz także kilku klas różnych relacji w kombinacji ze sobą. Im więcej kombinacji chcemy przetestować, tym więcej przypadków testowych będziemy musieli stworzyć. W skrajnym przypadku, gdy chcemy przetestować kombinacje klas ze wszystkich podziałów D1, D2, …, Dk , musielibyśmy stworzyć d 1 ⋅ d 2 ⋅ … ⋅ d k testów. W kolejnych podrozdziałach opiszemy kilka technik kombinacyjnych. Wszystkie omówimy na podstawie następującego przykładu. Zespół deweloperski tworzy produkt komercyjny „z półki” (ang. Comercial Off The Shelf, COTS). To oznacza, że należy go przetestować w wielu środowiskach. Program współpracuje z przeglądarką, drukarką, systemem operacyjnym oraz bazą danych. Wymagania opisują następujące rodzaje wymienionych obiektów, z którymi program musi współpracować: rodziny przeglądarek (IE, Firefox, Chrome); rodziny drukarek (HP, Epson, Samsung, Canon); rodziny systemów operacyjnych (Windows, Linux, iOS); silniki baz danych (MySQL, PostgreSQL). Dla uproszczenia nie uwzględniamy tu konkretnych modeli wymienionych obiektów. W praktyce mogłaby zajść potrzeba rozróżniania tych obiektów na poziomie wersji. Wszystko zależy od analizy ryzyka, tego, co chcemy przetestować oraz jaką przyjmujemy hipotezę błędu. Nasz system może np. bardzo silnie zależeć od architektury systemu operacyjnego, a mniej od rodzaju podłączonej do niego drukarki. Dla każdej konfiguracji oczekiwanym wyjściem powinno być poprawne działanie programu, bez żadnych komunikatów o błędach. Opisaliśmy cztery charakterystyki programu, dotyczące rodzaju przeglądarki, drukarki, systemu operacyjnego oraz bazy, z jakimi może pracować nasz system: D1 = {IE, Firefox, Chrome}; D2 = {HP, Epson, Samsung, Canon};
D3 = {Windows, Linux, iOS}; D4 = {MySQL, PostgreSQL}. Dziedzina (charakterystyka) D1 została podzielona na 3 klasy równoważności, D2 na 4, D3 na 3 i D4 na 2. Każdy przypadek testowy dotyczy uruchomienia systemu w określonej konfiguracji środowiska, zatem jest charakteryzowany wejściem złożonym z czterech elementów: przeglądarka, drukarka, system operacyjny oraz baza danych. Elementem testowym jest nasz program. Mamy następujące warunki testowe (wszystkie dla tego elementu testowego): WT1: Przeglądarka = IE; WT2: Przeglądarka = Firefox; WT3: Przeglądarka = Chrome; WT4: Drukarka WT5: Drukarka WT6: Drukarka WT7: Drukarka
= HP; = Epson; = Samsung; = Canon;
WT8: System operacyjny = Windows; WT9: System operacyjny = Linux; WT10: System operacyjny = iOS; WT11: Baza danych = MySQL; WT12: Baza danych = PostgreSQL.
8.8.2. Each Choice Kryterium Each Choice wymaga, aby każda klasa każdej charakterystyki została przetestowana przynajmniej raz. Elementy pokrycia są wywodzone wprost z warunków testowych. EP1: Przeglądarka = IE (dla WT1); EP2: Przeglądarka = Firefox (dla WT2); EP3: Przeglądarka = Chrome (dla WT3); EP4: Drukarka = HP (dla WT4); EP5: Drukarka = Epson (dla WT5); EP6: Drukarka = Samsung (dla WT6);
EP7: Drukarka = Canon (dla WT7); EP8: System operacyjny = Windows (dla WT8); EP9: System operacyjny = Linux (dla WT9); EP10: System operacyjny = iOS (dla WT10); EP11: Baza danych = MySQL (dla WT11); EP12: Baza danych = PostgreSQL (dla WT12). Każdy przypadek testowy powinien pokrywać jeden lub więcej niepokrytych dotąd elementów pokrycia. W praktyce, stosując metodę Each Choice, testerzy zwykle starają się, aby każdy kolejny przypadek testowy pokrywał jak najwięcej niepokrytych dotąd elementów pokrycia. Czasami jest to niemożliwe ze względu na istniejące ograniczenia (np. jeśli przeglądarka IE nie współpracuje z systemem operacyjnym Linux, elementy te nie mogą współistnieć w ramach jednego przypadku testowego). W ogólności, dolne ograniczenie na minimalną liczbę przypadków testowych wynosi max i{d i}, gdzie d i jest liczbą klas i-tej charakterystyki. W naszym pokazane w tabeli 8.33.
przykładzie wystarczą
4 przypadki
testowe,
Tabela 8.33. Przypadki testowe spełniające kryterium Each Choice14
Id
W ejścia przeglądarka drukarka OS
PT1 IE
PT2 Firefox
HP
Eps on
baza danych
działa bez błędów14
EP1 EP8 EP1
działa bez Pos tg reS QL błędów
EP2 EP5 EP9 EP1
Windows MyS QL
Linux
Oczekiwane Pok wyjście EP
EP3 EP6 PT3 Chrome
S ams ung iOS
MyS QL
PT4 IE
Canon
Windows Pos tg reS QL
działa bez błędów
EP1 EP1
działa bez błędów
EP1 EP7 EP8 EP1
W przypadku PT4 musieliśmy wybrać drukarkę Canon, gdyż był to ostatni niepokryty element. Pozostałe trzy (przeglądarka, OS i baza danych) wybraliśmy arbitralnie. Należy jednak pamiętać, że dobre testowanie nie polega na ślepym podążaniu za regułami narzuconymi przez określoną technikę. Tester, znając system i wiedząc, gdzie może tkwić błąd, może wykorzystać taką sytuację, aby precyzyjnie określić pozostałe wejścia i zwiększyć szansę na wykrycie błędu.
8.8.3. Base Choice Czasami okazuje się, że pewne kombinacje są z jakichś powodów bardziej istotne od innych. Na przykład większość użytkowników testowanego systemu może używać systemu operacyjnego Windows i przeglądarki Internet Explorer. Tester może wziąć tego typu zależności pod uwagę i podczas tworzenia przypadków testowych w sposób szczególny potraktować określone kombinacje tak, by były testowane częściej od innych. W kryterium Base Choice określamy jedną kombinację charakterystyk jako podstawowy element pokrycia, a kolejne budujemy, wymieniając tylko jeden element na wszystkie pozostałe wartości. Załóżmy, że naszą podstawową konfiguracją będzie (IE, HP, Windows, MySQL). Otrzymujemy wtedy elementy pokrycia zebrane w tabeli 8.34. Tabela 8.34. Elementy pokrycia dla kryterium Base Choice
Id
Przeglądarka
Drukarka
OS
Baza danych
EP1 (bazowy)
IE
HP
W indows
MySQL
EP2
Firefox
HP
W indows
MySQL
EP3
Chrome
HP
W indows
MySQL
EP4
IE
Eps on
W indows
MySQL
EP5
IE
S ams ung
W indows
MySQL
EP6
IE
Canon
W indows
MySQL
EP7
IE
HP
Linux
MySQL
EP8
IE
HP
iOS
MySQL
EP9
IE
HP
W indows
Pos tg reS QL
Elementy należące do bloku bazowego (IE, HP, Windows i MySQL) zostały wypisane pogrubioną czcionką. Zauważmy, że są one testowane bardzo często. Każdy z pozostałych elementów wszystkich wejść występuje tylko w jednym elemencie pokrycia, np. iOS wchodzi w skład jedynie EP8, Canon – jedynie EP6 itd. Przypadki testowe odpowiadają elementom pokrycia. Każdy przypadek pokrywa jeden element pokrycia. Minimalna liczba przypadków testowych potrzebnych do spełnienia kryterium Base Choice to . Jeden przypadek to blok bazowy, a każda z pozostałych k grup to przypadki, w których wszystkie wejścia są ustawione na wartości bazowe oprócz jednego, które dla i-tej grupy może przyjąć d i – 1 możliwych wyborów (wszystkie poza wartością bazową).
8.8.4. Multiple Base Choice Kryterium Multiple Base Choice (wielu bloków bazowych) jest rozszerzeniem kryterium Base Choice. Polega ono na wyborze kilku bloków bazowych, a następnie na wymianie pojedynczych elementów w każdym z nich. Może się okazać, że podczas wymiany elementów w jednym bloku bazowym otrzymamy już istniejącą kombinację powstałą z wymiany elementów w innym bloku bazowym. W takim przypadku usuwamy z suity testowej powtarzające się przypadki. Kryterium umożliwia testerowi szczególne potraktowanie więcej niż jednej kombinacji elementów wejścia.
Załóżmy, że naszymi dwoma blokami bazowymi są bloki (IE, HP, Windows, MySQL) oraz (Firefox, HP, Linux, MySQL). W tabeli 8.35 zebrano elementy pokrycia. Tabela 8.35. Elementy pokrycia dla kryterium Multiple Base Choice
Id
Przeglądarka
Drukarka
OS
Baza danych
EP1 (bazowy)
IE
HP
W indows
MySQL
EP2 (bazowy)
Firefox
HP
Linux
MySQL
EP3
Firefox
HP
W indows
MySQL
EP4
Chrome
HP
W indows
MySQL
EP5
IE
Eps on
W indows
MySQL
EP6
IE
S ams ung
W indows
MySQL
EP7
IE
Canon
W indows
MySQL
EP8
IE
HP
Linux
MySQL
EP9
IE
HP
iOS
MySQL
EP10
IE
HP
W indows
Pos tg reS QL
EP11
IE
HP
Linux
MySQL
EP12
Chrome
HP
Linux
MySQL
EP13
Firefox
Eps on
Linux
MySQL
EP14
Firefox
S ams ung
Linux
MySQL
EP15
Firefox
Canon
Linux
MySQL
EP16
Firefox
HP
iOS
MySQL
EP17
Firefox
HP
Linux
Pos tg reS QL
EP1 i EP2 reprezentują bloki bazowe. EP3–EP10 to modyfikacje bloku EP1. EP11– EP17 to modyfikacje bloku EP2. Zauważmy, że EP3 i EP8 jest również modyfikacją bloku EP2. Przypadki testowe odpowiadają elementom pokrycia, tzn. każdy przypadek pokrywa jeden element.
8.8.5. Pair-wise testing Technika testowania par (ang. pair-wise testing) opiera się na założeniu, że najwięcej błędów jest powodowanych przez pojedynczy czynnik lub interakcję dwóch czynników [103]. Kuhn i Reilly [104] przestudiowali 171 błędów w oprogramowaniu dla serwera Apache i odkryli, że 70% z nich było spowodowanych przez nieprawidłową interakcję dwóch modułów. Istnieje wiele innych opracowań potwierdzających te wyniki. Technika testowania par pozwala na znaczną redukcję rozmiaru suity testowej. Kryterium pokrycia par wymaga, aby dla dowolnych dwóch elementów dowolnych dwóch charakterystyk istniał test zawierający tę kombinację. Każdy element pokrycia dotyczy więc kombinacji wartości dwóch charakterystyk. Dla naszego przykładu mamy następujące elementy pokrycia: Kombinacje przeglądarki i drukarki EP1: (IE, HP); EP2: (IE, Epson); EP3: (IE, Samsung); EP4: (IE, Canon); EP5: (Firefox, HP); EP6: (Firefox, Epson); EP7: (Firefox, Samsung); EP8: (Firefox, Canon); EP9: (Chrome, HP); EP10: (Chrome, Epson); EP11: (Chrome, Samsung); EP12: (Chrome, Canon). Kombinacje przeglądarki i systemu operacyjnego EP13: (IE, Windows); EP14: (IE, Linux); EP15: (IE, iOS);
EP16: (Firefox, Windows); EP17: (Firefox, Linux); EP18: (Firefox, iOS); EP19: (Chrome, Windows); EP20: (Chrome, Linux); EP21: (Chrome, iOS). Kombinacje przeglądarki i bazy danych EP22: (IE, MySQL); EP23: (IE, PostgreSQL); EP24: (Firefox, MySQL); EP25: (Firefox, PostgreSQL); EP26: (Chrome, MySQL); EP27: (Chrome, PostgreSQL). Kombinacje drukarki i systemu operacyjnego EP28: (HP, Windows); EP29: (Epson, Windows); EP30: (Samsung, Windows); EP31: (Canon, Windows); EP32: (HP, Linux); EP33: (Epson, Linux); EP34: (Samsung, Linux); EP35: (Canon, Linux); EP36: (HP, iOS); EP37: (Epson, iOS); EP38: (Samsung, iOS); EP39: (Canon, iOS). Kombinacje drukarki i bazy danych EP40: (HP, MySQL); EP41: (Epson, MySQL); EP42: (Samsung, MySQL); EP43: (Canon, MySQL); EP44: (HP, PostgreSQL);
EP45: (Epson, PostgreSQL); EP46: (Samsung, PostgreSQL); EP47: (Canon, PostgreSQL). Kombinacje systemu operacyjnego i bazy danych EP48: (Windows, MySQL); EP49: (Windows, PostgreSQL); EP50: (Linux, MySQL); EP51: (Linux, PostgreSQL); EP52: (iOS, MySQL); EP53: (iOS, PostgreSQL). Elementów pokrycia jest dosyć dużo (53), ale, jak za chwilę zobaczymy, można je będzie pokryć niewielką liczbą przypadków testowych. W ogólności elementów pokrycia dla kryterium pokrycia par jest czyli tyle, ile wynosi suma iloczynów możliwych kombinacji dla wszystkich par charakterystyk. W naszym przykładzie mamy 4 charakterystyki podzielone odpowiednio na 3, 4, 3 i 2 elementy, zatem elementów pokrycia powinno być n = 3 ⋅ 4 + 3 ⋅ 3 + 3 ⋅ 2 + 4 ⋅ 3 + 4 ⋅ 2 + 3 ⋅ 2 = 12 + 9 +6 +12 +8 +6 = 53, czyli dokładnie tyle, ile uzyskaliśmy. Po określeniu elementów pokrycia należy stworzyć przypadki, które je pokryją. Niestety problem (w wersji decyzyjnej) znalezienia minimalnego zestawu przypadków testowych spełniających kryterium pokrycia par jest NP-zupełny15. Jak zatem skonstruować sensowny (tzn. nieduży) zestaw testów dla tego pokrycia? Istnieją co najmniej trzy podejścia do problemu: 1) wypisać przypadki „ręcznie”, starając się nie generować zbyt wielu testów; 2) użyć jednego z algorytmów zachłannych; 3) wykorzystać tzw. tablice ortogonalne. Podejścia 2 i 3 są systematyczne i dobrze sformalizowane. Podejście 1 można wykorzystać przy małych problemach (np. gdy mamy do czynienia tylko z trzema charakterystykami i każda dzielona jest na dwie części). Opiszemy teraz po kolei wszystkie trzy podejścia. Podejście „ręczne”. Zauważmy najpierw, że przypadków testowych musi być co najmniej tyle, ile wynosi iloczyn kombinacji wartości dla dwóch największych charakterystyk, czyli 4 ⋅ 3 = 12. Pierwszy krok polegać będzie zatem na stworzeniu
dwunastu kombinacji charakterystyk dla drukarki i przeglądarki. Etap ten jest pokazany z lewej strony rysunku 8.28. Szare kolumny oznaczają elementy dodane w tym etapie. Każda kombinacja pokrywa jeden element pokrycia. W drugim kroku uzupełnimy tabelę o wartości charakterystyki systemu operacyjnego. Chcemy jednocześnie pokrywać pary drukarka/OS oraz przeglądarka/OS. Po dodaniu pierwszych trzech wartości: Win, Linux, iOS pokryliśmy pary (HP, Win), (HP, Linux), (HP, iOS) oraz (IE, Win), (Firefox, Linux) i (Chrome, iOS). Gdybyśmy w PT4–PT6 dodali znów wartości Win, Linux i iOS w takiej samej kolejności, pokrylibyśmy kombinacje dla drukarki Epson, ale nie pokrylibyśmy nowych kombinacji przeglądarka/OS. Dlatego dodajemy wartości Win, Linux i iOS w innej kolejności, tak, aby pokryć nie tylko trzy nowe pary drukarka/OS, lecz także trzy nowe pary przeglądarka/OS. Analogicznie postępujemy, dodając OS w przypadkach PT7–PT9. W ostatnich trzech przypadkach musimy dodać trzy różne OS (aby pokryć kombinacje drukarka/OS), ale możemy je wstawić w dowolnej kolejności, gdyż wszystkie pary przeglądarka/OS są już pokryte. W kroku trzecim uzupełniamy wartości w kolumnie dla bazy danych. Chcemy jednocześnie pokrywać pary drukarka/baza, przeglądarka/baza oraz OS/baza. Dla każdego rodzaju drukarki wpisujemy zarówno MySQL, jak i Postgres, uważając, aby przy okazji pokryć również możliwie jak najwięcej par przeglądarka/baza oraz OS/baza. Wynik tej czynności jest pokazany po prawej stronie na rysunku 8.28. Gwiazdka oznacza dowolną wartość. W pola nią oznaczone możemy wpisać dowolne wartości dla bazy danych, ponieważ wartości istniejące pokrywają już wszystkie możliwe kombinacje. Można to sprawdzić, analizując ostatnią kolumnę prawej tabeli. Występują w niej wszystkie elementy pokrycia, od 1 do 53. Zostawianie gwiazdek jest ważne, ponieważ w razie gdybyśmy musieli dodawać kolejne kolumny dla kolejnych charakterystyk, gwiazdki te ułatwią nam uzyskiwanie większej liczby kombinacji przez wybór tej wartości, która nam najbardziej w danej chwili pasuje. Dzięki temu minimalizujemy konieczność dodawania nowych wierszy w celu pokrycia jakichś kombinacji. Pogrubione liczby na rysunku 8.28 oznaczają pokrycie danego elementu po raz pierwszy. Pięćdziesiąt trzy elementy pokrycia udało się pokryć za pomocą jedynie 12 przypadków testowych. Oczywiście przy ich implementacji pola oznaczone w tabeli gwiazdkami należy zamienić na dowolne, ale ustalone wartości.
Id
Drukarka Przeg lądarka
Pokryte EP
Id
Drukarka Przeg lądarka
PT1
HP
IE
1
PT1
HP
IE
PT2
HP
Firefox
5
PT2
HP
Firefox
PT3
HP
Chrome
9
PT3
HP
Chrome
PT4
Eps on
IE
2
PT4
Eps on
IE
PT5
Eps on
Firefox
6
PT5
Eps on
Firefox
PT6
Eps on
Chrome
10
PT6
Eps on
Chrome
PT7
S ams ung
IE
3
PT7
S ams ung
IE
PT8
S ams ung
Firefox
7
PT8
S ams ung
Firefox
PT9
S ams ung
Chrome
11
PT9
S ams ung
Chrome
PT10
Canon
IE
4
PT10
Canon
IE
PT11
Canon
Firefox
8
PT11
Canon
Firefox
PT12
Canon
Chrome
12
PT12
Canon
Chrome
KROK 1. Kombinacje drukarka/przeg lądarka Id
Drukarka Przeg lądarka
KROK 2. Dodane kombinacje drukarka/OS i przeg lądarka/OS OS
Baza
Pokryte EP
Win
MyS QL
1, 13, 28, 22, 40, 48
PT1
HP
IE
PT2
HP
Firefox
PT3
HP
Chrome
PT4
Eps on
PT5
Eps on
PT6
Eps on
PT7
S ams ung
PT8
S ams ung
Firefox
iOS
MyS QL
77, 18, 38, 24, 42, 52
PT9
S ams ung
Chrome
Win
Pos tg res
11, 19, 30, 27, 46, 49
PT10
Canon
IE
Linux
*
4, 14, 35
PT11
Canon
Firefox
Win
MyS QL
8, 16, 31, 24, 43, 48
PT12
Canon
Chrome
iOS
Pos tg res
12, 21, 39, 27, 47, 53
Linux Pos tg res
5, 17 32, 25, 44, 51
iOS
*
9, 21, 36
IE
iOS
Pos tg res
2, 15, 37, 23, 45, 53
Firefox
Win
*
6, 16, 29
Chrome
Linux
MyS QL
10, 20, 33, 26, 41, 50
IE
Linux
*
3, 14, 34
KROK 3. Dodane kombinacje drukarka/baza, przeglądarka/baza i OS/baza Rysunek 8.28. Etapy ręcznego tworzenia przypadków dla kryterium pokrycia par Algorytmy zachłanne. W literaturze występuje wiele podejść algorytmicznych do znajdowania małych zestawów testów spełniających kryterium pokrycia par (zobacz np. przeglądowy artykuł [105]). Omówimy tu jedno z nich, InParameterOrder autorstwa Lei i Tai [106]. Ogólna idea ich algorytmu polega na rozpoczęciu od generacji kombinacji dwóch największych zbiorów
charakterystyk (bez utraty ogólności oznaczmy je jako D1 i D2), a następnie – w miarę potrzeb – rozszerzania tabeli wszerz (dodając nową charakterystykę) lub wzdłuż (dodając nowe przypadki testowe). Przez vi będziemy oznaczać wartości itej charakterystyki. Pseudokod dla tego generalnego podejścia jest podany na listingu 8.4.
InParameterOrder(D1, D2, …, Dk) T := {( v1, v2): v1 i v2 są wartościami charakterystyk D1 i D2 jeśli k = 2 to koniec dla każdej Di, i = 3, 4, …, k dla każdego testu (v1, …, vi–1) ∈ T //rozbudowa tablicy wszerz zastąp go przez (v1, …, vi–1, vi), gdzie vi jest wartością Di //rozbudowa tablicy wzdłuż dopóki T nie pokrywa wszystkich par między Di a każdym z D1, …, Di–1 dodaj nowy test dla D1, D2, …, Di do T koniec dopóki koniec dla koniec dla Listing 8.4. Procedura InParameterOrder dla pokrycia par Lei i Tai w swojej pracy zaproponowali kilka algorytmów opartych na tym schemacie. Pokazali również, że dla k charakterystyk, z których największa ma rozmiar d max rozmiar zestawu testowego wygenerowanego przez algorytm IPO
wyniesie . W naszym przypadku byłoby to 42 ⋅ (4 – 1) = 16 ⋅ 3 = 48. W metodzie „ręcznej” udało nam się zbudować zestaw trzy razy mniejszy. Istnieją oczywiście inne algorytmy, które mogą dać lepsze rezultaty niż IPO. Wykorzystanie tablic ortogonalnych. Tablice ortogonalne to obiekty matematyczne, którymi zajmuje się dział matematyki zwany projektowaniem eksperymentów (ang. experimental design). Są to tablice podobne do tej z tabeli 8.35, z tą różnicą, że zamiast wartości konkretnych zmiennych występują w nich liczby.
Tablice ortogonalne mają ponadto dodatkową, silniejszą właściwość, którą omówimy za chwilę. Istnieje cała rodzina tablic ortogonalnych, w zależności od liczby zmiennych oraz zakresu ich wartości, jakie mają być ujęte w tablicy. Tablica ortogonalna w szczególności spełnia właściwość pokrycia par, to znaczy dla dowolnych dwóch jej kolumn (odpowiadającym charakterystykom) i dla dowolnych dwóch wartości z odpowiadających im zakresów istnieje wiersz w tablicy ortogonalnej zawierający te wartości w zadanych kolumnach. Dlatego mając tablicę ortogonalną, możemy przekształcić wartości zmiennych poszczególnych kolumn na wartości naszych charakterystyk, otrzymując w ten sposób zbiór przypadków testowych spełniających kryterium pokrycia par. Z formalnego, matematycznego punktu widzenia, tablicę ortogonalną można zdefiniować następująco: t – (v, k, λ) – tablica ortogonalna (ang. orthogonal array), gdzie t ≤ k, to tablica o wymiarach λvt × k, której elementy należą do v-elementowego zbioru X i która ma następującą właściwość: w każdym t-elementowym podzbiorze kolumn każda t-krotka elementów z Xt pojawia się dokładnie w λ wierszach Jeśli w definicji tablicy ortogonalnej opuścimy warunek o tym, aby każda krotka pojawiała się dokładnie w λ wierszach, to takie obiekty nazywa się tablicami pokryć (ang. covering array). W internecie (np. [107]) można znaleźć przykłady minimalnych (lub najmniejszych znalezionych do tej pory) tablic pokryć dla różnych parametrów t, v, k. Definicja tablicy ortogonalnej jest zbyt ogólna dla naszych potrzeb. Po pierwsze, w testowaniu par interesują nas tylko podzbiory dwóch kolumn, zatem możemy wziąć t = 2. Po drugie, chcemy mieć jak najmniejszy zbiór testów, a więc tablicę ortogonalną o minimalnej liczbie wierszy. Dlatego przyjmiemy λ = 1, bo wystarczy, że każda kombinacja pojawi się w przynajmniej jednym przypadku testowym. Od tej pory dla kryterium pokrycia par rozważać będziemy jedynie 2 – (v, k, 1)-tablice ortogonalne. Przy tworzeniu tablicy ortogonalnej według tej definicji zakłada się, że każda zmienna przyjmuje taką samą liczbę możliwych wartości, co w przypadku testowanych systemów nie musi być prawdą. Na przykład testowany przez nas system ma jedną zmienną o 4 wartościach, dwie o 3 wartościach i jedną o 2 wartościach. Można rozszerzyć pojęcie tablicy ortogonalnej i dopuścić, aby jej kolumny reprezentowały zbiory o różnych licznościach. Jeśli jednak dla określonej
konfiguracji parametrów trudno jest nam taką tablicę stworzyć, to musimy założyć, że wszystkie zmienne przyjmują wartość równą maksymalnej charakterystyce. W naszym przypadku jest to 4. Po stworzeniu tablicy ortogonalnej będziemy mogli zamienić „nadmiarowe” liczby na arbitralne wartości z poprawnego zakresu odpowiednich zmiennych (nie wpłynie to na naruszenie kryterium pokrycia) i być może zredukować liczbę wierszy tablicy. Wróćmy zatem do naszego przykładu. Mamy 4 charakterystyki o maksymalnym rozmiarze 4. Potrzebna nam będzie zatem 2 – (4, 4, 1)-tablica ortogonalna. Jest ona pokazana na rysunku 8.29a. Istnieje cała matematyczna teoria mówiąca, jak generować tablice ortogonalne [108]. Wiele narzędzi wspomagających testowanie umożliwia generację w sposób automatyczny.
Rysunek 8.29. 2-(4, 4, 1)-tablica ortogonalna i jej przekształcenie na zbiór przypadków testowych Po pierwsze, zauważmy, że tablica z rysunku 8.29a jest rzeczywiście 2 – (4, 4, 1)tablicą ortogonalną. W każdej kolumnie występuje jedna z 4 liczb oraz każda para wartości w dowolnych dwóch kolumnach występuje dokładnie raz. Kolumny mogą teraz reprezentować nasze charakterystyki, odpowiednio: drukarkę, przeglądarkę, system operacyjny i bazę. Wprowadźmy następujące mapowanie wartości tablicy ortogonalnej na wartości charakterystyk: drukarki: HP=1, Epson=2, Samsung=3, Canon=4;
przeglądarki: IE=1, Firefox=2, Chrome=3; systemy operacyjne: Windows=1, Linux=2, iOS=3; bazy danych: MySQL=1, PostgreSQL=2. Tablica ortogonalna w każdej kolumnie wykorzystuje 4 liczby, dlatego niektóre z nich są dla nas nieistotne: wartość 4 dla przeglądarki i systemu operacyjnego oraz 3 i 4 dla baz danych. W miejsca tych liczb wpisujemy gwiazdki, które symbolizują dowolną wartość z zakresu zmiennej (rys. 8.29b). Zauważmy, że w tak powstałej tablicy możemy, wykorzystując możliwość użycia wartości dowolnych, połączyć niektóre wiersze. Na przykład, wstawiając wartości 1, 1, 1 w miejsce gwiazdek wierszu B = (1,*,*,*), otrzymamy wiersz A. Możemy zatem połączyć oba wiersze w wiersz AB = (1,1,1,1). Wstawiając 1 w C = (2,1,*,2) oraz 1 i 2 w D = (2,*,1,*), otrzymamy taki sam wiersz CD = (2,1,1,2). Wstawiając 2 i 1 w E = (4,2,*,*) oraz 2 w F = (4,*,2,1), dostajemy wiersz EF = (4,2,2,1). 16 wierszy udało nam się zredukować do 13 (rys. 8.29c). Wystarczy już tylko zastąpić liczby odpowiadającymi im wartościami charakterystyk (rys. 8.29d). Pozostałe gwiazdki możemy uzupełnić dowolnymi wartościami – nie mają one żadnego wpływu na spełnienie kryterium. Czytelnik może sprawdzić, że tak powstała suita spełnia kryterium pokrycia par. Typy konfiguracji oznacza się, stosując specjalną notację. Jeśli mamy αi zmiennych o i możliwych wartościach dla 2 ≤ i ≤ N, to taki typ zapisuje się jako Konfigurację z naszego przykładu (jedna zmienna o 4 wartościach, dwie zmienne o 3 wartościach i jedna zmienna o 2 wartościach) można zatem zapisać jako 21 32 41. W sieci można znaleźć repozytoria zawierające minimalne (lub najmniejsze do tej pory znalezione) zestawy testów dla wielu typów konfiguracji, np. [109].
8.8.6. Pokrycie n-tupletów (n-wise) Kryterium pokrycia par można uogólnić na dowolne tuplety. Na przykład pokrycie trójek (3-wise) wymaga, by każda kombinacja dowolnych wartości trzech dowolnych charakterystyk była pokryta przynajmniej przez jeden przypadek testowy. Szczególne dwa przypadki graniczne to pokrycie 1-wise, które jest dokładnie tym samym, co kryterium Each Choice, oraz pokrycie k-wise dla k równego liczbie wszystkich charakterystyk, które jest dokładnie tym samym, co pełne pokrycie kombinatoryczne omówione w kolejnym podrozdziale.
Można w łatwy sposób pokazać, że kryterium n-wise (pokrycie n-tupletów) subsumuje pokrycie k-wise (k-tupletów) dla każdego k < n. Oczywiście im większe n tym więcej przypadków testowych będzie potrzebnych, aby spełnić kryterium. Przy tworzeniu testów dla pokrycia n-tupletów można, tak jak w przypadku kryterium pokrycia par, stosować trzy wymienione wcześniej podejścia. Aby zastosować tablice ortogonalne dla kryterium n-tupletów, musimy wykorzystać n – (v, k, 1)-tablice ortogonalne, gdzie k jest liczbą charakterystyk, a v jest rozmiarem maksymalnej charakterystyki.
8.8.7. Pełne pokrycie kombinatoryczne Ostatnim z omówionych pokryć kombinatorycznych jest pełne pokrycie kombinatoryczne, które subsumuje wszystkie wymienione kryteria kombinacyjne. Wymaga ono przetestowania wszystkich kombinacji wartości wszystkich charakterystyk. Dla naszego przykładu musielibyśmy zbudować 4 ⋅ 3 ⋅ 3 ⋅ 2 = 72 przypadki testowe. Kryterium to można stosować, gdy liczba charakterystyk oraz ich możliwych wartości jest niewielka. Rozmiar wymaganej do spełnienia kryterium suity testowej rośnie bowiem wykładniczo wraz ze wzrostem liczby charakterystyk oraz liczby wartości tych charakterystyk. Kryteria kombinacyjne najczęściej wykorzystuje się przy interakcji co najwyżej dwóch czynników. Wyższe rzędy kombinacji zwykle nie pozwalają na wykrycie większej liczby błędów niż w przypadku kryterium pokrycia par. W tabeli 8.36 przedstawiono podsumowanie metod kombinacyjnych. pokrycie kombinacyjne (ang. combinatorial coverage) – odesetek pokrytych testami kombinacji warunków testowych. Warunki testowe są definiowane w zależności od typu przyjętej metody kombinacyjnej, np. Each Choice, Base Choice, Multiple Base Choice, Pair-wise Tabela 8.36. Podsumowanie metod kombinacyjnych
Warunki tes towe
Parametry z okreś lonymi wartoś ciami (jeden parametr z jedną wartoś cią odpowiada jednemu warunkowi tes towemu)
Elementy Kombinacje warunków tes towych, zależnie od metody (np. pokrycia w Each Choice elementy pokrycia s ą wpros t warunkami tes towymi) Dla każdej zidentyfikowanej kombinacji warunków Kryterium tes towych is tnieje tes t, któreg o dane wejś ciowe realizują tę pokrycia kombinację Pokrycie
p=
liczba pokrytych tes tami elementów pokrycia liczba ws zys tkich elementów pokrycia
× 100%
8.8.8. Subsumpcja kryteriów kombinacyjnych Na rysunku 8.30 przedstawiono relacje między kombinacyjnymi kryteriami pokrycia. Lewa gałąź wykresu tak naprawdę powinna przedstawiać cały łańcuch subsumpcji, ponieważ kryterium 2-wise (pokrycia par) subsumowane jest przez pokrycie 3-wise, to z kolei przez pokrycie 4-wise itd. Podobna sytuacja dotyczy gałęzi prawej: pokrycie wyborów bazowych jest subsumowane przez kryterium pokrycia wyboru dla dwóch warunków bazowych, to z kolei – przez kryterium wyboru dla trzech warunków bazowych itd.
Rysunek 8.30. Hierarchia subsumpcji dla kombinacyjnych kryteriów pokrycia
8.9. Testowanie dziedziny
8.9.1. Opis metody Testowanie dziedziny jest uogólnieniem technik podziału na klasy równoważności oraz
wartości
granicznych.
Podejście
to
stosuje
się
wtedy,
gdy
klasy
równoważności dziedziny wejściowej są wielowymiarowe, tzn. gdy granica klasy równoważności zależy od kombinacji więcej niż jednej zmiennej. Gdy zmiennych jest n, mówimy o n-wymiarowej dziedzinie wejściowej. Zanim przejdziemy do dokładnego omówienia metody, wprowadzimy niezbędne pojęcia z zakresu geometrii euklidesowej i algebry liniowej, aby dobrze zrozumieć ideę stojącą za kryterium pokrycia punktami ON i OFF omówionym w punkcie 8.9.6. Rozważymy najpierw
przypadek
ograniczeń
liniowych,
ponieważ
są
one
najczęściej
spotykanym typem ograniczeń. Na zakończenie przedyskutujemy przypadek nieliniowy. analiza dziedziny (ang. domain analysis) – czarnoskrzynkowa technika projektowania testów używana do identyfikacji skutecznych i efektywnych przypadków testowych, kiedy wiele zmiennych może lub powinno być testowanych razem; bazuje na metodach: podziału na klasy równoważności oraz analizie wartości brzegowej i uogólnia je do przypadku wielowymiarowego
8.9.2. Hiperpłaszczyzny i podprzestrzenie Równania i nierówności zawierające wiele zmiennych wyznaczają różnego rodzaju obiekty geometryczne. Warunek (liniowy) postaci a 1 x1 + a 2 x2 + ··· + a n xn = b, gdzie a i oraz b są ustalonymi liczbami, wyznacza w n-wymiarowej przestrzeni
tzw. hiperpłaszczyznę, czyli podzbiór wyjściowej przestrzeni o wymiarze n – 1. Na przykład na dwuwymiarowej płaszczyźnie równanie a 1 x1 + a 2 x2 = b wyznacza prostą (czarna prosta na rys. 8.31 z lewej strony), czyli obiekt jednowymiarowy. W trójwymiarowej przestrzeni warunek a 1 x1 + a 2 x2 + a 3 x3 = b wyznacza dwuwymiarową płaszczyznę (na rys. 8.31 z prawej oznaczona szarym kolorem), czyli obiekt dwuwymiarowy.
Rysunek 8.31. Hiperpłaszczyzny i podprzestrzenie w przestrzeni dwui trójwymiarowej Gdybyśmy zamiast równości wzięli nierówność, to takie warunki dzielą przestrzeń wyjściową na dwie podprzestrzenie o tym samym wymiarze. Na przykład nierówność a 1 x1 + a 2 x2 ≥ b dzieli dwuwymiarową przestrzeń na dwie części, leżące po obu stronach prostej a 1 x1 + a 2 x2 = b. Na rysunku 8.31 jedna z nich
oznaczona jest kolorem szarym. Nierówność a 1 x1 + a 2 x2 + a 3 x3 ≥ b dzieli trójwymiarową przestrzeń na dwie podprzestrzenie, leżące po różnych stronach płaszczyzny a 1 x1 + a 2 x2 + a 3 x3 = b. Metoda punktów ON-OFF którą opiszemy dalej dotyczy – tak jak analiza wartości brzegowych – punktów brzegowych. Musimy zatem dobrze zdefiniować pojęcie brzegu obszaru. Dla dziedzin wyznaczanych nierównościami nieostrymi
sprawa jest prosta: brzegiem obszaru wyznaczonego warunkiem a 1 x1 + ··· + a n xn ≥ b jest zbiór B punktów spełniających warunek Jeśli jednak zamienimy nierówność na ostrą, to zbiór B wyznacza brzeg przestrzeni dopełniającej. Na przykład, dla warunku x1 – x2 > 0 zbiór punktów spełniających go jest oznaczony kolorem szarym na rysunku 8.32. Brzeg, czyli zbiór punktów spełniających warunek x1 = x2, nie należy jednak do tego obszaru, ale do obszaru dopełniającego, czyli zbioru punktów spełniających warunek x1 – x2 ≤ 0. Konwencja, którą zwykle stosuje się przy oznaczaniu brzegów jest następująca: jeśli obszar jest „domknięty” i zbiór B do niego należy, to oznacza się go linią ciągłą. Jeśli obszar jest „otwarty” i B do niego nie należy, to oznacza się go linią przerywaną, tak jak na rysunku 8.32. W przestrzeni co najmniej dwuwymiarowej brzeg nie jest już pojedynczą liczbą, lecz jakąś podprzestrzenią przestrzeni wyjściowej. Powstaje następujący problem:
jeśli obszar O jest „otwarty”, to które punkty są jego własnym brzegiem, skoro B jest brzegiem przestrzeni dopełniającej? Intuicyjnie, będą to punkty należące do O i leżące możliwie blisko punktów z B. Jak jednak zdefiniować pojęcie bliskości w więcej niż jednym wymiarze, w dodatku w sytuacji, gdy przestrzeń reprezentowana w komputerze jest dyskretna (najmniejsza zmiana współrzędnej punktu to epsilon maszynowy)? Można tu oczywiście wykorzystać pojęcia z zakresu geometrii analitycznej, wprowadzać formalnie metryki na przestrzeniach wielowymiarowych itd., ale byłoby to zbyt skomplikowane. Tester powinien znajdować możliwie najprostsze działające i rozsądne rozwiązania. Jeśli precyzja reprezentacji liczb nie jest jakoś specjalnie istotna przy testowaniu danego ograniczenia, to można za brzeg obszaru wyznaczonego warunkiem ∑ i a i xi > b przyjąć zbiór punktów spełniających równanie ∑ i a i xi = b + ε, gdzie ε jest odpowiednio małą liczbą, wyznaczającą poziom precyzji, poniżej którego tester nie musi schodzić. Zakładamy wtedy, że każdy punkt spełniający ten warunek leży na brzegu rozważanego obszaru.
Rysunek 8.32. Obszar z brzegiem „otwartym” Jeśli precyzja reprezentacji jest istotna, to za brzeg możemy przyjąć wszystkie takie punkty (x1, x2, … xn) spełniające a 1 x1 + … a n xn > b, że zmiana któregokolwiek z nich powoduje, że nowy punkt już do obszaru O nie należy.
Formalnie, brzegiem będzie taki zbiór punktów (x1, x2, … xn), że dla każdego i ∈ {1, …, n} zachodzi jedno z dwóch poniższych równań: a 1 x1 + … + a i–1 xi–1 + a i (xi + ε) + a i+1 xi+1 + … + a nxn ≤ b lub a 1 x1 + … + a i–1 xi–1 + a i (xi – ε) + a i+1 xi+1 + … + a nxn ≤ b gdzie ε jest epsilonem maszynowym, czyli dokładnością maszynową reprezentacji liczb w danym systemie.
8.9.3. Wyznaczanie hiperpłaszczyzny przez punkty Ile punktów potrzeba, by wyznaczyć prostą w przestrzeni dwuwymiarowej? Odpowiedź jest oczywista: wystarczą dwa punkty. Reguła ta działa także dla wyższych wymiarów: aby w przestrzeni n-wymiarowej wyznaczyć hiperpłaszczyznę, czyli podprzestrzeń o wymiarze n – 1, wystarczy n punktów, które nie leżą w podprzestrzeni o wymiarze n – 2. Czyli np. płaszczyznę w przestrzeni 3-wymiarowej można jednoznacznie wyznaczyć za pomocą 3 punktów nieleżących na jednej prostej (to dlatego najbardziej stabilnymi krzesłami są te o trzech, a nie czterech nogach!). Rozważania te przydadzą się w zrozumieniu idei metody punktów ON-OFF. Dzięki tej obserwacji będziemy w stanie – przy sensownych założeniach co do testowanego warunku – wykazać, że można przetestować poprawność tego warunku przy odpowiednio niewielkiej liczbie przypadków testowych.
8.9.4. Punkty IN, OUT, ON i OFF Wprowadzimy teraz cztery kategorie punktów względem rozważanego obszaru O. Punkty te będą wykorzystane w technikach IN-OUT oraz ON-OFF. Posłużymy się przykładem z rysunku 8.33. Załóżmy, że naszym obszarem jest szary trójkąt „bez jednego brzegu”, wyznaczony warunkami:
(1) –4/5a + b < –2/5 (2) a ≤ 3 (3) b ≥ –2
Rysunek 8.33. Przykłady punktów IN, OUT, ON i OFF dla testowanego obszaru Definiujemy następujące kategorie punktów: punkt ON – to punkt leżący na brzegu rozważanego obszaru O; punkt OFF – to punkt leżący na brzegu obszaru sąsiadującego z O; punkt IN – to punkt leżący wewnątrz O i nieleżący na brzegu O; punkt OUT – to punkt leżący poza O i nieleżący na brzegu obszaru sąsiadującego z O. Brzeg wyznaczony warunkiem –4/5a + b = –2/5, nie należy do trójkąta, zatem punkt leżący na tej prostej jest punktem OFF. Punkt (3, –2) leżący w prawym dolnym rogu jest częścią wspólną dwóch brzegów trójkąta, zatem jest punktem ON. Punkt (3, 2) w prawym górnym rogu jest punktem granicznym, ale nie leży na
brzegu trójkąta, ponieważ nie spełnia warunku (1) – jest zatem punktem OFF. Linia ciągła tuż poniżej prostej –4/5a + b = –2/5 oznacza brzeg trójkąta. Punkt na niej leżący jest punktem ON. Punkt w środku trójkąta nie należy do jego brzegu, jest więc punktem IN. Punkt w lewej górnej ćwiartce układu leży poza trójkątem, ale nie na brzegu obszaru z nim graniczącym – jest zatem punktem OUT. Punkty IN oraz OUT posłużą nam do konstrukcji techniki uogólniającej metodę podziału na klasy równoważności na przypadek wielowymiarowy. Punkty ON i OFF – do konstrukcji techniki uogólniającej metodę analizy wartości brzegowych. Warunki opisujące ograniczenia stają się warunkami testowymi. Punkty IN/OUT (odpowiednio ON/OFF) stają się elementami pokrycia dla testowania dziedziny oraz wartości brzegowych w przestrzeniach wielowymiarowych.
8.9.5. Strategia IN-OUT dla testowania dziedziny Strategia testowania dziedziny jest bardzo prosta. Dla każdego z ograniczeń definiujemy jeden punkt IN oraz jeden punkt OUT. Następnie, dla każdego punktu tworzymy przypadek testowy. Ten sam punkt może być wykorzystany dla dwóch lub więcej ograniczeń.
8.9.6. Strategia N-ON × M-OFF dla testowania wartości brzegowych Strategia testowania wartości granicznych dziedziny polega na przetestowaniu każdego z ograniczeń osobno przez wykorzystanie N punktów ON i M punktów OFF dla każdego ograniczenia. Rozmiar suity testowej będzie zależał więc od wyboru N oraz M. Dla K ograniczeń liczba przypadków testowych będzie wynosić K(N + M). Najprostsza strategia to 1 × 1 – każde ograniczenie testujemy za pomocą jednego punktu ON i jednego punktu OFF. Możemy również wykorzystać tylko same punkty ON. Zastanówmy się, jak powinna wyglądać optymalna suita testowa, aby z jednej strony nie generować zbyt dużej liczby przypadków, a z drugiej – aby dobrze przetestować warunki.
Rysunek 8.34. Zdolność punktów ON-OFF do wykrywania błędów dziedziny Na rysunku 8.34 jest pokazana dziedzina wyznaczona równaniem x1 – x2 ≥ 0. Jej brzeg jest oznaczony czarną linią. Załóżmy, że pomyłka programisty polega na zaimplementowaniu błędnego warunku, którego brzeg – pokazany linią kropkowaną – lekko odbiega od poprawnego. Strategia 1 × 1 nie zawsze doprowadzi do wykrycia błędu – wybierając punkty ON i OFF tak jak na rysunku 8.34a, nie wykryjemy naruszenia warunku. Zarówno w przypadku poprawnego, jak i błędnie zaimplementowanego ograniczenia punkt 1 (ON) zawsze leży w szarym obszarze, a punkt 2 (OFF) zawsze leży poza nim. Gdyby jednak punkt 1 leżał blisko punktu 2, wtedy wykrylibyśmy błąd, bo byłby on punktem granicznym poprawnego brzegu, ale leżałby poza obszarem wyznaczonym brzegiem błędnym. Taka sytuacja jest pokazana na rysunku 8.34b. Zauważmy, że pozycja punktu ON nie ma większego znaczenia, jeśli tylko punkt OFF jest blisko niego. Błąd zostanie wykryty w przypadku użycia zarówno pary punktów 3, 4, jak i 5, 6. Jedyna problematyczna sytuacja powstałaby, gdyby punkt ON leżał blisko przecięcia się obu brzegów. Wtedy punkt OFF dla poprawnej dziedziny mógłby również być punktem OFF dla niepoprawnej. Jednak prawdopodobieństwo, że trafimy punktem ON blisko punktu przecięcia, jest znikome. Zakładając, że błędne ograniczenie jest zaimplementowane jako warunek liniowy, można wręcz udowodnić, że podejście 1 × 1 z dużym prawdopodobieństwem wykryje błąd, jeśli tylko punkty ON i OFF będą leżały blisko siebie. Ile punktów potrzeba w przypadku przestrzeni o większym wymiarze, n > 2? Z pomocą przychodzą nam obserwacje poczynione w punkcie 8.9.3. Brzeg zwykle jest hiperpłaszczyzną, a ta jest wyznaczana przez n punktów. Jeśli zatem ustawimy n punktów ON, to powinny one dokładnie wyznaczyć brzeg.
Mogą one wszystkie również należeć do obszaru wyznaczonego przez błędnie zaimplementowany warunek. Ale jeśli tylko punkty ON będą leżeć blisko siebie i przesuniemy jeden z nich o niewielką wartość tak, aby stał się punktem OFF, to przyjmując strategię (n – 1) × 1, będziemy w stanie wykryć błąd dziedziny, w analogiczny sposób, jak to opisaliśmy w przypadku dwuwymiarowym. Jeśli chcemy mieć swobodę w wyborze punktów ON i OFF (np. z jakichś powodów nie chcemy ich definiować blisko siebie), to przedstawiona strategia nie zawsze się sprawdzi (patrz problem z rys. 8.34a). Jeżeli brzeg jest n – 2-wymiarową hiperpłaszczyzną w przestrzeni n-wymiarowej, to można przyjąć strategię n × 1, tzn. wybrać n punktów ON leżących na brzegu tak, aby wyznaczały go jednoznacznie16, oraz jeden punkt OFF. Taka sytuacja jest pokazana na rysunku 8.43c. Ponieważ n = 2, wybieramy w dowolny sposób 2 punkty ON tak, aby jednoznacznie definiowały brzeg. Następnie dodajemy, znów w dowolny sposób, 1 punkt OFF. Jeśli błędna implementacja warunku ma postać liniową, to strategia n × 1 zawsze wykryje błąd. Jeśli testowany warunek ma postać równości, to sprawa jest jeszcze prostsza – do przetestowania warunku k-wymiarowego w przestrzeni n-wymiarowej, n > k wystarczy k punktów ON, rozmieszczonych w sposób dowolny na brzegu określonym testowaną równością. Wyznaczają one bowiem jednoznacznie hiperpłaszczyznę w przestrzeni k + 1-wymiarowej. Każda inna hiperpłaszczyzna, w szczególności błędnie zaimplementowana – o ile została zaimplementowana przy użyciu operatora równości – nie będzie zawierać wszystkich k punktów ON. Programista mógłby jednak omyłkowo zastosować w warunku operator relacyjny zamiast równości. Wtedy same punkty ON mogą nie wystarczyć, bo kwymiarowy brzeg może być po prostu zawarty w błędnie zdefiniowanej podprzestrzeni k + 1-wymiarowej. W takiej sytuacji można ponownie zastosować strategię (k – 1) × 1, czyli wziąć k punktów ON leżących blisko siebie i przesunąć minimalnie jeden z nich tak, aby stał się punktem OFF.
Rysunek 8.35. Redukcja liczby punktów ON i OFF dla kombinacji ograniczeń Jeżeli mamy do czynienia z większą liczbą ograniczeń, to możemy zredukować liczbę punktów ON i OFF, a zatem i liczbę testów, w następujący sposób. Jeśli dwa ograniczenia mają punkt wspólny jak na rysunku 8.35, to tworzymy punkt w miejscu ich przecięcia, który jest ON dla obu warunków. W jego pobliżu drugi punkt tak, by był punktem OFF dla obu warunków. Taka metoda pozwala nam zredukować liczbę testów z czterech (po dwa punkty na każde ograniczenie) do dwóch. pokrycie dziedziny (ang. domain coverage) – odsetek pokrytych testami punktów IN/OUT lub ON/OFF, zdefiniowanych w zależności od typu metody. Tabela 8.37. Podsumowanie metod analizy dziedziny
W arunki testowe
Podzbiory dziedziny (dla strategii IN- OUT ) lub ograniczenia (dla strategii ON- OFF)
Elementy pokrycia
Punkty IN i OUT (dla s trateg ii IN-OUT) lub punkty ON i OFF (dla s trateg ii ON-OFF)
Kryterium pokrycia
Dla każdeg o punktu IN i OUT (odpowiednio ON i OFF) is tnieje tes t wykorzys tujący tę wartoś ć
Pokrycie
liczba wykorzys tanych w tes tach punktów p IN/OUT (lub ON/OFF) × = 100% liczba ws zys tkich punktów IN/OUT (lub ON/OFF)
W tabeli 8.37 podsumowano metodę analizy dziedziny. Podczas liczenia pokrycia nie powinno się wliczać ani do licznika, ani do mianownika wzoru zduplikowanych punktów, które są wykorzystywane dla więcej niż jednego ograniczenia.
8.9.7. Ograniczenia nieliniowe Ograniczenia nieliniowe to takie, których nie da się wyrazić jako kombinacja liniowa poszczególnych zmiennych. Ograniczenia te łatwo poznać po tym, że wyznaczają hiperpłaszczyzny lub podprzestrzenie, które nie są „liniowe”. Na przykład równanie x2 + y2 = 16 wyznacza granicę w kształcie okręgu o promieniu 4. Dla ograniczeń nieliniowych nie ma uniwersalnej teorii, która mówiłaby, ile co najmniej punktów ON i OFF należy użyć w testowaniu. Sprawa jest prostsza, jeśli zawęzimy nasze rozważania do pewnych klas ograniczeń. Jeśli na przykład wiemy, że ograniczenia są implementowane jako równania okręgów, to wystarczą trzy punkty ON, aby zweryfikować, czy ograniczenie rzeczywiście jest poprawnym okręgiem (bo 3 punkty jednoznacznie wyznaczają okrąg). Jeśli używanym ograniczeniem jest wielomian stopnia n, to wystarczy n + 1 punktów ON, gdyż z algebry wiadomo, że jest on jednoznacznie identyfikowany przez n + 1 punktów. Generalnie im „większa nieliniowość”, tym więcej powinniśmy użyć punktów ON i OFF. Oczywiście, tak jak w przypadku innych technik, na decyzję o wyborze strategii wpływają poziom przyjętego i akceptowanego ryzyka, czas, zasoby itd. Nie ma tu jednego, uniwersalnego rozwiązania.
8.9.8. Przykład
Rozporządzenie ministra gospodarki z dnia 21 grudnia 2005 roku w sprawie zasadniczych wymagań dla urządzeń ciśnieniowych i zespołów urządzeń ciśnieniowych opisuje kategorię, jaką należy przypisać zbiornikowi na podstawie dwóch parametrów: objętości V (w litrach) oraz ciśnienia PS (w barach). Kategoria jest przyznawana na podstawie relacji, jakie zachodzą między parametrami. Są one przedstawione w tabeli 8.38. Tabela 8.38. Kategoryzacja urządzeń ciśnieniowych w zależności od dopuszczalnego ciśnienia i pojemności
Kategoria Zakres parametrów wg §11
0.5 bar < PS ≤ 200 bar ∧ V ≤ 1 l lub PS > 0.5 bar ∧ V > 1.0 l ∧ PS ⋅ V ≤ 25 bar ⋅ l
I
PS > 0.5 bar ∧ V > 1.0 l ∧ 25 bar ⋅ l< PS ⋅ V ≤ 50 bar ⋅ l
II
PS > 0.5 bar ∧ V > 1.0 l ∧ 50 bar ⋅ l< PS ⋅ V ≤ 200 bar ⋅ l
III
PS > 0.5 bar ∧ V > 1.0 l ∧ 200 bar ⋅ l< PS ⋅ V ≤ 1000 bar ⋅ l lub 200 bar< PS ≤ 1000 bar ∧ V ≤ 1.0 l
IV
0.5 bar< PS ≤ 1000 bar ∧ PS ⋅ V > 1000 bar ⋅ l lub PS > 1000 bar
Graficzna reprezentacja tych zależności jest pokazana na rysunku 8.36 z lewej strony. Brzegi należą do regionów leżących pod nimi lub po ich lewej stronie. Chcemy przetestować program Kategoryzacja, który przyznaje kategorię na podstawie dwóch parametrów wejściowych: V oraz PS. Ze względu na specyfikę problemu, zastosowanie analizy dziedziny jest niewystarczające – musimy przetestować również warunki brzegowe. Dziedzina jest dwuwymiarowa, a ograniczenia mają postać liniową, dlatego decydujemy się na strategię 1 × 1 oraz definiowanie punktów ON i OFF blisko siebie. Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden element testów – program Kategoryzacja.
ET1: Kategoryzacja.
Rysunek 8.36. Graficzna reprezentacja warunków dla kategoryzacji urządzeń ciśnieniowych Krok 2. Wyprowadzenie warunków testowych. Mamy do przetestowania pięć obszarów (§11, I, II, III, IV), z których każdy jest zdefiniowany przez kilka warunków ograniczających. Gdybyśmy mieli pewność, że każdy warunek zaimplementowany jest w programie w jednym miejscu, to wystarczyłoby sprawdzić 8 ograniczeń zebranych w tabeli 8.38. Nie mamy jednak takiej pewności. Dlatego dla każdego obszaru i każdego ograniczenia musimy przeprowadzić oddzielną analizę. Mamy 13 takich ograniczeń wyznaczających brzegi poszczególnych obszarów. Każde z nich jest oznaczone czarną kropką na rysunku 8.36 z prawej strony. Będziemy się do nich odwoływać przez stosowanie notacji WT1, WT2, …, WT13. Krok 3. Wyprowadzenie elementów pokrycia. Dla każdego warunku testowego musimy stworzyć dwa elementy pokrycia, odpowiadające granicom sąsiadujących ze sobą obszarów. Każdy element pokrycia będzie się składać z jednego punktu ON i jednego OFF. Wydaje się więc, że dla jednego warunku musimy stworzyć 4 punkty – po 2 dla każdej z sąsiadujących ze sobą granic. Ale właśnie ze względu na sąsiedztwo obszarów, wykorzystamy punkt ON jednego z nich jako punkt OFF obszaru sąsiadującego. W ten sposób zredukujemy liczbę
potrzebnych punktów z 52 do 26. Na rysunku 8.36 z prawej strony każda kropka odpowiada dwóm punktom. Jeden z nich zawsze leży na odpowiedniej granicy, drugi – tuż obok – należy do brzegu obszaru sąsiadującego z tą granicą. Jest 13 warunków testowych, mamy więc 26 punktów. Będziemy się do nich odwoływać, używając numeru warunku oraz litery A lub B. Na przykład para punktów odpowiadających warunkowi WT6 to 6A i 6B. Literą A oznaczymy punkty leżące na granicach pokazanych na rysunku. Literą B – punkty leżące na granicach sąsiadujących obszarów. Zauważmy, że dla przetestowania granicy V = 1 obszaru §11 wystarczy nam tylko jedna spośród par punktów odpowiadających WT3 i WT4. Oba te wymagania są nam jednak potrzebne – jedno dla przetestowania jednej z granic obszaru I, a drugie – obszaru II. Mamy ostatecznie następujących 26 elementów pokrycia EP1A, EP1B, EP2A, EP2B, …, EP13A, EP13B. Należy pamiętać, że testowane warunki mają nałożone ograniczenie na wartości zmiennych. Na przykład warunek nr 8 możemy sprawdzać dla 1 ≤ V ≤ 100 i 0.5 ≤ PS ≤ 50. Krok 4. Zdefiniowanie przypadków testowych. Przyjmijmy
akceptowaną
dokładność pomiaru jako ε = 0.1, zarówno w stosunku do ciśnienia, jak i pojemności. Tabela 8.39 w poszczególnych kolumnach zawiera przypadki testowe. Dla każdego z nich są podane wartości PS oraz V, a także informacja o tym, który punkt jest ON, a który OFF dla poszczególnych ograniczeń. Ze względu na oszczędność miejsca pominięto niektóre wiersze i kolumny. Zauważmy, że każde ograniczenie zostało pokryte punktem ON i punktem OFF. Podczas analizy tego przykładu tester powinien zwrócić również uwagę na brak jakichkolwiek informacji o przypadkach, w których V < 0.1 l, mimo wyraźnego oznaczenia tego faktu w specyfikacji na wykresie. Prawdopodobnie zbiorniki o pojemności mniejszej niż 1/10 litra są zbyt małe na to, aby podlegały kategoryzacji. Niemniej jednak tester powinien się co do tego upewnić i potwierdzić swoje domysły. Dodatkowo, tester może również skupić się na przetestowaniu punktów leżących na przecięciu dwóch lub większej liczby prostych opisujących warunki liniowe. Powoduje to powstanie dodatkowych warunków testowych, WT14–WT24, odpowiadających punktom 14–24 przedstawionym na rysunku 8.37. Dla każdego
z nich można stworzyć jeden przypadek testowy, odpowiadający dokładnie współrzędnym danego punktu. Tabela 8.39. Przypadki testowe dla programu Kategoryzacja
PT 1 PT 2
... PT 15 PT 16 ... PT 25
PT 26
V
0.5
... 10
PS
1000 1000.1 ... 5
1A PS ≤ 1000
ON
1B PS > 1000
OFF ON
0.5
OFF
10.1
... 10000
10000
5
... 0.5
0.6
...
...
...
...
... 8A PS · V ≤ 50
... ON
OFF
...
8B PS · V > 50
... OFF
ON
...
... 13A PS ≤ 0.5
...
... ON
OFF
13B PS > 0.5
...
... OFF
ON
oczekiwane wyjś cie III
IV
... I
II
... niezdef. IV
Rysunek 8.37. Warunki testowe na połączeniach linii
8.10. Testowanie oparte na przypadkach użycia 8.10.1. Opis metody Testerzy lubią pracować z przypadkami użycia, ponieważ są one gotowym opisem testów funkcjonalnych. W najprostszym przypadku tester otrzymuje przypadek użycia i po prostu go przeprowadza, sprawdzając, czy wszystko odbywa się według założonego w przypadku scenariusza. Często rola testerów nie
sprowadza się jedynie do funkcji biernego odbiorcy gotowego przypadku użycia – tester może (i powinien) pracować nad jego powstawaniem, aby już na etapie prac projektowych wykrywać usterki, nieścisłości oraz złą formę utrudniającą testowalność przypadku użycia. przypadek użycia (ang. use-case) – ciąg transakcji w dialogu między uczestnikami (użytkownikami bądź systemami) skutkujących osiągnięciem założonego celu wszystkich uczestników Przypadek użycia określa umowę między uczestnikami systemu względem jego zachowania [110]. Każdy uczestnik ma cel, jaki chce osiągnąć w danym przypadku użycia. Uczestnicy są nazywani aktorami. Aktor główny to ten uczestnik, z którego perspektywy jest opisywany przypadek użycia. Cel aktora głównego jest nazywany celem głównym. Najczęściej przypadki użycia przedstawia się w formie tekstowej, choć można używać innych form graficznych, takich jak diagramy czy sieci Petriego. Hipoteza błędu w tej metodzie mówi, że błędy powstają na skutek nieprawidłowej implementacji kodu obsługującego sekwencje interakcji między aktorami. aktor (ang. actor) – użytkownik, inna osoba lub system wchodzący w określony sposób w interakcję z testowanym systemem Przypadek użycia musi określać tzw. scenariusz główny (czyli typowy ciąg akcji, prowadzący do szczęśliwego zakończenia interakcji między aktorami). Mogą również występować tzw. scenariusze alternatywne, opisujące interakcję w przypadku wystąpienia jakichś problemów. Poprawnie napisany przypadek użycia, to taki, w którym: występują jasno zdefiniowani aktorzy (np. użytkownik i system); aktorzy mają dobrze sprecyzowane cele; uwzględniono fakt, że cele mogą się nie powieść; występuje nie więcej niż kilkanaście kroków głównego scenariusza; podano minimalną gwarancję, tzn. najmniejsze obietnice, które system składa uczestnikom (istotne zwłaszcza w przypadku niepowodzenia celu aktora głównego).
Wszystkie te cechy przypadku użycia mogą podlegać testowaniu, przyjmującemu najczęściej formę testowania statycznego – przeglądu lub inspekcji. Testowanie samego przypadku użycia polega na przetestowaniu scenariusza głównego oraz wszystkich scenariuszy alternatywnych. Zarówno warunkami pokrycia, jak i elementami pokrycia są scenariusze (główny oraz alternatywne). Każdy przypadek testowy powinien pokrywać jeden scenariusz. Podczas przeprowadzania testów można również mierzyć pokrycie kroków scenariuszy.
8.10.2. Przykład Rozważmy bankomat, którego jedną z funkcji jest tzw. szybka wypłata pieniędzy (50 PLN) przez użytkownika. Polega ona na tym, że klient po autoryzacji w systemie może wybrać opcję szybkiej wypłaty, nie przechodząc przez ekran wpisywania kwoty oraz pytania o wydrukowanie potwierdzenia transakcji. Przypadek użycia SzybkaWypłata jest opisany w tabeli 8.40. Zauważmy, że każdy krok każdego scenariusza jest identyfikowany inną liczbą. Pozwala to na ścisłe odwoływanie się do poszczególnych kroków. Gdybyśmy kroki scenariuszy alternatywnych numerowali tak, jak kroki scenariusza głównego (1, 2, 3 …), wprowadzilibyśmy niejednoznaczność, a co za tym idzie – zamieszanie. Przypadek nie opisuje scenariusza alternatywnego dla kroku 6 (wypłata innego rodzaju), ponieważ skupimy się na funkcjonalności szybkiej wypłaty. Wszystkie rozszerzenia przypadku użycia są opisane w końcowej sekcji z dodaną informacją, który przypadek użycia omawia dany wariant. Zanim tester przeprowadzi testowanie na podstawie dostarczonego przypadku użycia, może dokonać przeglądu przypadku i zwrócić uwagę na kilka kwestii, na przykład: nie uwzględniono, że klient może włożyć kartę do bankomatu w chwili, gdy nie wyświetla on ekranu powitalnego, tylko jakiś inny ekran; między krokami 8.2 a 8.3 nie ma żadnej informacji dla klienta; upewnić się, czy po nieodebraniu gotówki stan konta się nie zmienia (tzn. czy spełniona jest minimalna gwarancja) – być może powinno to być napisane wyraźnie w przypadku użycia; Tabela 8.40. Przypadek użycia dla szybkiej wypłaty z bankomatu
Przypadek użycia
PU004 Sz ybkaWypłata
Cel
Umożliwienie s zybkiej wypłaty 50 PLN z bankomatu
Aktorzy
klient, bankomat
W yzwalacz
Włożenie karty do bankomatu
W arunek wstępny
Pos iadanie karty bankomatowej
Minimalna gwarancja
Zapis anie ws zys tkich akcji w dzienniku (log u). W przypadku nieudanej trans akcji s tan konta s ię nie zmieni.
Nazwa scenariusza
S cenarius z g łówny – s zybka wypłata
Krok Akcja 1
Klient wkłada kartę do bankomatu
2
S ys tem identyfikuje klienta
3
S ys tem pros i o podanie numeru PIN
4
Użytkownik wprowadza poprawny PIN
5
S ys tem wyś wietla ekran opcji
6
Użytkownik wybiera opcję „ S zybka wypłata 50 PLN”
7
S ys tem s prawdza s tan konta, wypłaca 50 PLN, uaktualnia s tan konta i wyś wietla komunikat „ pros zę odebrać g otówkę”
8
Użytkownik odbiera g otówkę
S cenarius z alternatywny – zła karta
9
S ys tem zwraca kartę i wyś wietla komunikat „ pros zę odebrać kartę”
10
Użytkownik odbiera kartę
11
S ys tem zamyka trans akcję i wyś wietla ekran powitalny s przed kroku 1
2.1
S ys tem rozpoznaje błędną kartę i wyś wietla komunikat „ zła karta”
2.2
S ys tem zwraca kartę klientowi i wyś wietla ekran powitalny
4.1.1 Użytkownik wprowadza niepoprawny PIN S cenarius z alternatywny – zły PIN
S cenarius z alternatywny – brak akcji klienta
4.1.2
S ys tem wyś wietla komunikat o złym numerze PIN
Jeś li użytkownik wpis ał błędny PIN po raz 3, s ys tem zwraca kartę, zamyka trans akcję 4.1.3 i wyś wietla ekran powitalny; w przeciwnym razie powrót do kroku 3 4.2.1
Użytkownik nie podejmuje akcji przez 30 s ekund
4.2.2
S ys tem zatrzymuje kartę i wyś wietla s tos owny komunikat
4.2.3
S ys tem zamyka trans akcję i wyś wietla ekran powitalny
Przypadek użycia
PU004 Sz ybkaWypłata 7.1
S ys tem neg atywnie weryfikuje iloś ć ś rodków na koncie
S cenarius z alternatywny – brak ś rodków na koncie
S cenarius z alternatywny – g otówka nieodebrana S cenarius z alternatywny – klient nie odbiera karty Rozszerzenia
7.2
S ys tem wyś wietla komunikat o braku ś rodków i zwraca kartę
7.3
Klient odbiera kartę
7.4
S ys tem zamyka trans akcję i wyś wietla ekran powitalny
8.1
Użytkownik nie podejmuje akcji przez 20 s ekund
8.2
S ys tem zatrzymuje g otówkę i zg łas za incydent nieodebrania g otówki do banku
8.3
S ys tem wyś wietla ekran powitalny
10.1
Przypadek jak w s cenarius zu 4.2.
6′ Klient wybiera wypłatę inneg o rodzaju (PU005) 7′ Utrata połączenia z bankiem (PU011)
czy krok 7 nie jest zbyt obszerny? Może warto podzielić go na cztery kroki: weryfikacja środków na koncie, wypłata, uaktualnienie stanu konta oraz wyświetlenie komunikatu? Pozostawienie go w obecnej formie może utrudnić testowanie; co się stanie, jeśli zajdzie nieprzewidziane zdarzenie między wypłatą a uaktualnieniem stanu konta (np. utrata łączności z bankiem, brak prądu)? czy nie powinno się zdefiniować podprzypadku dla kroku 7.3 dotyczącego braku akcji klienta (nieodebranie karty)? Analogiczne
podprzypadki są opisane w scenariuszach alternatywnych dla kroków 4 (scenariusz 4.2), 8 i 10; bankomat potrzebuje komunikować się z bankiem, jednak w przypadku użycia taki aktor nie występuje. Tego typu uwagi powinny być tak naprawdę poczynione wcześniej, na etapie projektowania przypadku użycia. Jeśli jednak tester widzi niejednoznaczności, niekonsekwencje, nie rozumie sformułowań użytych w treści przypadku lub po prostu czuje, że w jakimś miejscu scenariusza mogą pojawić się problemy nieopisane w przypadku, to zawsze powinien zgłaszać swoje wątpliwości, czy to formalnie (jako incydent), czy też nieformalnie (przez rozmowę z projektantem lub analitykiem). Jak widać, nawet w dosyć prostym i – wydawałoby się – dobrze skonstruowanym przypadku można odkryć dużą liczbę problemów bądź niejasności. Zostawmy jednak kwestię analizy statycznej i przejdźmy do tworzenia przypadków testowych. Krok 1. Określenie przedmiotu i elementów testów. Przedmiotem (elementem) testów jest funkcjonalność szybkiej wypłaty opisana przypadkiem SzybkaWypłata. ET1: SzybkaWypłata. Krok 2, 3. Wyprowadzenie warunków testowych i elementów pokrycia. Warunki testowe, będące jednocześnie elementami pokrycia, to wszystkie scenariusze opisane w przypadku: WT1=EP1 Scenariusz główny – szybka wypłata; WT2=EP2 Scenariusz alternatywny WT3=EP3 Scenariusz alternatywny WT4=EP4 Scenariusz alternatywny WT5=EP5 Scenariusz alternatywny WT6=EP6 Scenariusz alternatywny WT7=EP7 Scenariusz alternatywny
2 – zła karta; 4.1 – zły PIN; 4.2 – brak akcji klienta; 7 – brak środków na koncie; 8 – gotówka nieodebrana; 10 – brak akcji klienta.
Zauważmy, że chociaż scenariusz alternatywny 10 wygląda dokładnie tak samo jak scenariusz alternatywny 4.2, stanowią one dwa osobne elementy pokrycia. Występują one bowiem w różnych momentach interakcji użytkownika z systemem i dlatego nie mogą być opisane jednym, wspólnym warunkiem testowym. Krok 4. Zdefiniowanie przypadków testowych. Dalej szczegółowo opiszemy tylko dwa przypadki – jeden pokrywający scenariusz główny (czyli EP1) i drugi – pokrywający scenariusz alternatywny 2 (zła karta). Kroki zdefiniowane w przypadku testowym odpowiadają krokom scenariusza, który jest testowany. Przypadki te są opisane w tabelach 8.41 oraz 8.42. Tabela 8.41. Przypadek testowy dla scenariusza głównego przypadku użycia SzybkaWypłata
Nazwa przypadku użycia PU004 – Sz ybkaWypłata Nazwa przypadku testowego
PT004.0.0 S cenarius z g łówny – s zybka wypłata
Opis
Klient dokonuje udanej s zybkiej wypłaty 50 PLN z bankomatu
Aktorzy
Klient, bankomat
Pokryte elementy pokrycia
EP1
Pokryte Kroki przypadku użycia
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
W arunki wstępne
Klient włożył kartę do bankomatu wyś wietlająceg o ekran powitalny
# Krok
Oczekiwany wynik
1
Klient wkłada kartę do bankomatu
S ys tem pros i o podanie numeru PIN
2
Klient wprowadza poprawny PIN
Klient wybiera opcję 3 „ S zybka wypłata 50 PLN” 4
Klient odbiera g otówkę
S ys tem wyś wietla ekran opcji Pieniądze zos tają wypłacone, s ys tem wyś wietla komunikat „ pros zę odebrać g otówkę” S ys tem zwraca kartę i wyś wietla komunikat „ pros zę odebrać kartę”
5 Klient odbiera kartę
S ys tem wyś wietla ten s am ekran powitalny, który wyś wietlał przed wykonaniem kroku 1
Kroki opisują akcje podejmowane przez testera, który symuluje zachowanie użytkownika. Oczekiwane wyniki to kroki wykonywane przez system. Są one opisane „wysokopoziomowo” – badamy w końcu obiekt testów na poziomie funkcjonalnym. Nie interesuje nas wewnętrzny stan systemu, ale to, w jaki sposób objawia on swoje działanie na zewnątrz. Dlatego na przykład w kroku 3 przypadku testowego z tabeli 8.41 nie piszemy, że np. system łączy się z bankiem. Na tym poziomie testów funkcjonalnych nie jesteśmy w stanie tego zweryfikować. Powinno to zostać przetestowane wcześniej, na etapie testów integracyjnych oprogramowania bankomatu z systemem bankowym. W tabeli 8.43 podsumowano metodę testowania opartego na przypadkach użycia. Tabela 8.42. Przypadek testowy dla scenariusza alternatywnego przypadku użycia SzybkaWypłata
Nazwa przypadku użycia
PU004 – Sz ybkaWypłata
Nazwa przypadku testowego
PT004.2.0 S cenarius z alternatywny – zła karta
Opis
Nieudana s zybka wypłata z powodu użycia złej karty
Aktorzy
Klient, bankomat
Pokryte elementy pokrycia
EP2
Pokryte Kroki przypadku użycia
1, 2.1, 2.2
W arunki wstępne
Klient włożył kartę niewłaś ciweg o banku do bankomatu wyś wietlająceg o ekran powitalny
# Krok
Oczekiwany wynik
Klient wkłada Bankomat wyś wietla komunikat „ zła karta” , zwraca 1 złą kartę do kartę i wyś wietla ten s am ekran powitalny, który bankomatu wyś wietlał przed wykonaniem kroku 1 Tabela 8.43. Podsumowanie metody testowania opartego na przypadkach użycia
Warunki tes towe S cenarius ze: g łówny oraz alternatywne Elementy pokrycia
S cenarius ze: g łówny oraz alternatywne
Kryterium pokrycia
Dla każdeg o s cenarius za is tnieje tes t złożony z jeg o kroków
Pokrycie
Nie definiuje s ię metryki pokrycia
Zarówno w technice testowania opartej na przypadkach użycia, jak i w opisanych dalej technikach opartych na scenariuszach i historyjkach użytkownika, metryka pokrycia nie jest definiowana. Wynika to stąd, że metryka ta ma zastosowanie w sytuacjach, w których jest możliwe istnienie nieosiągalnych elementów pokrycia w ramach jakiegoś wymagania lub w których przypadki
testowe nie pokrywają wszystkich tych elementów. W przypadkach użycia testy wprost odnoszą się do scenariuszy (=wymagań!), więc w tym sensie pokrycie zawsze wyniesie 100%. Można zdefiniować miarę wyrażającą stosunek pokrytych do wszystkich możliwych scenariuszy, ale taka miara, w przypadku, gdy przyjmuje wartość mniejszą od 100%, będzie wyrażać po prostu fakt nieprzetestowania jakiegoś wymagania. W tym sensie różnica w wartościach tej metryki między np. 50% czy 75% nie ma żadnego znaczenia i nie niesie ze sobą żadnej sensownej informacji.
8.11. Testowanie oparte na scenariuszach 8.11.1. Opis metody Celem testowania opartego na scenariuszach jest dostarczenie przypadków testowych pokrywających na ustalonym poziomie scenariusze obiektu testów. Testowanie scenariuszy jest oparte na analizie bazy testów dla testowanego obiektu i wyprowadzeniu na jej podstawie modelu zachowania tego obiektu. Model zachowania opisuje sekwencje zdarzeń tworzących przepływ pracy (ang. workflow) w danym obiekcie. Hipoteza błędu mówi, że błędy powstają na skutek nieprawidłowego oprogramowania procesów biznesowych. Testowanie oparte na scenariuszach jest podobne do testowania opartego na przypadkach użycia. Zwykle jednak techniki tej używa się na wyższym poziomie ogólności niż przypadki, które dotyczą pojedynczych, niewielkich interakcji między użytkownikiem a systemem. Testowanie oparte na scenariuszach może swoim zasięgiem obejmować testowanie całego przepływu zdarzeń w systemie. Metodę opiszemy na przykładzie działania bankomatu, którego fragment funkcjonalności posłużył nam jako ilustracja techniki testowania opartego na przypadkach użycia. W tabeli 8.44 podsumowano metodę testowania opartego na scenariuszach. Tabela 8.44. Podsumowanie metody testowania opartego na scenariuszach
W arunki testowe
Scenariusze: główny oraz alternatywne
Elementy pokrycia
S cenarius ze: g łówny oraz alternatywne
Kryterium pokrycia
Dla każdeg o s cenarius za is tnieje tes t złożony z jeg o kroków
Pokrycie
Nie definiuje s ię metryki pokrycia
8.11.2. Przykład Krok 1. Określenie przedmiotu i elementów testów. Przedmiotem testów i jedynym elementem testów jest oprogramowanie bankomatu. ET1: oprogramowanie bankomatu. Krok 2. Wyprowadzenie warunków testowych. Klient zidentyfikował następujące scenariusze dla oprogramowania bankomatu: Scenariusz główny: udana wypłata gotówki z bankomatu. Scenariusze alternatywne: odmowa transakcji z powodu użycia złej karty; wpisanie przez klienta 10. Wzorzec blokowy wystąpi wtedy, gdy we fragmencie programu postaci: if (x>=4 and x=2 and y 0) then y:=y+1
nie stanowi niepodzielnej instrukcji, ponieważ zwiększenie y o jeden nie nastąpi zawsze z chwilą wejścia do instrukcji if, ale tylko wtedy, gdy warunek tej instrukcji będzie
spełniony.
Dlatego
ten
fragment
kodu
powinniśmy
przedstawić
następująco: if (x>0) then y:=y+1 Teraz każda z dwóch linii kodu stanowi jednocześnie osobną instrukcję. Kryterium pokrycia instrukcji wymaga, aby każda instrukcja została wykonana przynajmniej raz podczas wykonywania przypadków testowych. W terminologii grafu przepływu sterowania (patrz p. 5.4.1) oznacza to, że wymagamy, aby każdy wierzchołek tego grafu został odwiedzony przynajmniej raz. Hipoteza błędu mówi, że błąd występuje w konkretnej linii programu, zatem każdą z nich należy przynajmniej raz wykonać. testowanie instrukcji (ang. statement testing) – białoskrzynkowa technika projektowania przypadków testowych, w których przypadki projektuje się tak, aby wykonały instrukcje W testowaniu instrukcji warunkami testowymi – i jednocześnie elementami pokrycia – są poszczególne instrukcje programu. Kryterium pokrycia to odsetek wykorzystanych elementów pokrycia. Jeśli naszym modelem nie jest bezpośrednio kod, lecz jego graf przepływu sterowania, to elementami pokrycia mogą być nie pojedyncze instrukcje, ale bloki podstawowe1. Wtedy, w przypadku pokrycia mniejszego od 100%, te dwie miary (pokrycie instrukcji i pokrycie blokówwierzchołków grafu) mogą się od siebie różnić. pokrycie instrukcji kodu (ang. statement coverage) – odsetek instrukcji wykonywalnych, które zostały przetestowane przez zestaw testowy Osiągnięcie 100% pokrycia może być niemożliwe ze względu na istnienie martwego kodu. Model wykorzystujący graf przepływu sterowania zawsze może dostarczyć zbiór ścieżek wykonania pokrywających wszystkie instrukcje, jednak pokrycie to ma charakter syntaktyczny. Aby rzeczywiście instrukcje były pokryte, należy uruchomić program z konkretnymi danymi wejściowymi. Może się okazać, że syntaktyczne pokrycie kodu jest niemożliwe, bo nie da się wymusić
sterowania przechodzącego określoną ścieżką. Ilustruje to fragment kodu z listingu 9.1.
x:=0; (B1) if (x>0) then
(B2)
P (B3) else Q. (B4) Listing 9.1. Fragment programu z martwym kodem Niezależnie od instrukcji poprzedzających ten fragment, predykat w warunku if nigdy nie będzie spełniony, zatem fragment P programu nigdy się nie wykona. Z syntaktycznego punktu widzenia, dwie ścieżki: B1 → B2 → B3 oraz B1 → B2 → B4 zapewniają 100% pokrycie instrukcji. Problem polega na tym, że ścieżki pierwszej nie da się wykonać dla żadnych danych wejściowych. Pokrycie tego fragmentu może zatem maksymalnie wynieść 75% (pokryte 3 spośród 4 instrukcji lub bloków podstawowych). Już w 1987 roku standard IEEE ANSI 87B stwierdzał, że pokrycie instrukcji jest minimalnym akceptowalnym poziomem pokrycia. Boris Beizer w swojej książce [14] poszedł jeszcze dalej, stwierdzając radykalnie, iż „testing less than this for new software is unconscionable and should be criminalized” (przetestowanie w nowym oprogramowaniu mniej niż pokrycia instrukcji jest nierozsądne i powinno być karalne). Beizer podaje również kilka dobrych praktyk dotyczących tego kryterium: 1. Nieprzetestowanie fragmentu kodu stwarza zagrożenie niewykrycia usterki, proporcjonalne do rozmiaru niepokrytego kodu oraz do prawdopodobieństwa wystąpienia błędu. 2. Ścieżki o wysokim prawdopodobieństwie wykonania zawsze są dokładnie testowane, jeśli chcemy zademonstrować poprawne typowe działanie systemu. Jeśli nie można z jakichś względów pokryć testami wszystkich instrukcji, to rozsądniej jest opuścić właśnie te typowe ścieżki, gdyż na pewno będą one wykonane podczas testów integracyjnych lub systemowych.
3. Błędy logiczne i mętne myślenie są odwrotnie proporcjonalne do prawdopodobieństwa wykonania ścieżki (im bardziej udziwniony i podatny na błędy kod, tym mniejsza szansa, że zostanie on wykonany w typowym przebiegu). 4. Subiektywne prawdopodobieństwo wykonania ścieżki szacowane przez projektanta bądź programistę jest zwykle zupełnie inne od prawdziwego. Jedynie analiza działania programu (np. przez profilowanie, patrz podrozdz. 7.4) może dać obiektywne szacowanie. W tabeli 9.1 przedstawiono podsumowanie metody testowania instrukcji. Tabela 9.1. Podsumowanie metody testowania instrukcji
9.1.2. Przykład Rozważmy program sortowania metodą bąbelkową, opisany w rozdziale 2. Jego kod (tym razem z poprawną instrukcją w linii 4.) oraz odpowiadający mu graf przepływu sterowania są pokazane na rysunku 9.1. Chcemy przetestować tę funkcję przy użyciu kryterium pokrycia linii kodu. Krok 1. Określenie przedmiotu i elementów testów. Jedynym elementem testu jest funkcja SortBąbelkowe. ET1: SortBąbelkowe.
Rysunek 9.1. Kod i CFG dla programu SortBąbelkowe Krok 2. Wyprowadzenie warunków testowych. Warunkami testowymi są wykonywalne instrukcje. Zauważmy, że w naszym kodzie jedna linia programu odpowiada pojedynczej instrukcji. Wszystkie warunki testowe są dla ET1. WT1: instrukcja WT2: instrukcja WT3: instrukcja WT4: instrukcja
nr 2; WT5: instrukcja nr 3; WT6: instrukcja nr 4; WT7: instrukcja nr 5; WT8: instrukcja
nr 6; nr 9; nr 10; nr 11.
Krok 3. Wyprowadzenie elementów pokrycia. Elementy pokrycia tworzą bezpośrednio warunki testowe: EP1: instrukcja EP2: instrukcja EP3: instrukcja EP4: instrukcja
nr 2; EP5: instrukcja nr 3; EP6: instrukcja nr 4; EP7: instrukcja nr 5; EP8: instrukcja
nr 6; nr 9; nr 10; nr 11.
Wymóg przejścia przez wszystkie elementy pokrycia jest równoważny wymogowi pokrycia wszystkich wierzchołków CFG z rysunku 9.1. Zauważmy, że istnieje pojedyncza ścieżka sterowania przechodząca przez wszystkie wierzchołki: B1 → B2 → B3 → B5 → B6 → B4 → B3 → B7 → B8. Oznacza to, że wystarczy tylko jeden przypadek testowy realizujący tę ścieżkę, aby spełnić 100% pokrycia instrukcji. Należy znaleźć dla niego takie wartości wejścia (tablicę T), aby pętla for, ciało instrukcji if oraz pętla do-while wykonały się przynajmniej raz. Krok 4. Zdefiniowanie przypadków testowych. Dla kryterium pokrycia instrukcji stosuje się zwykle podejście maksymalizujące, tzn. każdym nowym przypadkiem staramy się pokryć jak najwięcej elementów pokrycia. W przypadku programu SortBąbelkowe wystarczy tylko jeden przypadek, pokazany w tabeli 9.2. Tabela 9.2. Przypadki testowe dla pokrycia instrukcji
Id
W ejście
PT1 T = [5, 3]
Oczekiwane wyjście
Pokryte EP
T = [3, 5]
EP1, EP2, EP3, EP4, EP5, EP6, EP7, EP8
Instrukcje 2, 3 i 4 (odpowiadające EP1, EP2 i EP3) wykonają się zawsze, niezależnie od postaci wejścia. Ciało pętli for wykona się przynajmniej raz, ponieważ 0 < 2 – 1. Najpierw zostanie wykonana instrukcja if w linii 5., pokrywając EP4. Warunek tej instrukcji będzie spełniony, bo 5 = T[0] > T[1] = 3, zatem nastąpi wykonanie linii 6. (EP5). Po zwiększeniu i o 1 i powrocie do początku pętli for jej warunek nie będzie już spełniony (bo 1 < 1 jest fałszem), w związku z czym pętla for zakończy swoje działanie i program przejdzie do linii 9 (EP6). Następnie zostanie sprawdzony warunek pętli do-while w linii 10 (EP7). Nie jest on spełniony (bo mamy n = 1, a 1 > 1 jest fałszem), dlatego sterowanie wyjdzie z pętli do-while i przejdzie do linii 11., pokrywając EP8 i kończąc działanie funkcji. Kryterium pokrycia instrukcji jest dosyć słabe. Na przykład nie przetestowaliśmy sytuacji, w której pętla for nie wykona się ani razu albo
sytuacji, w której warunek instrukcji if nie byłby spełniony. W terminologii grafu przepływu sterowania oznacza to, że nie pokryliśmy wszystkich krawędzi CFG.
9.2. Testowanie gałęzi 9.2.1. Opis metody Kryterium pokrycia gałęzi (zwane też pokryciem krawędzi) wymaga, aby testy pokryły
wszystkie
możliwe
przepływy
sterowania
między
blokami
podstawowymi. W terminologii CFG oznacza to po prostu pokrycie wszystkich krawędzi grafu. W definicji pokrycia gałęzi dodatkowo będziemy wymagać spełnienia kryterium pokrycia instrukcji. Zagwarantuje to subsumpcję między tymi kryteriami. Gdybyśmy nie dodali tego wymogu, dla programu złożonego z jednego bloku podstawowego (np. z pojedynczej instrukcji) jego graf przepływu sterowania nie miałby żadnych krawędzi i dlatego kryterium pokrycia gałęzi byłoby spełnialne przez pusty zbiór testów, który w oczywisty sposób nie pokrywałby instrukcji. Jeśli w CFG istnieje przynajmniej jedna krawędź, to oczywiście pokrycie krawędzi implikuje pokrycie wierzchołków. gałąź (ang. branch) podstawowymi testowanie
gałęzi,
–
(ang.
przepływ
branch
sterowania
testing)
–
między
dwoma
blokami
białoskrzynkowa
technika
projektowania przypadków testowych, w której przypadki te są projektowane w celu wykonania gałęzi; w metodologii TMap [28] testowanie gałęzi nosi nazwę testowania algorytmu (ang. algorithm test) Warunkami testowymi są wszystkie przepływy sterowania między blokami podstawowymi, a więc wszystkie krawędzie CFG. Elementy pokrycia są tożsame z warunkami testowymi. Jeśli program nie ma rozgałęzień, to warunkami testowymi oraz elementami pokrycia są instrukcje kodu. W tabeli 9.3 podsumowano metodę testowania gałęzi. pokrycie gałęzi (ang. branch coverage) – odsetek gałęzi sprawdzonych przez zestaw przypadków testowych. 100% pokrycia gałęzi implikuje 100% pokrycia instrukcji oraz decyzji
Tabela 9.3. Podsumowanie metody testowania gałęzi
9.2.2. Przykład Rozważmy ponownie funkcję SortBąbelkowe z poprzedniego rozdziału i stwórzmy dla niej przypadki testowe spełniające kryterium pokrycia gałęzi. Krok 1. Określenie przedmiotu i elementów testów. Elementem testu jest funkcja SortBąbelkowe. ET1: SortBąbelkowe. Krok 2 i 3. Wyprowadzenie warunków testowych i elementów pokrycia. Warunkami testowymi i jednocześnie elementami pokrycia są krawędzie grafu przepływu sterowania. WT1=EP1: (B1, B2); WT5=EP5: (B5, B4); WT9=EP9: (B7, B2); WT2=EP2: (B2, B3); WT6=EP6: (B6, B4); WT10=EP10: (B7, B8). WT3=EP3: (B3, B5); WT7=EP7: (B4, B3); WT4=EP4: (B5, B6); WT8=EP8: (B3, B7); Graficznie te elementy są pokazane na rysunku 9.2.
Rysunek 9.2. Krawędzie CFG jako elementy pokrycia w testowaniu gałęzi
Krok 4. Zdefiniowanie przypadków testowych. Musimy dostarczyć takie dane wejściowe, aby wszystkie gałęzie zostały pokryte. Rozważmy przypadek wejścia T = [4, 3]. Dla tej tablicy program przejdzie ścieżką p = B1 → B2 → B3 → B5 → B6 → B4 → B3 → B7 → B8 (zarówno pętla zewnętrzna do-while, jak i pętla for wykonają się tylko raz). Warunek w instrukcji if będzie spełniony, zatem sterowanie przejdzie przez wierzchołek B6. Wykonanie ścieżki p spowoduje pokrycie EP1, EP2, EP3, EP4, EP6, EP7, EP8, EP10. Zostały nam zatem do pokrycia jeszcze EP5 i EP9. EP5 zostanie pokryte, gdy warunek w instrukcji if nie będzie spełniony, tzn. gdy porównywane elementy będą posortowane. EP9 z kolei zostanie pokryte, gdy warunek pętli zewnętrznej do-while będzie prawdziwy, co nastąpi, gdy tablica będzie miała więcej niż 2 elementy. Jeśli więc podamy na wejście np. tablicę T = [3, 4, 5], to sterowanie przejdzie ścieżką q = B1 → B2 → B3 → B5 → B4 → B3 → B5 → B4 → B3 → B7 → B2 → B3 → B5 → B4 → B3 → B7 → B8. Ścieżka ta, ze względu na istnienie w niej krawędzi (B5, B4) oraz (B7, B2) pokrywa elementy EP5 i EP9. Skonstruowaliśmy dwa przypadki testowe pokrywające 100% gałęzi, jednak można ten zbiór zredukować do jednego przypadku. Zauważmy, że przechodząc ścieżką testową r = B1 → B2 → B3 → B5 → B6 → B4 → B3 → B5 → B4 → B3 → B7 → B2 → B3 → B5 → B4 → B3 → B7 → B8, przejdziemy po wszystkich gałęziach. Przypadek wymuszający takie przejście to np. T = [3, 2, 5]. Generalnie im dłuższa ścieżka testowa, tym trudniej dobrać wartości wejściowe tak, by wymusić przejście programu dokładnie po tej ścieżce. Dlatego czasami można zrezygnować z chęci uzyskania możliwie jak najmniejszego zbioru przypadków testowych na rzecz stworzenia nieco większej ich liczby, ale za to łatwiej podlegających wymuszeniu określonego sterowania. Skonstruowaliśmy dwa zbiory testowe, z których każdy spełnia kryterium pokrycia gałęzi. Zbiory te przedstawione są w tabelach 9.4 i 9.5 (pogrubiona czcionka oznacza pokrycie danego elementu po raz pierwszy). Tabela 9.4. Przypadki testowe spełniające kryterium pokrycia gałęzi – wariant I
Id
W ejście
PT1 T = [4, 3]
Oczekiwane wyjście
Pokryte elementy
T = [3, 4]
EP1, EP2, EP3, EP4, EP6, EP7, EP8, EP10
PT2 T = [3, 4, 5]
T = [3, 4, 5]
EP1, EP2, EP3, EP5, EP7, EP8, EP9, EP10
Tabela 9.5. Przypadki testowe spełniające kryterium pokrycia gałęzi – wariant II
Id
W ejście
Oczekiwane wyjście
Pokryte elementy
PT1
T = [3, 2, 5]
T = [3, 4, 5]
EP1, EP2, EP3, EP4, EP5, EP6, EP7, EP8, EP9, EP10
Gdybyśmy w wariancie I ograniczyli się tylko do przypadku PT1, stopień pokrycia wyniósłby 8/10 = 80%.
9.3. Testowanie decyzji 9.3.1. Opis metody Testowanie decyzji należy do rodziny kryteriów pokryć logicznych, gdyż dotyczy wartości logicznych predykatów występujących w instrukcjach decyzyjnych2. Stąd zresztą inna nazwa techniki: pokrycie predykatów. Decyzja jest więc po prostu wyrażeniem logicznym, które może przyjąć wartość logiczną prawdy lub fałszu. wynik decyzji (ang. decision outcome) – rezultat decyzji, określający gałąź do podążania Przykładami instrukcji decyzyjnych są: instrukcja warunkowa if: if (decyzja) then ...; warunek w instrukcji while: while (decyzja) do ...; warunek w instrukcji do-while: do ... while (decyzja); instrukcja switch-case; warunek pętli w instrukcji for: for (inicjalizacja; decyzja; inkrementacja) do ....
Testowanie decyzji wymaga, aby każda decyzja w programie przynajmniej raz przyjęła wartość logiczną prawdy i przynajmniej raz – wartość logiczną fałszu. Na przykład, jeśli w kodzie występuje instrukcja if (x>0 and y==0) then ... to przynajmniej raz warunek (x > 0 ∧ y = 0) powinien być prawdziwy i przynajmniej raz fałszywy. Prawdziwość można wymusić, przyjmując np. x = 1, y = 0, natomiast fałszywość przyjmując np. x = 1, y = 1. W przypadku instrukcji switch-case wymaga się przyjęcia przez zmienną w instrukcji switch wszystkich możliwych wartości ujętych we fragmentach case. Konstrukcja switch(x) case y1: ... break; case y2: ... break; ... case yn: ... break; da się bowiem wyrazić jako wielokrotna instrukcja if-then-else: if (x==y1) then ... else if (x==y2) then ... else if(x==yn) then ... endif Przyjęcie przez jeden z warunków instrukcji switch-case wartości logicznej prawdy oznacza automatycznie przyjęcie przez pozostałe warunki wartości logicznej fałszu. decyzja (ang. decision) – punkt w programie, w którym przepływ sterowania ma dwie lub więcej alternatywnych dróg; węzeł grafu przepływu sterowania, z którego wychodzą dwie lub więcej gałęzi
Tak jak w przypadku pokrycia gałęzi, do kryterium pokrycia decyzji dodajemy formalny warunek o pokryciu instrukcji, aby kryterium subsumowało pokrycie instrukcyjne dla
programów złożonych z pojedynczych bloków
podstawowych, niezawierających żadnych rozgałęzień. Hipoteza błędu mówi, że błędy mogą powstać na skutek określonego przepływu sterowania, wyznaczonego przez decyzje podejmowane w instrukcjach decyzyjnych. Rozważmy następujący prosty przykład z listingu 9.2.
1 x:=0; 2 if (a>b) then 3 x:=x+1 4 endif 5 y:=100/x Listing 9.2. Prosty program Przyjęcie wartości a i b takich, że a > b spowoduje wykonanie instrukcji 1, 2, 3, 4, 5 i skutkować będzie pełnym pokryciem instrukcyjnym tego fragmentu. Zauważmy jednak, że jeśli instrukcja 3 się nie wykona, to zmienna x cały czas będzie mieć wartość 0, a w linii 5. dzielimy przez x. Dzielenie przez 0 jest niedozwolone i wykonanie takiej operacji może skutkować awarią lub nieprzewidzianym działaniem programu bądź systemu operacyjnego. Stosując kryterium pokrycia instrukcji, możemy nie przewidzieć przetestowania programu w ten sposób. Sytuację tę możemy natomiast sprawdzić, przyjmując kryterium pokrycia decyzji. Wymusi ono bowiem istnienie przynajmniej jednego przypadku testowego, w którym predykat linii 2. ma wartość fałszu. To spowoduje dzielenie przez 0 w linii 5 i wykrycie awarii. Warunkami testowymi w tej metodzie są bloki podstawowe zawierające decyzje, czyli instrukcje if, for, while, switch-case itd. Dla danego warunku testowego elementami pokrycia są wszystkie możliwe wartości logiczne (prawda oraz fałsz), jakie może przyjąć decyzja odpowiadająca temu warunkowi. Jeśli program nie zawiera rozgałęzień, warunkami testowymi i elementami pokrycia są instrukcje, tak jak w kryterium pokrycia instrukcji. W tabeli 9.6 podsumowano metodę testowania decyzji.
pokrycie decyzji (ang. decision coverage) – odsetek możliwych wyników decyzji, które zostały przetestowane przez zestaw testowy. 100% pokrycia decyzji implikuje 100% pokrycia instrukcji Tabela 9.6. Podsumowanie metody testowania decyzji
W arunki W ierzchołki CFG reprezentujące decyzje testowe Elementy Krawędzie CFG odpowiadające wynikom decyzji pokrycia Kryterium Każda decyzja przynajmniej raz powinna przyjąć wartoś ć pokrycia prawdy i przynajmniej raz wartoś ć fałs zu
Pokrycie
p =
liczba pokrytych tes tami krawędzi decyzyjnych CFG liczba ws zys tkich krawędzi decyzyjnych CFG
× 100%;
w przypadku CFG z 1 blokiem pods tawowym 0% lub 100% w zależnoś ci od s pełnienia kryterium ins trukcyjneg o
9.3.2. Testowanie decyzji a testowanie gałęzi Testowanie decyzji i testowanie gałęzi są podobnymi kryteriami. Tak naprawdę w przypadku pełnego, stuprocentowego pokrycia są sobie równoważne3, tzn. 100% pokrycia gałęzi jest równoważne 100% pokrycia decyzji. Innymi słowy oznacza to, że jeśli jakiś zbiór przypadków testowych spełnia jedno z tych kryteriów, to automatycznie spełnia też drugie. Różnice pojawiają się przy mniejszych stopniach pokrycia, ponieważ – z punktu widzenia grafu przepływu sterowania – testowanie decyzji jest równoważne pokryciu pewnego podzbioru gałęzi (odpowiadających za realizację decyzji). Różnicę tę omówimy na przykładzie z kolejnego podrozdziału.
9.3.3. Przykład
Rozważmy funkcję Bisekcja znajdującą pierwiastek4 ciągłej funkcji f(x) w zadanym przedziale metodą bisekcji. Funkcja na wejściu przyjmuje granice l i r przedziału (l, r) na którym będzie szukać pierwiastka. Jeśli funkcja jest ciągła, a granice przedziału mają różne znaki, to przedział ten na pewno zawiera co najmniej jedno miejsce zerowe. Algorytm, przedstawiony na listingu 9.3 kończy działanie, gdy aktualnie przeszukiwany przedział ma długość mniejszą niż pewna ustalona wartość ε lub gdy trafi dokładnie w miejsce zerowe f. W przypadku, gdy parametry wejściowe mają ten sam znak, algorytm od razu zwraca komunikat o niepoprawnym przedziale wejściowym.
function Bisekcja(double l, double r) 1 double eps := 0.0001 2 double left := l 3 4
double right := r double mid
5 6 7
if (left*right > 0) then return "Niepoprawny przedział wejściowy" while (right-left > eps) do
8 9 10 11
mid = (left+right)/2 if (f(mid)==0) then return "Dokładny pierwiastek = "+mid if (f(left)*f(mid)>0) then
12 13
left := mid else if (f(left)*f(mid) 0.0001, m := 0.0000625), pokrywa EP3; B4=fałsz, pokrywa EP6; B6=fałsz (bo f(l) ⋅ f(m) = l ⋅ m < 0), pokrywa EP8; B8= prawda r := 0.0000625, pokrywa EP9; B3=prawda (l = –0.000125, r = 0.0000625, r – l = 0.0001875, m := –0.00003125; B4=fałsz; B6=prawda (bo f(l) ⋅ f(m) = l ⋅ m > 0), l := –0.00003125, pokrywa EP7; B3=fałsz (bo r – l = 0.0000625 – (–0.00003125) = 0.00009375 < 0.0001, pokrywa EP4. Jak zauważyliśmy wcześniej, pokrycie EP10 jest nieosiągalne. Uzyskaliśmy zatem 9/10 = 90% pokrycia decyzji. Sprawdźmy, ile dla tego samego zbioru testów wynosi pokrycie gałęzi. W grafie przepływu sterowania występuje 13 krawędzi. Jedyną niepokrytą krawędzią (ze względu na nieosiągalność EP10) jest krawędź (B8, B10). Stopień pokrycia gałęzi wynosi zatem 12/13 ≈ 92% – nieco więcej, niż stopień pokrycia decyzji.
9.4. Testowanie warunków 9.4.1. Opis metody Często zdarza się, że decyzje w instrukcjach warunkowych mają dosyć skomplikowaną postać, w szczególności mogą wykorzystywać spójniki logiczne
takie jak alternatywę (logiczne „lub”) czy koniunkcję (logiczne „i”). Warunek jest szczególnym przypadkiem decyzji, w której nie występują spójniki logiczne. Na przykład w decyzji D: x > y ∧ (z == 0 ∨ y > 0) występują trzy warunki: x > y, z == 0 oraz y > 0. W języku logiki decyzje są nazywane predykatami, a warunki – klauzulami. warunek, warunek rozgałęzienia (ang. condition, branch condition) – wyrażenie logiczne, którego wartością może być prawda lub fałsz warunek atomowy (ang. atomic condition) – wyrażenie logiczne niezawierające spójników logicznych, mogące przyjmować warunek prawdy lub fałszu wartość warunku (ang. condition outcome) – wyliczenie wartości logicznej warunku jako prawdy lub fałszu Kryterium pokrycia warunków jest analogiczne do kryterium pokrycia decyzji, z tym że – jak sama nazwa wskazuje – dotyczy warunków, a nie decyzji. Stanowi ono, że każdy warunek w każdej decyzji powinien przynajmniej raz przyjąć wartość logiczną prawdy i przynajmniej raz wartość logiczną fałszu. Tak jak poprzednio, dodajemy do kryterium formalnie wymóg pokrycia instrukcji. pokrycie warunków (ang. condition coverage) – odsetek wyników warunków, jaki został uzyskany przez zestaw testowy Tabela 9.8. Przypadki testowe dla decyzji złożonej z 3 warunków
Id
x y
z x >y
z == 0
PT1 1 3
0 fałs z
prawda prawda fałs z
PT2 1 –2 1 prawda fałs z
y >0
fałs z
D := (x > y ∧ (z == 0 ∨ y > 0))
fałs z
Aby spełnić pokrycie warunków dla decyzji D, musimy dostarczyć takie przypadki testowe, które spowodują prawdziwość oraz fałszywość każdego z trzech warunków. Przypadki te muszą zatem wymusić 6 następujących sytuacji: x > y, x ≤ y, z = 0, z ≠ 0, y > 0 oraz y ≤ 0. Przypadków testowych zwykle jest o wiele
mniej niż warunków, gdyż dla decyzji złożonej z k warunków jeden zbiór wartości wejściowych pokrywa k różnych wartości logicznych, po jednej dla każdego warunku. Rozważmy dla zdefiniowanej decyzji D dwa przypadki testowe opisane w tabeli 9.8. Kolumny x, y, z zawierają wartości odpowiednich zmiennych. W kolejnych trzech kolumnach są podane wartości logiczne, jakie przyjmują poszczególne warunki decyzji D dla zadanych x, y, z. W ostatniej kolumnie jest podana wartość logiczna całej decyzji. Zauważmy, że kryterium pokrycia warunków jest spełnione przez dwa przypadki testowe: (1, 3, 0) oraz (1, –2, 1), ponieważ każdy warunek przyjął przynajmniej raz wartość logiczną prawdy i przynajmniej raz wartość logiczną fałszu. Jednak przypadki te nie spełniają kryterium pokrycia decyzji – w obu przypadkach D jest fałszywe! Przykład ten pokazuje, że kryterium pokrycia warunków nie subsumuje pokrycia decyzji, choć intuicyjnie wydawałoby się, że tak musi być, skoro kryterium „rozbija” decyzję na mniejsze kawałki i dla każdego z nich stosuje te same wymagania, co dla całej decyzji. Brak subsumpcji wynika stąd, że przyjmowanie wartości logicznych przez warunki nie ma nic wspólnego ze strukturą decyzji, tzn. nie bierze pod uwagę tego, w jaki sposób warunki te są połączone spójnikami logicznymi. Warunkami testowymi dla kryterium pokrycia warunków są decyzje. Dla każdego warunku testowego elementami pokrycia są ewaluacje zarówno na wartość prawdy, jak i fałszu wszystkich warunków wchodzących w skład decyzji. Na przykład, jeśli warunkiem testowym jest decyzja D = x > y ∧ (z == 0 ∨ y > 0), to odpowiada jej sześć elementów pokrycia: x > y jest prawdą, z == 0 jest prawdą, y > 0 jest prawdą; x > y jest fałszem, z == 0 jest fałszem, y > 0 jest fałszem. Jeśli program nie zawiera decyzji, to warunkami testowymi i elementami pokrycia są instrukcje. W tabeli 9.9 przedstawiono podsumowanie metody testowania warunków. Tabela 9.9. Podsumowanie metody testowania warunków
9.4.2. Przykład Rozważmy kod z listingu 9.4 implementujący metodę isLeapYear sprawdzającą, czy podany rok jest przestępny. Przypomnijmy, że rok jest przestępny, jeśli dzieli się przez 4, chyba że dzieli się przez 100, ale nie przez 400.
int isLeapYear(int year) { leapYear=0; if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) { leapYear=1; } return leapYear } Listing 9.4. Funkcja sprawdzająca przestępność roku Znak „%” oznacza w Javie dzielenie modulo. Na przykład wyrażenie year % 4 == 0 jest prawdziwe, gdy wartość year jest podzielna przez 4, tzn. gdy daje przy dzieleniu przez 4 resztę 0. Operatory „&&” i „||” oznaczają w Javie odpowiednio koniunkcję i alternatywę, a „!=” to operator „różne od”. Chcemy przetestować ten fragment kodu za pomocą kryterium pokrycia warunków.
Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden przedmiot testów – funkcję leapYear. ET1: leapYear. Krok 2. Wyprowadzenie warunków testowych. Warunkiem testowym jest (jedyna) decyzja w instrukcji if. WT1: ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) (dla ET1). Krok 3. Wyprowadzenie elementów pokrycia. Elementami pokrycia są wszystkie możliwe ewaluacje wszystkich warunków dla każdej decyzji. W naszym przypadku mamy tylko jedną decyzję i 3 warunki, co daje 6 następujących elementów pokrycia odpowiadających WT1: EP1: year % 4 = 0 jest prawdą; EP4: year % 100 ≠ 0 jest fałszem; EP2: year % 4 = 0 jest fałszem; EP5: year % 400 = 0 jest prawdą; EP3: year % 100 ≠ 0 jest prawdą; EP6: year % 400 = 0 jest fałszem. Krok 4. Zdefiniowanie przypadków testowych. Zauważmy, że wartości logiczne warunków mogą zależeć od wartości innych warunków. Na przykład, jeśli rok jest podzielny przez 400, to w szczególności zawsze będzie podzielny przez 4 i przez 100. W przypadku metody testowania warunków nie wpływa to na możliwość pełnego pokrycia (może co najwyżej spowodować zwiększenie liczby potrzebnych przypadków testowych), ale jest problematyczne przy bardziej skomplikowanych kryteriach, takich jak niektóre warianty testowania warunków znaczących opisane w podrozdziale 9.7. Rozważmy następujące przypadki (tab. 9.10). Tabela 9.10. Przypadki testowe dla testowania warunków
Id
year
year % year % 4=0 100 ≠ 0
PT1 2000 prawda fałs z
year % 400 = 0
Oczekiwane Pokryte Decyzja wyjście EP WT1 (leapYear)
prawda 1
EP1, EP4,
prawda
EP5 PT2 1999 fałs z
prawda fałs z
0
EP2, EP3, EP6
fałs z
Każdy z warunków jest co najmniej raz ewaluowany na prawdę i co najmniej raz na fałsz, zatem osiągnęliśmy 100% pokrycie warunków. Przy okazji uzyskaliśmy również 100% pokrycia decyzji.
9.5. Testowanie warunków/decyzji 9.5.1. Opis metody Jak wspomnieliśmy w poprzednim rozdziale, kryterium pokrycia warunków nie subsumuje kryterium pokrycia decyzji. Metoda testowania warunków/decyzji (ang. Conditon/Decision testing, C/D) jest formą techniki testowania warunków zmodyfikowaną tak, aby oprócz pokrycia warunków wymusić również pokrycie decyzji. Kryterium pokrycia warunków/decyzji zakłada, że każdy warunek oraz każda decyzja w programie przynajmniej raz przyjmie wartość prawdy oraz fałszu. Tak jak zwykle, z przyczyn formalnych dodajemy do kryterium jeszcze wymóg pokrycia instrukcji. Warunkami testowymi są decyzje, a elementami pokrycia – wszystkie wartości logiczne każdego warunku oraz każdej decyzji. Jeśli w programie nie ma decyzji, to warunkami testowymi i elementami pokrycia są instrukcje. W tabeli 9.11 przedstawiono podsumowanie metody testowania warunków/decyzji. pokrycie warunków/decyzji (ang. decision/condition coverage) – odsetek wszystkich możliwych wyników warunków oraz decyzji, które zostały sprawdzone przez zestaw testowy. 100% pokrycia warunków/decyzji implikuje 100% pokrycia instrukcji, 100% pokrycia warunków oraz 100% pokrycia decyzji Tabela 9.11. Podsumowanie metody testowania warunków/decyzji
9.5.2. Przykład Rozważmy ponownie zbiór testów z tabeli 9.8 dla decyzji D = (x > y ∧ (z == 0 ∨ y > 0)). Aby spełnić kryterium pokrycia warunków, skonstruowaliśmy dwa przypadki testowe, (x, y, z) = (1, 3, 1) oraz (x, y, z) = (1, –2, 1). Zauważyliśmy, że choć pokrywają one w 100% warunki, nie pokrywają w 100% decyzji, gdyż w obu przypadkach D jest fałszem. Teraz, dla kryterium warunków/decyzji mamy następujące elementy pokrycia: EP1: x > y jest prawdą; EP5: y > 0 jest prawdą; EP2: x > y jest fałszem; EP6: y > 0 jest fałszem; EP3: z == 0 jest prawdą; EP7: D jest prawdą; EP4: z == 0 jest fałszem; EP8: D jest fałszem. Aby spełnić dla D kryterium pokrycia warunków/decyzji możemy dodać trzeci przypadek, wymuszający prawdziwość decyzji (np. dla (x, y, z) = (1, 0, 0)). Możemy też spróbować uzyskać pełne pokrycie, używając nadal tylko dwóch przypadków testowych. Aby D była prawdziwa, musi być x > y oraz z = 0 lub y > 0. W drugim przypadku x > y musi być fałszem, co automatycznie spowoduje ewaluację D na fałsz i pozostałe dwa warunki dobierzemy tak, aby pokryły brakujące elementy pokrycia. Weźmy zatem jako pierwszy test (x, y, z) = (1, 0, 0). Pokrywa on EP1, EP3, EP6 oraz EP7. Aby pokryć pozostałe elementy, musi zachodzić x ≤ y, z ≠ 0, y > 0 oraz ~(x > y ∧ (z == 0 ∨ y > 0)). Możemy na przykład przyjąć (x, y, z) = (2, 2, 2).
Ostatecznie, uzyskaliśmy 100% pokrycie warunków/decyzji przypadków testowych. Są one przedstawione w tabeli 9.12.
dla
dwóch
Tabela 9.12. Przypadki testowe dla pokrycia warunków/decyzji
Id
x y z x >y
z == 0
y >0
PT1 1 0 0 prawda prawda fałs z PT2 2 2 2 fałs z
fałs z
D := (x > y ∧ (z == 0 ∨ y > 0)) prawda
prawda fałs z
9.6. Testowanie wielokrotnych warunków 9.6.1. Opis metody Testowanie wielokrotnych warunków jest wzmocnieniem techniki testowania warunków oraz warunków/decyzji. Kryterium to zakłada, że pokryte będą wszystkie możliwe kombinacje wartości logicznych warunków w każdej decyzji (tradycyjnie, dodajemy również formalny wymóg pokrycia instrukcji). Na przykład dla decyzji (x > y ∨ z == 0) musimy przetestować cztery kombinacje wartości logicznych warunków x > y oraz z == 0: (1) x > y jest prawdą i z == 0 jest prawdą; (3) x > y jest fałszem i z == 0 jest prawdą; (2) x > y jest prawdą i z == 0 jest fałszem; (4) x > y jest fałszem i z == 0 jest fałszem. warunek wielokrotny, warunek złożony (ang. multiple condition, compound condition) – dwa lub więcej warunków połączonych spójnikami logicznymi „and”, „or” lub „xor” Warunek może przyjmować dwie wartości logiczne, zatem dla decyzji zbudowanej z n warunków musimy przetestować 2n kombinacji. Ich wykładnicza liczba sprawia, że kryterium to może być trudne do pełnego pokrycia zwłaszcza w przypadku programów, których decyzje są złożone z wielu warunków. W oczywisty sposób pokrycie wielokrotnych warunków subsumuje zarówno pokrycie warunków, jak i pokrycie decyzji. Warunkami testowymi są decyzje, a elementami pokrycia – wszystkie możliwe kombinacje logiczne warunków dla
każdej decyzji. Jeśli w programie nie ma decyzji, to warunkami testowymi i elementami pokrycia są instrukcje. W tabeli 9.13 podsumowano metodę testowania warunków wielokrotnych. pokrycie
warunków
wielokrotnych,
pokrycie
kombinacji
warunków
w decyzjach, pokrycie kombinacji warunków (ang. multiple condition coverage, branch condition combination coverage, condition combination coverage) – odsetek kombinacji wszystkich wyjść warunków prostych w jednej instrukcji, które zostały sprawdzone przez zestaw testowy. 100% pokrycia warunków wielokrotnych oznacza 100% pokrycia warunków znaczących Tabela 9.13. Podsumowanie metody testowania warunków wielokrotnych
W arunki W ierzchołki CFG reprezentujące decyzje testowe Elementy Ws zys tkie kombinacje wartoś ci log icznych warunków pokrycia w każdej z decyzji Kryterium Każda kombinacja wartoś ci log icznych warunków w każdej pokrycia decyzji powinna wys tąpić przynajmniej raz Pokrycie
p = (liczba pokrytych kombinacji wartoś ci warunków/liczba ws zys tkich kombinacji wartoś ci warunków) × 100%
9.6.2. Zwarcie (semantyka short-circuit) W praktyce, w wielu przypadkach nie ma potrzeby tworzenia wykładniczej liczby testów, ze względu na tzw. zwarcia (ang. short-circuit evaluation). Jest to stosowana przez kompilatory technika optymalizacji obliczania wartości logicznych decyzji. Czasami struktura logiczna predykatu powoduje, że obliczenie wartości logicznej jednej jej części determinuje wartość logiczną całej decyzji. Zobaczmy, jak technika short-circuit działa w praktyce. Załóżmy, że kompilator musi obliczyć wartość logiczną decyzji D = x ∧ y ∧ (w ∨ z). Najpierw jest obliczana wartość logiczna warunku znajdującego się na początku decyzji, czyli x. Załóżmy, że x jest fałszem. Kompilator, znając strukturę decyzji (koniunkcja trzech składników), wie, że skoro x jest fałszem, to niezależnie od
wartości pozostałych warunków, decyzja i tak będzie miała wartość fałszu. Przypadek, gdy x jest fałszem determinuje zatem wartość całej decyzji. Kompilator zaoszczędza czas, nie musi bowiem wyliczać wartości logicznej y, w ani z. Z praktycznego punktu widzenia wszystkie przypadki testowe wartościujące x na fałsz są sobie równoważne, bo wartości y, w i z w ogóle nie będą brane pod uwagę. Zamiast tworzyć 8 przypadków testowych tak jak w tabeli 9.14, wystarczy jeden, w którym x = fałsz, a pozostałe warunki mogą przyjmować dowolne wartości logiczne. Na takiej samej zasadzie, gdy x = prawda, kompilator jeszcze nie może zdeterminować wartości D. Jeśli jednak y = fałsz, wiadomo, że cała decyzja będzie miała wartość fałszu, zatem wartościowania w i z nie mają znaczenia. Ostatecznie, dla spełnienia kryterium pokrycia wielokrotnych warunków w sytuacji stosowania short-circuit evaluation zamiast 16 przypadków wystarczy tylko 5. Są one pokazane w tabeli 9.15. Gwiazdka oznacza dowolną wartość. Tabela 9.14. Przypadki testowe nieuwzględniające zwarcia
x
y
w
z
x
y
w
z
1 fałs z prawda prawda prawda 5 fałs z prawda prawda prawda 2 fałs z prawda prawda fałs z
6 fałs z prawda prawda fałs z
3 fałs z prawda fałs z
prawda 7 fałs z prawda fałs z
prawda
4 fałs z prawda fałs z
fałs z
fałs z
8 fałs z prawda fałs z
Tabela 9.15. Testy dla pokrycia wielokrotnych warunków z short-circuit
Id
x
y
w
z
D
PT1
fałs z
*
*
*
fałs z
PT2
prawda
fałs z
*
*
fałs z
PT3
prawda
prawda
prawda
*
prawda
PT4
prawda
prawda
fałs z
prawda
prawda
PT5
prawda
prawda
fałs z
fałs z
fałs z
Zauważmy, że chociaż formalnie zbiór ten nie spełnia kryterium pokrycia wielokrotnych warunków, to praktycznie tak jest – np. PT1 teoretycznie jest równoważny wszystkim ośmiu przypadkom, w których x jest fałszem. Skoro i tak nie wymusimy ewaluacji pozostałych warunków, to – w sensie przyjętego kryterium pokrycia – nie ma sensu tworzyć przypadków z wszystkimi możliwymi ich kombinacjami. Liczba przypadków w zredukowanym zbiorze testowym będzie oczywiście zależeć od struktury logicznej decyzji oraz od szczegółów technicznych optymalizacji stosowanej przez kompilator. Na przykład ewaluacja wartości logicznej wcale nie musi przebiegać od skrajnie lewego do skrajnie prawego warunku. Kompilator może przeprowadzać ewaluację w innej kolejności tak, aby zminimalizować średni czas potrzebny na określenie wartości logicznej decyzji. Znajomość tych technicznych szczegółów jest kluczowa dla testera, jeśli chce wykorzystać technikę short-circuit do redukcji przypadków testowych. Z semantyką short-circuit jest związany jeszcze jeden, być może poważniejszy, problem. Jeśli mamy na przykład decyzję D postaci A or B(), gdzie B jest wywoływaną funkcją, to w przypadku, gdy A przyjmuje wartość prawdy, B może w ogóle nie być wywołane! Jeśli ciało funkcji B inicjalizuje np. ważną część danych, to programista może być przekonany o tym, że ewaluacja wartości logicznej D musiała spowodować wykonanie funkcji B, a więc dane zostały zainicjalizowane. W przypadku stosowania short-circuit nie musi to być prawdą! zwarcie (ang. short-circuiting) – technika języków programowania/interpreterów przy obliczaniu warunków złożonych, w której wyliczanie warunku po jednej stronie operatora logicznego może być pominięte, jeśli ewaluacja warunku po drugiej stronie tego operatora jest wystarczająca do określenia wyniku końcowego
9.6.3. Nieosiągalne kombinacje warunków Czasami może się zdarzyć, że pewnych kombinacji warunków w obrębie jednej decyzji nie da się uzyskać. Może to wynikać ze stosowania short-circuit evaluation lub z faktu istnienia zależności między warunkami (mówi się wtedy, że zmienne są powiązane, ang. coupled). Rozważmy typowy warunek pętli, zakładając semantykę short-circuit:
while (iy && y>z || x y ∧ y > z), to w szczególności oznacza to, że musi być x > z, zatem trzeci warunek, x ≤ z, nie może być spełniony. Jeśli dwa pierwsze warunki są fałszem, implikuje to prawdziwość warunku trzeciego. Wartość logiczna warunku x ≤ z zależy od wartości logicznej dwóch pozostałych warunków. Gdy pewne elementy pokrycia lub warunki testowe są nieosiągalne dla pewnego kryterium pokrycia C, można je po prostu zignorować, gdyż zwykle ich wymuszony brak nie wpływa na jakość suity testowej. Lepszym rozwiązaniem jest jednak rozważenie spełnienia pokrycia dla innego, subsumowanego przez C kryterium. Na przykład, jeśli warunek testowy X jest nieosiągalny dla C, ale ten sam warunek da się pokryć w słabszym, subsumowanym przez C kryterium C′, to można wymienić nieosiągalne wymaganie w C na osiągalne wymaganie z C′. W przypadku grafowych kryteriów pokryć można też wykorzystać objazdy lub ścieżki poboczne opisane w podrozdziale 9.14.
9.6.4. Przykład Rozważmy funkcję RównanieKwadratowe z listingu 9.5 obliczającą pierwiastki równania ax2 + bx + c = 0. Chcemy skonstruować dla niej testy spełniające kryterium pokrycia warunków wielokrotnych. Zakładamy, że kompilator nie używa semantyki short-circuit.
RównanieKwadratowe(double a, double b, double c) 1 2 3
if (a==0 && b==0) then if (c==0) then return("Równanie nieoznaczone")
4 5 6
else return("Równanie sprzeczne") if (a==0) then
7 8 9 10 11
return "x = "+(-c/b) delta=b*b-4*a*c double x1=(-b+sqrt(delta))/(2*a) double x2=(-b-sqrt(delta))/(2*a) return "x1 = "+x1+", x2 = "+x2
Listing 9.5. Funkcja RównanieKwadratowe Krok 1. Określenie przedmiotu i elementów testów. Mamy tylko jeden element testów – testowaną funkcję: ET1: RównanieKwadratowe. Krok 2. Wyprowadzenie warunków testowych. Warunkami testowymi są wszystkie decyzje występujące w programie: WT1: (a = 0 ∧ b = 0) w linii 1. (dla ET1); WT2: (c = 0) w linii 2. (dla ET1); WT3: (a = 0) w linii 6. (dla ET1). Krok 3. Wyprowadzenie elementów pokrycia. WT2 i WT3 składają się z pojedynczych warunków, więc odpowiadające im elementy pokrycia będą dokładnie takie same, jak w kryterium pokrycia decyzji. Dla WT1 musimy natomiast rozważyć wszystkie 4 możliwe kombinacje prawdy i fałszu dla dwóch warunków stanowiących ten waruntek testowy. EP1: a = 0 jest prawdą, b = 0 jest prawdą (dla WT1);
EP2: a = 0 jest prawdą, b = 0 jest fałszem (dla WT1); EP3: a = 0 jest fałszem, b = 0 jest prawdą (dla WT1); EP4: a = 0 jest fałszem, b = 0 jest fałszem (dla WT1); EP5: a = 0 jest prawdą (dla WT2); EP6: a = 0 jest fałszem (dla WT2); EP7: a = 0 jest prawdą (dla WT3); EP8: a = 0 jest fałszem (dla WT3). Krok 4. Zdefiniowanie przypadków testowych. Zauważmy, że warunek a = 0 odpowiadający WT3 występuje również w WT1. Aby pokryć elementy dla WT3, musimy wymusić, aby sterowanie osiągnęło linię 6. kodu. Należy więc uważnie dobierać wartości dla a i b dla WT1 tak, aby działanie programu nie zakończyło się w linii 3. lub 5. Jeśli np. chcemy, by w linii 6. a = 0 było prawdą, to w warunku a = 0 ∧ b = 0 w linii 1. b = 0 musi być fałszem – w przeciwnym razie decyzja w linii 1. będzie prawdziwa i program wejdzie w blok linii 2.–5., kończąc swoje działanie przed osiągnięciem linii 6. Kompletny zbiór przypadków testowych jest podany w tabeli 9.16. Tabela 9.16. Przypadki testowe dla kryterium pokrycia wielokrotnych warunków
Id
a b
c a=0
b=0
c=0
Oczekiwany wynik
Pokryte EP
PT1 0 0
0 prawda prawda prawda
r. nieoznaczone
EP1, EP5
PT2 0 0
1 prawda prawda fałs z
r. s przeczne
EP1, EP6
PT3 0 1
4 prawda fałs z
x=4
EP2, EP7
PT4 1 0
9 fałsz
prawda (fałs z)
x1 = 3, x2 = –3
EP3, EP8
– 1
0 fałsz
fałs z
PT5 1
(fałs z)
(prawda) x1 = 0, x2 = 1
EP4, EP8
Elementy pokryte po raz pierwszy są oznaczone pogrubioną czcionką. Wartości logiczne ujęte w nawias oznaczają wartości nie wykorzystywane przy obliczaniu elementów pokrycia. Pogrubione wartości logiczne dotyczą warunku testowego WT3 (tylko dla tych przypadków sterowanie dochodzi do decyzji z WT3).
Jak widać warunek ten przyjmuje zarówno wartość prawdy (w PT3), jak i fałszu (w PT4 oraz w PT5). Kolumny a = 0 oraz b = 0 dotyczą warunku testowego WT1. Warunki w decyzji WT1 przyjmują wszystkie możliwe kombinacje: prawda/prawda (PT1 i PT2), prawda/fałsz (PT3), fałsz/prawda (PT4) oraz fałsz/fałsz (PT5). Warunek w decyzji WT2 również przyjmuje zarówno wartość prawdy (w PT1), jak i fałszu (w PT2). Wartości dla c = 0 w PT3, PT4 i PT5 nic nie pokrywają, bo w tych przypadkach sterowanie nie dochodzi do warunku WT2. Powyższe 5 przypadków wykorzystało wszystkie elementy pokrycia. Spełniliśmy zatem w 100% kryterium pokrycia wielokrotnych warunków dla testowanej funkcji.
9.7. Testowanie warunków znaczących (MC/DC) 9.7.1. Opis metody Pokrycie warunków wielokrotnych może skutkować dużym rozmiarem suity testowej. Technika pokrycia warunków/decyzji stanowi pewne remedium, jest bowiem czymś więcej niż pokrycie decyzji oraz warunków z osobna, a jednocześnie nie dopuszcza do wykładniczego wzrostu liczby przypadków testowych. Nie jest jednak kryterium idealnym. Gdy tworzymy testy na poziomie warunków, chcielibyśmy, aby wartość logiczna równocześnie wpływ na wartość logiczną decyzji.
danego
warunku
miała
Kryterium MC/DC pozwala na spełnienie tego wymagania, wymagając mniej testów niż w przypadku pokrycia warunków wielokrotnych jednocześnie będąc silniejsze niż pokrycie warunków/decyzji. Kryterium MC/DC jest popularne w przemyśle oprogramowania awioniki. Jest rówież wprost wymagane przez normę RTCA/DO-178C [8] dla oprogramowania systemów lotniczych o znaczeniu krytycznym. Właściwość, mówiąca o warunkach, jakie muszą być spełnione, aby zmiana wartości logicznej ustalonego warunku zmieniała wartość logiczną całej decyzji, jest nazywana determinacją decyzji przez warunek. Ten ustalony warunek (na którym aktualnie się skupiamy) nazywać będziemy warunkiem znaczącym. Pozostałe warunki decyzji nazywać będziemy warunkami pobocznymi. Należy pamiętać, że fakt ustalenia warunku jako znaczącego oznacza określenie wartości logicznych wszystkich pozostałych, pobocznych warunków w decyzji.
determinacja (ang. determination) decyzji D przez warunek W zawarty w tej decyzji oznacza, że wartość logiczna D zmieni się, jeśli zmieni się wartość logiczna W Rozważmy decyzję a ∨ b złożoną z dwóch warunków. Chcemy, aby a było warunkiem znaczącym. Aby zmiana wartości a spowodowała zmianę wartości a ∨ b, warunek b musi przyjąć wartość 0 (fałszu), bo wtedy a ∨ 0 przyjmie wartość fałszu dla a = 0 i prawdy dla a = 1, czyli wartość logiczna decyzji zmieni się przy zmianie wartości warunku a. Analogicznie, w przypadku, gdy b jest warunkiem znaczącym a a pobocznym, wartość a musi być ustawiona na 0. Kryterium pokrycia warunków znaczących (MC/DC, od ang. modified condition decision coverage) wymaga spełnienia kryterium pokrycia warunków/decyzji oraz dodatkowo tego, aby dla każdej decyzji każdy warunek znaczący przynajmniej raz przyjął wartość logiczną prawdy oraz przynajmniej raz wartość fałszu. testowanie warunków znaczących, zmodyfikowane testowanie warunków/decyzji (ang. modified condition/decision testing) – białoskrzynkowa technika projektowania testów, w której przypadki testowe są projektowane tak, aby wykonywać pojedyncze warunki wejścia, które niezależnie wpływają na wyjście decyzji Zdefiniowane powyżej oryginalne kryterium MC/DC stwarza niestety kilka problemów. Pierwszy, podobnie jak w przypadku innych kryteriów logicznych, dotyczy semantyki short-circuit oraz powiązania zmiennych. Kwestię tę przedyskutowaliśmy w punktach 9.6.2 i 9.6.3. Problem drugi jest związany ze statusem warunków pobocznych – przez wiele lat w środowisku testerów były prowadzone dyskusje, czy przy zmianie wartości logicznej warunku znaczącego wartości warunków pobocznych muszą pozostać niezmienione, czy też zmiany te są dopuszczalne. Nie są to rozważania czysto teoretyczne, gdyż w wielu przypadkach wymuszenie niezmienniczości wartości warunków pobocznych prowadzi do nieosiągalnych (ang. infeasible) wymagań. Jeśli chcemy tego uniknąć, musimy w jakiś sposób osłabić kryterium. Te rozważania doprowadziły do zdefiniowania różnych wersji kryterium MC/DC. Omówimy teraz dwa z nich.
9.7.2. Dwa warianty kryterium MC/DC
Kryterium skorelowanego pokrycia warunków znaczących. Kryterium to wymaga, aby w każdej decyzji każdy warunek znaczący przyjął przynajmniej raz wartość prawdy i przynajmniej raz fałszu, przy czym wartości warunków pobocznych muszą wymuszać prawdziwość decyzji w jednym przypadku i fałszywość w drugim. Zauważmy, że tak zdefiniowane kryterium subsumuje kryterium pokrycia decyzji6, bo wymuszamy, aby każda decyzja przyjęła obie wartości logiczne. Subsumuje również kryterium pokrycia warunków, gdyż z definicji wymaga, aby każdy warunek (w momencie, gdy jest traktowany jako warunek znaczący) przyjął obie wartości logiczne. Wartość decyzji nie musi być taka sama, jak wartość warunku znaczącego. Ważne jest tylko, aby przy zmianie wartości warunku znaczącego decyzja również zmieniła wartość logiczną. Kryterium to dopuszcza inne wartościowania dla warunków pobocznych dla obu wartościowań warunku znaczącego. Rozważmy na przykład decyzję D = p ∧ (q ∨ r). Aby p zdeterminowało D, q ∨ r musi mieć wartość logiczną 1 (prawdy). Możemy to osiągnąć na trzy sposoby: (q, r) ∈ {(1, 0), (0, 1), (1, 1)}. Aby spełnić kryterium względem warunku znaczącego p, możemy np. wybrać testy (p, q, r) = (1, 1, 0) oraz (p, q, r) = (0, 0, 1). Aby q zdeterminowało D, musi być p = 1 ∧ r = 0. Zatem, aby spełnić kryterium względem warunku znaczącego q, mamy tylko jeden możliwy zestaw testów do wyboru: (p, q, r) = (1, 1, 0) oraz (p, q, r) = (1, 0, 0). Aby q zdeterminowało D, musi być p = 1 ∧ q = 0, zatem znów mamy tylko jeden możliwy zestaw testów: (p, q, r) = (1, 0, 1) oraz (p, q, r) = (1, 0, 0). W tabeli 9.17 są zebrane wszystkie kombinacje wartości logicznych warunków potrzebne do spełnienia kryterium skorelowanego pokrycia warunków znaczących. Tabela 9.17. Wartościowanie warunków dla skorelowanego pokrycia warunków znaczących
W arunek znaczący p
q
p
q
r
p ∧ (q ∨ r)
1
1
0
1
0
0
1
0
1
1
0
1
1
0
0
0
r
1
0
1
1
1
0
0
0
Szare pola oznaczają wartości warunku znaczącego. Jak widać, dla każdego z nich zmiana wartości powoduje zmianę wartości całej decyzji. W przypadku warunków znaczących q i r wartości warunków pobocznych nie zmieniają się przy zmianie wartości warunku znaczącego. W przypadku p wartości warunków pobocznych się zmieniają, ale jest to dopuszczalne przez kryterium pokrycia skorelowanego. Zauważmy, że niektóre przypadki występują więcej niż raz. Możemy zatem zredukować suitę testową, eliminując powtórzenia (patrz tab. 9.18). Takie duplikaty będą występowały zawsze. Okazuje się, że dla decyzji złożonej z n warunków potrzebnych jest nie 2 n, ale n + 1 testów, aby spełnić kryterium MC/DC. Tabela 9.18. Skorelowane pokrycie warunków znaczących – po usunięciu duplikatów
p
q
r
p ∧ (q ∨ r)
1
1
0
1
0
0
1
0
1
0
1
1
1
0
0
0
Kryterium ścisłego pokrycia warunków znaczących. Jest to oryginalna postać kryterium MC/DC. Wymaga ono, aby w każdej decyzji każdy warunek znaczący przyjął przynajmniej raz wartość prawdy i przynajmniej raz fałszu, przy czym wartości warunków pobocznych muszą być takie same dla obu wartościowań warunku znaczącego. Zauważmy, że chociaż nie jest to powiedziane wprost, kryterium to subsumuje pokrycie decyzji. Wynika to z własności warunku znaczącego – zmiana jego wartości logicznej powoduje zmianę wartości logicznej decyzji, zatem spełniając
kryterium ścisłego pokrycia warunków znaczących, wymusimy, aby każda decyzja przyjęła zarówno wartość prawdy, jak i fałszu. Rozważmy ponownie decyzję D = p ∧ (q ∨ r). W kryterium skorelowanego pokrycia, przyjmując p jako warunek znaczący, zauważyliśmy, że wartości q i r możemy wybrać na trzy sposoby, niezależnie dla każdej ewaluacji p. W kryterium ścisłego pokrycia wartości q i r również możemy wybrać na trzy sposoby, ale wybrane wartościowanie musi być takie samo dla obu ewaluacji p. Możemy wybrać na przykład wartości (p, q, r) = (1, 1, 0) oraz (p, q, r) = (0, 1, 0). Dla warunków znaczących q i r mamy tylko jedną możliwość wyboru wartości logicznych wszystkich warunków, dokładnie tak samo jak w kryterium skorelowanego
pokrycia. Ostatecznie, zbiór
przypadków testowych może
wyglądać tak jak w tabeli 9.19. Po usunięciu duplikatów, zbiór testowy zredukuje się do czterech przypadków (patrz tab. 9.20). Tabela 9.19. Wartościowania warunków dla ścisłego pokrycia warunków znaczących
Id P1 P2 Q1 Q2 R1 R2
W arunek znaczący
p
p
q
r
q
r
p ∧ (q ∨ r)
1
1
0
1
0
1
0
0
1
1
0
1
1
0
0
0
1
0
1
1
1
0
0
0
Tabela 9.20. Ścisłe pokrycie warunków znaczących – przypadki po usunięciu duplikatów
Id
p
q
r
p ∧ (q ∨ r)
1 (P1, Q1)
1
1
0
1
2 (P2)
0
1
0
0
3 (R1)
1
0
1
1
4 (Q2, R2)
1
0
0
0
9.7.3. Skorelowane a ścisłe pokrycie warunków znaczących Istnieje nieoczywista na pierwszy rzut oka, ale znacząca ze względów praktycznych, różnica między kryterium pokrycia skorelowanego a ścisłego. Okazuje się, że w sytuacji, gdy między warunkami w ramach jednej decyzji zachodzą pewne zależności, kryterium skorelowanego pokrycia może być spełnione w 100%, natomiast kryterium ścisłego pokrycia będzie miało wymagania nieosiągalne. Rozważmy następujący przykład: system, mogący pracować w trybie administratora lub użytkownika zwykłego, operuje na pliku, który może być otwarty do zapisu lub do odczytu. Załóżmy, że zachodzą następujące dwa ograniczenia: (1) Użytkownik musi być administratorem dla pliku w trybie zapisu oraz zwykłym użytkownikiem dla pliku w trybie odczytu; (2) Plik nie może być w tym samym czasie otwarty w trybie zapisu i odczytu. Zdefiniujmy następujące warunki: p = system pracuje w trybie zwykłego użytkownika; q = plik jest otwarty w trybie do zapisu; r = plik jest otwarty w trybie do odczytu. Załóżmy, że pewna akcja w systemie może być podjęta tylko wtedy, gdy operatorem jest zwykły użytkownik, a plik jest w trybie odczytu lub zapisu: D = p ∧ (q ∨ r). Jest to dokładnie ta sama decyzja, którą rozważaliśmy w punkcie 9.7.2. Ograniczenia (1) i (2) możemy wyrazić następującymi formułami logicznymi: (1) ~p ⇔ q (2) ~(q ∧ r) Ograniczenia sprawiają, że niektóre kombinacje warunków stają się nieosiągalne. Tablica prawdy7 dla predykatu D jest pokazana w tabeli 9.21 wraz
z komentarzem dotyczącym nieosiągalności. Tabela 9.21. Tablica prawdy dla predykatu D = p ∧ (q ∨ r)
Id
p
q
r
p ∧ (q ∨ c)
Uwagi
1
1
1
1 1
narus za og raniczenia (1) i (2)
2
1
1
0 1
narus za og raniczenie (1)
3
1
0
1 1
4
1
0
0 0
5
0
1
1 0
6
0
1
0 0
7
0
0
1 0
narus za og raniczenie (1)
8
0
0
0 0
narus za og raniczenie (1)
narus za og raniczenie (2)
Jedynie wiersze 3, 4 i 6 opisują poprawne kombinacje. Załóżmy, że chcemy spełnić oba kryteria dla p jako warunku znaczącego. Kryterium ścisłego pokrycia wymaga wyboru jednej z par: 2 i 6, 3 i 7 lub 1 i 5. W każdej z tych par jedna z kombinacji jest jednak niedopuszczalna. Kryterium skorelowanego pokrycia dla p można natomiast spełnić, wybierając np. kombinacje 3 i 6. W tabeli 9.22 podsumowano metodę testowania warunków znaczących. Tabela 9.22. Podsumowanie metody testowania warunków znaczących
W arunki W ierzchołki CFG reprezentujące decyzje testowe Elementy Warunki znaczące pokrycia Każdy warunek znaczący mus i przynajmniej raz przyjąć Kryterium wartoś ć prawdy i przynajmniej raz wartoś ć fałs zu; warunki pokrycia poboczne nie mog ą s ię zmieniać (ś cis łe pokrycie) lub
zmiana wartoś ci warunku znacząceg o mus i powodować zmianę wartoś ci decyzji (s korelowane pokrycie)
Pokrycie
p =
liczba pokrytych wartoś ci warunków znaczących liczba ws zys tkich wartoś ci warunków znaczących
× 100%
9.7.4. Wyznaczanie wartości warunków pobocznych dla warunku znaczącego W przykładach z poprzednich rozdziałów nie uzasadniliśmy formalnie, skąd braliśmy wartości warunków pobocznych tak, aby ustalony warunek stał się rzeczywiście warunkiem znaczącym. Opiszemy teraz metodę, która rozwiązuje ten problem. Wykorzystuje ona operator logiczny XOR, czyli alternatywę rozłączną, oznaczaną symbolem ⊕. Alternatywa rozłączna p ⊕ q jest zdaniem prawdziwym wtedy i tylko wtedy, gdy dokładnie jedno spośród p i q jest prawdziwe. Tablica prawdy dla operatora XOR podana jest w tabeli 9.23. Tabela 9.23. Tablica prawdy dla operatora XOR
p
q
p⊕ q
1
1
0
1
0
1
0
1
1
0
0
0
Niech D będzie decyzją zawierającą warunek W. Przez DW=1 (odp. DW=0) będziemy oznaczać decyzję D, w której warunek W przyjął odpowiednio wartość prawdy lub fałszu. Przez DW będziemy oznaczać formułę logiczną opisującą warunki, jakie muszą zajść, aby W było warunkiem znaczącym. Dla wszystkich wartościowań warunków takich, że DW jest prawdą, warunek W jest znaczący. Na przykład, jeśli DW = U ∧ X, gdzie U, X są warunkami wchodzącymi w skład D, aby
W był znaczący, U ∧ X musi być prawdą, tzn. zarówno warunek U, jak i X muszą być prawdziwe. Formuła DW jest wyznaczona przez połączenie operatorem XOR dwóch wersji D: w jednej W jest prawdą, a w drugiej – fałszem: DW = DW=1 ⊕ DW=0 Rozważmy teraz 4 przykłady, analizując cztery możliwe postaci, jakie może przyjąć DW. W każdym z nich chcemy określić warunki, jakie muszą być spełnione, aby p był warunkiem znaczącym. Przykład 1. Niech D = p ∨ q. Wtedy Dp = Dp=1 ⊕ Dp=0 = (1 ∨ q) ⊕ (0 ∨ q) = 1 ⊕ q = ~q Ostatnia równość wynika stąd, że XOR jest prawdą tylko wtedy, gdy dokładnie jeden z jego warunków jest prawdziwy, zatem 1 ⊕ q będzie prawdą, gdy q będzie fałszem i na odwrót – 1 ⊕ q będzie fałszem, gdy q będzie prawdą. Aby p było warunkiem znaczącym ~q, musi być prawdą, zatem q musi przyjąć wartość 0. Przykład 2. Niech D = p ⇔ q8. Wtedy Dp = Dp=1 ⊕ Dp=0 = (1 ⇔ q) ⊕ (0 ⇔ q) = q ⊕ ~q = 1 Formuła Dp jest tautologią, tzn. zawsze przyjmuje wartość logiczną prawdy, niezależnie od wartości jej klauzul. Oznacza to, że p jest warunkiem znaczącym w D niezależnie od tego, jaka jest wartość q. Przykład 3. Niech D = q ∧ p ∨ q ∧ ~p. Tak naprawdę ta decyzja to D = q, warunek p jest tu irrelewantny. Wtedy Dp = Dp=1 ⊕ Dp=0 = (q ∧ 1 ∨ q ∧ 0) ⊕ (q ∧ 0 ∨ q ∧ 1) = (q ∨ 0) ⊕ (0 ∨ q) = q ⊕ q = 0 Otrzymaliśmy wynik przeciwny niż w Przykładzie 2 – wartość Dp jest zawsze fałszywa. Oznacza to, że nie istnieje takie wartościowanie pozostałych warunków, aby p był warunkiem znaczącym. Przykład 4. Niech D = q ∧ (p ∨ r). Wtedy Dp = Dp=1 ⊕ Dp=0 = (q ∧ (1 ∨ r)) ⊕ (q ∧ (0 ∨ r)) = (q ∧ 1) ⊕ (q ∧ r) = q ⊕ (q ∧ r) = q ∧ ~r Ostatni krok może nie być oczywisty. Aby XOR miał wartość 1, q oraz q ∧ r muszą mieć odmienne wartości logiczne. Jeśli q = 1, to r musi być 0, aby q ∧ r = 1 ∧
r było fałszem. Jeśli q = 0, to każda wartość r da w rezultacie fałsz XORa, bo oba argumenty będą równe 0. Aby p był warunkiem znaczącym, wartość q ∧ ~r musi być prawdą, a to zachodzi tylko dla q = 1 oraz r = 0.
9.7.5. Przykład Rozważmy ponownie funkcję leapYear z listingu 9.4. Występuje tam predykat ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) Oznaczmy poszczególne warunki tej decyzji następująco: p := year%4 = 0 q := year%100 ≠ 0 r := year%400 = 0 Chcemy przetestować decyzję (p ∧ q) ∨ r za pomocą kryterium MC/DC w wersji ścisłej. Załóżmy, że kompilator nie stosuje semantyki short-circuit. Występują natomiast pewne ograniczenia między zmiennymi: (1) jeśli r = 1, to p = 1 i q = 0; (2) jeśli q = 0, to p = 1; (3) jeśli q = 1, to r = 0; (4) jeśli p = 0, to q = 1 i r = 0. Krok 1. Określenie przedmiotu i elementów testów. Przedmiotem (i jednocześnie elementem) testów jest funkcja leapYear. ET1: leapYear. Krok 2. Wyprowadzenie warunków testowych. Warunkiem testowym jest decyzja występująca w instrukcji warunkowej. WT1: ((year % 4 = 0) ∧ (year % 100 ≠ 0)) ∨ (year % 400 = 0) (dla ET1).
Krok 3. Wyprowadzenie elementów pokrycia. Zanim wyprowadzimy elementy pokrycia zobaczmy, które kombinacje warunków są w ogóle dopuszczalne. W tabeli 9.24 pokazano tablicę prawdy dla decyzji WT1 z uwagami o naruszeniu podanych wcześniej czterech ograniczeń. Dozwolone są jedynie kombinacje nr 2, 3, 4 i 6. Elementy pokrycia są następujące (wszystkie dla WT1): EP1: p = 0 i p jest warunkiem znaczącym; EP4: q = 1 i q jest warunkiem znaczącym; EP2: p = 1 i p jest warunkiem znaczącym; EP5: r = 0 i r jest warunkiem znaczącym; EP3: q = 0 i q jest warunkiem znaczącym; EP6: r = 1 i r jest warunkiem znaczącym. Obliczmy
formuły
opisujące
wartości
warunków
pobocznych
poszczególnych warunków znaczących: DP = (1 ∧ q) ∨ r ⊕ (0 ∧ q) ∨ r = (q ∨ r) ⊕ r = q ∧ ~r Dq = (p ∧ 1) ∨ r ⊕ (p ∧ 0) ∨ r = (p ∨ r) ⊕ r = p ∧ ~r
Dr = (p ∧ q) ∨ 1 ⊕ (p ∧ q) ∨ 0 = 1 ⊕ (p ∧ q) = ~(p ∧ q) Tabela 9.24. Tablica prawdy dla predykatu funkcji leapYear
Lp.
p q r (p ∧ q) ∨ r
Uwagi
1
1 1 1 1
narus za og raniczenia (1) i (3)
2
1 1 0 1
3
1 0 1 1
4
1 0 0 0
5
0 1 1 1
6
0 1 0 0
7
0 0 1 1
narus za og raniczenia (1), (2) i (4)
8
0 0 0 0
narus za og raniczenia (2) i (4)
narus za og raniczenia (1), (3) i (4)
dla
Aby p było warunkiem znaczącym (EP1 i EP2), musi być q = 1 ∧ r = 0. Dla EP1 i EP2 możemy wybrać tylko kombinacje nr 6 i 2. Pokrywają one kryterium MC/DC w wersji ścisłej dla p jako warunku znaczącego. Aby q było warunkiem znaczącym (EP3 i EP4), musi być p = 1 ∧ r = 0. Dla EP3 i EP4 możemy wybrać tylko kombinacje nr 4 i 2. Pokrywają one kryterium MC/DC w wersji ścisłej dla q jako warunku znaczącego. Aby r było warunkiem znaczącym (EP5 i EP6), są dopuszczalne wszystkie kombinacje p i q za wyjątkiem p = q = 1. Wybór kombinacji nr 4 i 3 spełnia kryterium MC/DC w wersji ścisłej dla r jako warunku znaczącego. Krok 4. Zdefiniowanie przypadków testowych. Z analizy przeprowadzonej w Kroku 3. wynika, że musimy wykorzystać wszystkie cztery dopuszczalne kombinacje warunków, tak jak to pokazano w tabeli 9.25. Tabela 9.25. Przypadki testowe spełniające kryterium MC/DC dla funkcji leapYear
q := year%100 ≠0
r := year%400 =0
D
PT1 2008 1
1
0
1 EP2, EP4
PT2 1600 1
0
1
1 EP6
PT3 1500 1
0
0
0 EP3, EP5
PT4 1999 0
1
0
0 EP1
Id
year
p := year%4 =0
Pokryte EP
Kryterium MC/DC w wersji ścisłej udało nam się pokryć w 100%, mimo istnienia zależności między warunkami. W każdej kolumnie wartości w szarych komórkach odpowiadają wartościom warunku znaczącego. Czytelnik może sprawdzić, że w odpowiednich wierszach wartości logiczne warunków pobocznych są niezmienne (np. dla warunku znaczącego p odpowiadające mu wiersze to PT1 i PT4, w których warunki q i r mają odpowiednio wartości 1 oraz 0).
9.8. Pokrycie MUMCUT oraz kryteria z nim związane
9.8.1. Opis metody Kryterium MUMCUT zostało zdefiniowane w pracy [118] jako strategia oparta na defektach, która gwarantuje znalezienie określonych typów błędów w specyfikacji wyrażonej w języku logiki. W podejściu tym zakłada się, że każda zmienna w wyrażeniu logicznym może przyjąć wartość logiczną niezależnie od wartości innych zmiennych w tym wyrażeniu. Można pokazać, że każdy predykat da się zapisać w tzw. dysjunktywnej postaci normalnej DNF (ang. Disjunctive Normal Form), która ma postać alternatywy koniunkcji klauzul. Przykładem formuły w postaci DNF jest formuła ab + cde + bef + af gdzie a, b, c, d, e, f są klauzulami, operacja mnożenia oznacza koniunkcję, a symbol + oznacza alternatywę. Predykat w postaci DNF można zawsze zapisać w tzw. nieredundantnej dysjunktywnej postaci normalnej IDNF (ang. Irredundant Disjunctive Normal Form), tzn. w postaci DNF, gdzie żadna z klauzul nie jest redundantna. Klauzula k jest redundantna dla predykatu P, jeśli P da się zapisać bez użycia k. Na przykład, predykat (ab + a)c można zapisać równoważnie jako ac – klauzula b jest redundantna. Jej wartość logiczna nie ma wpływu na wartość logiczną predykatu. Niech P będzie predykatem, t przypadkiem testowym a p koniunkcją klauzul. Przez P(t) (odpowiednio p(t)) oznaczać będziemy wartość logiczną predykatu P (odpowiednio wartość logiczną koniunkcji klauzul p) dla wartości logicznych zmiennych określonych w t. Przypadek testowy t dla predykatu P = p 1 + p 2 +···+ p m zapisanego w postaci IDNF nazywamy punktem prawdy (odpowiednio punktem fałszu), jeśli P(t) = 1 (odpowiednio P(t) = 0). Jeśli jednak tylko dla jednego p i zachodzi p i(t) = 1, a dla każdego j ≠ i mamy p j(t) = 0, to t jest nazywany unikalnym punktem
prawdy. Jeśli p i = x1, x2, …, xn, gdzie xj są klauzulami, to przez p i,–k oznaczać będziemy predykat otrzymany z p i przez zanegowanie k tej klauzuli xk . Przypadek testowy t nazwiemy punktem prawie fałszywym klauzuli xk w p i wchodzącego w skład predykatu P, jeśli P(t) = 0, ale p i,–k (t) = 1. Jesteśmy teraz gotowi do tego, by zdefiniować kryterium MUMCUT oraz inne, powiązane z nim kryteria. Zakładamy, że kryteria są stosowane do predykatu P = p 1 + p 2 + … + p m zapisanego w IDNF, tzn. każdy p i ma postać klauzulami.
gdzie xj są
9.8.2. Kryteria MUTP, MNFP, CUTPNFP i MUMCUT Pokrycie MUTP (ang. Multiple Unique True Point) – dla każdego i zbiór przypadków testowych zawiera unikalne punkty prawdy dla p i takie, że wszystkie możliwe wartości logiczne (prawdy i fałszu) każdej zmiennej niewystępującej w p i są pokryte. Pokrycie MNFP (ang. Multiple Near False Point) – dla każdych i, j zbiór przypadków testowych zawiera punkty prawie fałszywe dla j-tej klauzuli składnika p i takie, że wszystkie możliwe wartości logiczne (prawdy i fałszu) każdej zmiennej niewystępującej w p i są pokryte. Pokrycie CUTPNFP (ang. Corresponding Unique True Point And Near False Point) – dla każdych i, j dla których jest to możliwe zbiór przypadków testowych T zawiera unikalny punkt prawdy t ∈ T dla p i oraz punkt prawie fałszywy t′ ∈ T dla
j-tej klauzuli w p i takie, że t i t′ różnią się tylko na wartości logicznej j-tej klauzuli w p i. Taką parę testów t i t′ będziemy nazywać UN-parą. Pokrycie MUMCUT (ang. MUTP, MNFP and CUTPNFP criteria) – zbiór przypadków testowych spełnia MUMCUT, gdy spełnia jednocześnie kryteria MUTP, MNFP oraz CUTPNFP.
9.8.3. Przykład Rozważmy predykat P = ab + cd, czyli alternatywę dwóch koniunkcji, (a ∧ b) ∨ (c ∧ d), gdzie a, b, c, d są klauzulami. Każdy przypadek testowy składa się z czterech wartości logicznych odpowiadających wartościom klauzul a, b, c, d. Dla wygody przypadki zapisywać będziemy jako ciąg czterech wartości logicznych, np. t = 0100 oznacza przypadek, w którym b ma wartość logiczną prawdy, a pozostałe klauzule – wartość logiczną fałszu. Zbiór przypadków testowych T = {1101, 1110, 0111, 1011} spełnia kryterium MUTP, ponieważ: dwa pierwsze przypadki, 1101 oraz 1110 są unikalnymi punktami prawdy dla pierwszego składnika P, czyli termu ab i rozważane razem pokrywają wszystkie możliwe wartości logiczne dla predykatów niewystępujących w ab, czyli dla c i d; dwa pozostałe przypadki, 0111 oraz 1011 są unikalnymi punktami prawdy dla drugiego składnika P, czyli termu cd i rozważane razem
pokrywają wszystkie możliwe wartości logiczne dla predykatów niewystępujących w cd, czyli dla a i b. Zbiór przypadków testowych T = {0101, 0110, 1001, 1010} spełnia kryterium MNFP, ponieważ: przypadki 0101, 0110 są punktami prawie fałszywymi dla a i pokrywają wszystkie wartości logiczne klauzul niewystępujących pierwszym składniku P, czyli klauzul c i d;
w
ab
–
przypadki 1001, 1010 są punktami prawie fałszywymi dla b i pokrywają wszystkie wartości logiczne klauzul niewystępujących w ab – pierwszym składniku P, czyli klauzul c i d; przypadki 0101, 1001 są punktami prawie fałszywymi dla c i pokrywają wszystkie wartości logiczne klauzul niewystępujących w cd – drugim składniku P, czyli klauzul a i b; przypadki 0110, 1010 są punktami prawie fałszywymi dla d i pokrywają wszystkie wartości logiczne klauzul niewystępujących w cd – drugim składniku P, czyli klauzul a i b. Zbiór przypadków testowych T = {1100, 0100, 1000, 0011, 0001, 0010} spełnia kryterium CUTPNFP, ponieważ: przypadki {1100, 0100} tworzą UN-parę dla pierwszego składnika ab, czyli dla a; przypadki {1100, 1000} tworzą UN-parę dla drugiego składnika ab, czyli dla b; przypadki {0011, 0001} tworzą UN-parę dla pierwszego składnika cd, czyli dla c; przypadki {0011, 0010} tworzą UN-parę dla drugiego składnika cd, czyli dla d. Zbiór przypadków testowych T = {0001, 0010, 0011, 0100, 0101, 0110, 0111, 1000, 1001, 1010, 1011, 1100, 1101, 1110} spełnia MUMCUT, ponieważ jest sumą mnogościową przypadków spełniających kryteria MUTP, MNFP i CUTPNFP.
9.8.4. Zdolność metody MUMCUT do wykrywania określonych typów błędów
Opiszemy teraz kilka typów błędów, dla których – jeśli zostaną popełnione – stosowanie kryterium MUMCUT będzie gwarantowało ich wykrycie z definicji lub jeśli będą spełnione pewne dodatkowe, określone warunki. Jako przykład rozważmy następujący predykat, o którym zakładamy, że jest poprawny: , gdzie oznacza negację x. Poniższe rozważania są poprawne tylko dla predykatów zapisanych w postaci DNF. Jak wspomnieliśmy wcześniej, każdy predykat można przekształcić do tej postaci. ENF – błąd negacji wyrażenia (ang. Expression Negation Fault) – wyrażenie lub jego część jest zastąpiona jego negacją, np. P jest zastąpione przez TNF – błąd negacji termu (ang. Term Negation Fault) – term (tu: koniukcja klauzul) jest zastąpiony przez jego negację, np. P jest zastąpione przez TOF – błąd pominięcia termu (ang. Term Omission Fault) – term jest ominięty, np. P jest zastąpione przez
;
ORF – błąd operatora (ang. Operator Reference Fault) – operator alternatywy jest zastąpiony operatorem koniunkcji lub na odwrót, np. P jest zastąpione przez
albo np.
;
LNF – błąd negacji klauzuli (ang. Literal Negation Fault) – klauzula jest zastąpiona przez jej negację, np. P jest zastąpione przez
; ten
błąd często jest zwany błędem negacji zmiennej [119]; LOF – błąd pominięcia klauzuli (ang. Literal Negation Fault) – klauzula jest pominięta, np. P jest zastąpione przez
;
LIF – błąd nadmiarowej klauzuli (ang. Literal Insertion Fault) – do predykatu jest dodana klauzula, np. P jest zastąpione przez ; LRF – błąd zamiany klauzuli (ang. Literal Reference Fault) – klauzula jest zamieniona na inną, np. P jest zastąpione przez
; ten błąd
często jest określany jako błąd zamiany zmiennej; STF – błąd utknięcia (ang. Stuck-At Fault) – błąd powodujący stałą wartość klauzuli, np. gdy a ma zawsze wartość 1, P będzie równoważna , czyli będzie równoważna
; jeśli a będzie miało zawsze wartość 0, P .
Badania empiryczne pokazują, że rzeczywiste awarie w oprogramowaniu są w dużej mierze powodowane przez pomyłki programistów polegające na dodaniu lub usunięciu warunków w predykatach, a także użyciu niepoprawnych operatorów bądź operandów [10], [120], [121]. Kryterium MUMCUT dobrze nadaje się do wykrywania wymienionych błędów. Wiadomo, że zastosowanie MUMCUT dla predykatów w postaci IDNF gwarantuje wykrycie wszystkich dziewięciu wymienionych
typów
błędów.
MUMCUT
subsumuje
kryterium
pokrycia
warunków/decyzji. Ponadto, pod pewnymi warunkami subsumuje również kryterium MC/DC.
9.9. Testowanie pętli 9.9.1. Opis metody Jeśli program zawiera przynajmniej jedną pętlę, to liczba możliwych ścieżek wykonania jest potencjalnie nieskończona. Stwarza to problemy z wyczerpującym testowaniem ścieżek, bo po prostu jest to fizycznie niemożliwe. Jak zatem poradzić sobie z istnieniem w CFG pętli? Najprostsze podejście polega na dostarczeniu takich przypadków testowych, aby każda pętla wykonała się 0 razy oraz dokładnie raz9. Niektórzy autorzy dodają jeszcze warunek, aby pętla wykonała się więcej niż raz, przy czym zwykle dokładna liczba wykonań pętli nie jest sprecyzowana. Wynika to stąd, że w praktyce bardzo trudno jest dostarczyć dane wejściowe, które spowodują wykonanie danej pętli konkretną liczbę razy (najłatwiej zwykle wymusić wykonanie pętli 0 razy i 1 raz). W literaturze można spotkać także wymaganie, aby pętla była przetestowana maksymalną możliwą liczbę razy (jeśli znamy tę liczbę) [80]. Boris Beizer idzie jeszcze dalej. W swojej książce [14] proponuje następujące kryteria co do testowania pętli (zauważmy, że stosuje tu podejście trzech wartości granicznych, wykorzystując technikę analizy wartości brzegowych do liczby przebiegów pętli): jeśli to możliwe, przetestuj pętlę dla wartości mniejszej o 1 od najmniejszej wartości zmiennej iterującej; np. jeśli iterator w pętli może przebiegać od 0 do 100, spróbuj wymusić wartość –1 iteratora;
przetestuj minimalną liczbę iteracji (zwykle 0 lub – w przypadku dowhile – 1); przetestuj dla liczby iteracji większej o 1 od minimalnej liczby iteracji; przetestuj pętlę raz (jeśli jest to przypadek redundantny, pomiń go); przetestuj pętlę dwa razy (jeśli jest to przypadek redundantny, pomiń go); przetestuj pętlę dla typowej wartości przebiegów; przetestuj pętlę dla liczby przebiegów mniejszej o 1 od maksymalnej wartości; przetestuj pętlę dla maksymalnej liczby przebiegów; przetestuj pętlę dla liczby przebiegów większej o 1 od maksymalnej wartości. Jak widać, Beizer kładzie duży nacisk na testowanie negatywne, tzn. na testowanie pętli dla przebiegów, których liczba jest nieprawidłowa (np. ujemna lub większa od maksymalnej). W praktyce nie wszystkie z powyższych kryteriów mogą być osiągalne. Jeśli któregoś z nich nie da się wymusić, to należy go po prostu pominąć. Warunkami testowymi są pętle występujące w programie. Elementy pokrycia określają wymagane liczby iteracji każdej z pętli, a ich postać zależy od przyjętej metodologii. W tabeli 9.26 podsumowano metodę testowania pętli. Tabela 9.26. Podsumowanie metody testowania pętli
W arunki testowe
Pętle
Elementy pokrycia
Liczba wykonania każdej z pętli
Kryterium pokrycia
Zależnie od metody; każda pętla powinna być wykonana okreś loną liczbę razy
Pokrycie
p =
liczba pokrytych wykonań pętli liczba ws zys tkich wymag anych wykonań pętli
× 100%
9.9.2. Pętle zagnieżdżone W przypadku pętli zagnieżdżonych (jedna pętla w drugiej) Beizer proponuje następujące podejście: 1) rozpocznij od najbardziej wewnętrznej pętli, ustawiając wszystkie pętle zewnętrzne wobec niej na minimalną liczbę iteracji (tak, żeby pętla wewnętrzna mogła się wykonać); 2) przetestuj warunki brzegowe (wg 9 kryteriów podanych powyżej) dla pętli wewnętrznej; 3) jeśli przetestowałeś najbardziej zewnętrzną pętlę, idź do punktu 5.; 4) kontynuuj proces opisany powyżej dla każdej pętli z osobna, dopóki nie przetestujesz wszystkich pętli; 5) przetestuj warunki brzegowe dla wszystkich pętli naraz, tzn. postaraj się wymusić dla wszystkich pętli przebieg 0 iteracji, 1 iterację, maksymalną liczbę iteracji i liczbę o 1 większą od maksymalnej liczby iteracji. W praktyce spełnienie punktu 5. może być trudne lub niemożliwe. Można ten warunek nieco osłabić, wymagając np. maksymalnej liczby iteracji dla tak wielu pętli, jak to jest możliwe.
9.9.3. Testowanie wzorców pętli Bardzo ciekawa metoda testowania pętli – wykorzystująca tzw. wzorce pętli – jest podana w [122]. Pozwala ona w sposób systematyczny tworzyć przypadki testowe dla pętli zagnieżdżonych, zakładając, że obliczenia wszystkich ścieżek testowanego programu są liniowe. Wykorzystując metody algebry, w sposób analogiczny do metody punktów ON-OFF oblicza się minimalną liczbę testów10 potrzebnych do przetestowania każdej sekwencji, czyli ścieżki zawierającej pętlę. Sekwencje identyfikuje się przez budowę drzewa zagnieżdżeń. Jego pierwszy poziom stanowią ścieżki proste, czyli ścieżki biegnące od wierzchołka początkowego do końcowego i niezawierające pętli, a kolejne poziomy reprezentują cykle proste, których poziom zagnieżdżenia jest równy numerowi poziomu. Na rysunku 9.4 jest pokazany przykładowy CFG oraz jego drzewo zagnieżdżeń.
Rysunek 9.4. CFG i jego drzewo zagnieżdżeń pętli Drzewo zagnieżdżeń umożliwia identyfikację tzw. ścieżek iterowanych, czyli ścieżek zawierających pętle. Na przykład dla drzewa z rysunku 9.4 mamy cztery ścieżki iterowane: S1 = 1 (2 3 4)* 2 3 4 5 S2 = 1 2 (3 6)* 3 4 5 S3 = 1 (2 3 4)* 2 3 6 7 S4 = 1 2 (3 6)* 3 6 7 gdzie (X)* oznacza dowolną liczbę powtórzeń X (0 lub więcej). Metoda testowania wzorców pętli składa się z 4 kroków: znajdź w CFG wszystkie ścieżki proste i cykle; zbuduj drzewo zagnieżdżeń dla pętli w celu identyfikacji wszystkich ścieżek iterowanych; oblicz , gdzie k to liczba zmiennych wejściowych, a m – liczba zmiennych programu11;
dla każdej sekwencji wykonaj n różnych ścieżek, wliczając w to ścieżkę bazową. Zobaczmy na przykładzie działanie metody. Rozważmy ponownie CFG z rysunku 9.4 wraz z jego drzewem zagnieżdżeń. Załóżmy, że mamy k = 3 wartości wejściowe oraz m = 2 zmienne programu oraz że obliczenia ścieżek są liniowe. Mamy
Dla każdej sekwencji ścieżek iterowanych musimy zatem wykonać
2 ścieżki, wliczając w to ścieżki bazowe. Zbiór testowy powinien zatem zawierać 6 elementów, odpowiadających ścieżkom testowym pokazanym w tabeli 9.27. Tabela 9.27. Pokrycie ścieżek iterowanych w metodzie wzorców pętli
Id
Ścieżka testowa
Pokryte ścieżki iterowane
1
(1 2 3 4 5)
S1, S2 (obie po raz pierws zy)
2
(1 2 3 6 7)
S3, S4 (obie po raz pierws zy)
3
(1 2 3 4 2 3 4 5)
S1 (po raz drug i)
4
(1 2 3 6 3 4 5)
S2 (po raz drug i)
5
(1 2 3 4 2 3 6 7)
S3 (po raz drug i)
6
(1 2 3 6 3 6 7)
S4 (po raz drug i)
Metoda ma dwa główne ograniczenia. Pierwsze dotyczy wykonywalności ścieżek. Liczba obiegów jednej pętli może zależeć od innej. W przypadku problemów z uzyskaniem określonej liczby przebiegów pętli należy posłużyć się drzewem zagnieżdżeń w inny sposób. W każdym jego węźle należy zdefiniować licznik zliczający, ile razy program wszedł w dany cykl prosty. Test kończymy w momencie, gdy w każdym węźle licznik osiągnie wartość n określoną wzorem zdefiniowanym powyżej. Drugie ograniczenie dotyczy założenia o liniowości obliczeń. Można ten problem rozwiązać tak, że testowaniu podlegać będzie liniowa część obliczeń. W praktyce jednak okazuje się że np. większość obliczeń sporej części oprogramowania do zastosowań inżynierskich opiera się na aproksymacji liniowej [123].
9.9.4. Przykład Rozważmy ponownie program SortBąbelkowe z listingu 2.1. Dla wygody jest on przedstawiony ponownie na listingu 9.6. Załóżmy, że chcemy go przetestować metodą testowania pętli w wersji Beizera.
1 function SortBąbelkowe(int T[]) 2 n=liczba_elementów_T 3 do 4 5 6
for (i=0; iT[i+1]) then zamień miejscami T[i] z T[i+1]
7 8
end if end for
9 n=n-1 10 while (n>1) 11 return T 12 end Listing 9.6. Powtórzony kod programu SortBąbelkowe Zanim zaczniemy proces konstrukcji przypadków testowych, przeanalizujmy kod programu pod kątem występujących w nim pętli. Po pierwsze, pętla do-while zawsze wykona się co najmniej raz. Po drugie, zauważmy, że liczba przebiegów wewnętrznej pętli for zależy od zmiennej n, która zmniejsza się o 1 w każdym przebiegu do-while. Taka zależność pętli wewnętrznej od zewnętrznej jest bardzo częsta. Oznacza to, że nie da się obu tych pętli traktować niezależnie i prawdopodobnie niektóre wymagania będą nieosiągalne. Po trzecie, pętla for charakteryzuje się ustaloną z góry liczbą przebiegów, zatem nie możemy manipulować maksymalną czy minimalną liczbą jej wykonań. Liczba ta zależy wprost od wartości zmiennej n ustalanej w pętli zewnętrznej. Załóżmy ponadto, że maksymalny rozmiar tablicy T to MAXINT elementów. Krok 1. Określenie przedmiotu i elementów testów. Elementem testów jest program SortBębelkowe.
ET1: SortBąbelkowe. Krok 2. Wyprowadzenie warunków testowych. Warunkami testowymi są pętle, zatem mamy następujące dwa warunki testowe: WT1: pętla do-while w liniach 3.–10. (dla ET1); WT2: pętla for w liniach 4.–8. (dla ET1). Krok 3. Wyprowadzenie elementów pokrycia. Stosujemy metodologię Beizera. Wymóg ustawienia liczby iteracji na o jeden mniejszy niż wartość minimalna oraz o jeden większy niż maksymalna jest nieosiągalny dla obu pętli. Biorąc pod uwagę to oraz fakt zagnieżdżenia jednej pętli w drugiej, mamy następujące elementy pokrycia: EP1: wykonanie do-while 1 raz i EP2: wykonanie do-while 1 raz i EP3: wykonanie do-while 1 raz i EP4: wykonanie do-while 1 raz i EP5: wykonanie do-while 2 razy
for 0 razy (dla WT2); for 1 raz (dla WT2); for 2 razy (dla WT2); for typową liczbę razy, np. 45 (dla WT2); (dla WT1);
EP6: wykonanie do-while typową liczbę razy, np. 32 (dla WT1); EP7: wykonanie do-while MAXINT – 1 razy (dla WT1); EP8: wykonanie do-while MAXINT razy (dla WT1). EP1–EP4 odnoszą się do warunków z reguły 1. Beizera dla pętli zagnieżdżonych i dotyczą pętli wewnętrznej. EP5–EP8 to pozostałe warunki, dotyczące pętli zewnętrznej. Warunki opisane w punkcie 4. reguł Beizera dla pętli zagnieżdżonych mogą być spełnione tylko w przypadku wykonania obu pętli dokładnie raz, maksymalną liczbę razy lub o jeden mniej niż maksymalną liczbę razy. Przypadki te uwzględnione są w elementach pokrycia EP2, EP7 i EP8. Choć EP7 i EP8 nie nakładają wprost tych warunków dla pętli for, ze względu na strukturę pętli opisaną powyżej wymuszenie wykonania pętli while n razy spowoduje w pierwszym jej przebiegu wykonanie takiej samej liczby iteracji w pętli for. EP3 i EP4 są nieosiągalne, ponieważ pętla for zawsze wykona się co najwyżej tyle razy, ile pętla while.
Krok 4. Zdefiniowanie przypadków testowych. Dobór wartości wejściowych w przypadku naszych pętli jest prosty: rozmiar tablicy T narzuca wprost liczbę wykonań pętli while. Należy jednak pamiętać, że nie zawsze da się w prosty sposób wymusić określoną liczbę wykonań każdej z pętli występujących w programie. Przypadki testowe spełniające kryterium pokrycia pętli są podane w tabeli 9.28. Tabela 9.28. Przypadki testowe dla pokrycia pętli w programie SortBąbelkowe
Oczekiwane wyjście
Liczba wykonań while
Liczba wykonań for
Pokryte EP
PT1 T = [5]
[5]
1
0
EP1
PT2 T = [1, 2]
[1, 2]
1
1
EP2
PT3 T = [3, 2, 2]
[2, 2, 3]
2
2, 1
EP5
Id
wejście
PT4
T = [0, 1, 2, … , 32]
[0, 1 ,… , 32]
32
32, 31, … , 1 EP6
PT5
T = [1, … , MAXINT]
[1, … , MAXINT]
MAXINT – 1
MAXINT – 1, … , 1
EP7
PT6
T = [0, … , MAXINT]
błąd (zakres T)
–
–
EP8
Element EP8 może nie zostać pokryty w sensie wymuszenia określonej liczby przebiegów pętli, jeśli np. program zgłosi błąd przekroczenia rozmiaru tablicy. Awaria jest jednak również jakąś odpowiedzią programu. Jako testerzy chcieliśmy wymusić wykonanie MAXINT razy pętli while, co skutkowało błędem wykonania. W tym sensie możemy uznać EP8 za pokryty.
9.10. Liniowa sekwencja kodu i skok (LSKiS) 9.10.1. Opis metody
Liniowa sekwencja kodu i skok (ang. Linear Code Sequence And Jump, LCSAJ) jest białoskrzynkową metodą wymagającą pokrycia ścieżek związanych ze skokiem sterowania. Można ją zaliczyć do rodziny metod testowania ścieżek opisanych w punktach 9.10.3–9.12.6. Tak jak metoda pokrycia pętli, LSKiS jest próbą zdefiniowania sensownego kryterium pokrycia w sytuacji występowania pętli stwarzających potencjalnie nieskończoną liczbę możliwych ścieżek w programie. Najważniejsze pojęcie w metodzie LSKiS to skok. Jest to sterowanie, które przechodzi z linii x do linii y kodu, przy czym y ≠ x + 1, to znaczy linie te nie następują bezpośrednio po sobie. Skok może nastąpić zarówno w przód (np. jeśli warunek pętli while nie jest spełniony, to przeskakujemy całą pętlę do pierwszej linii po końcu struktury while), jak i w tył (np. po zakończeniu wykonywania pętli wracamy z powrotem do jej początku, aby sprawdzić warunek pętli).
Rysunek 9.5. Symboliczna reprezentacja LSKiS Liniowa sekwencja kodu i skok to ścieżka, w której budowie można wyróżnić następujące trzy elementy (patrz rys. 9.5): początek – początek modułu lub miejsce, do którego może nastąpić skok; koniec – koniec modułu lub miejsce, z którego może nastąpić skok; cel – miejsce, do którego następuje skok. Wszystkie trzy elementy to linie kodu. Powszechnie przyjętą konwencją jest odwoływanie się do nich za pomocą numerów linii kodu. Należy tylko pamiętać, aby w jednej linii kodu znajdowała się jedna instrukcja. Dzięki stosowaniu tej konwencji łatwo jest określić miejsca, w których następuje skok. Hipoteza błędu w technice LSKiS polega na tym, że błędy mogą powstawać podczas skoków, czyli w wyniku zaburzeń sekwencyjnego wykonania kodu. Warunkami testowymi i jednocześnie elementami pokrycia są trójki liczb (p, k, s) wyznaczające poszczególne ścieżki LSKiS, gdzie p, k, s oznaczają odpowiednio numery linii: początkowej, końcowej i docelowej po skoku. liniowa sekwencja kodu i skok, LSKiS (ang. Linear Code Sequence and Jump, LCSAJ) – sekwencja instrukcji określana przez trzy miejsca w kodzie, zwyczajowo identyfikowane numerami linii kodu źródłowego: rozpoczęcie liniowej sekwencji wykonywanych instrukcji, koniec sekwencji liniowej i docelowa linia, do której wykonywanie programu jest przekazywane po zakończeniu liniowej sekwencji LSKiS subsumuje kryterium pokrycia gałęzi oraz decyzji12. Metoda LSKiS jest krytykowana przede wszystkim ze względu na następujące kwestie: w praktyce wiele elementów pokrycia jest nieosiągalnych bez zmiany inicjalizacji zmiennych lub wymuszenia sterowania przez zmianę wartości zmiennych podczas działania programu przy użyciu debugera13; niewielkie zmiany w kodzie mogą radykalnie zmienić postać LSKiS, co sprawia, że metoda jest dosyć kosztowna w utrzymaniu;
typowe wykonanie programu zwykle pokrywa wiele LSKiS, co powoduje, że technika jest „przedobrzona”, tzn. kryteria pokrycia, określone w skomplikowany sposób, dają się pokryć bardzo niewielką liczbą testów. Wysiłek włożony w obliczenie tych LSKiS może więc stać pod znakiem zapytania. Ponadto, dla LSKiS postaci (a, b, c) jego spełnienie zagwarantuje również spełnienie każdego LSKiS postaci (x, b, c) gdzie a < x ≤ b. Niektóre LSKiS są więc nadmiarowe. pokrycie LSKiS (ang. coverage, LCSAJ) – odsetek LSKiS modułu, które zostały wykonane przez zestaw przypadków testowych; pokrycie 100% LSKiS oznacza pokrycie 100% decyzji W tabeli 9.29 podsumowano metodę testowania LSKiS. Tabela 9.29. Podsumowanie metody testowania LSKiS
W arunki testowe
Liniowe sekwencje kodu i skoku
Elementy pokrycia
Liniowe s ekwencje kodu i s koku
Kryterium pokrycia
Każda liniowa s ekwencja kodu i s kok powinna zos tać wykonana
Pokrycie
p=
liczba pokrytych LS KiS liczba ws zys tkich LS KiS
× 100%
9.10.2. Przykład Poniższy, niewielki przykład jest z jednej strony łatwo zrozumiały, z drugiej – nietrywialny. Funkcja z listingu 9.7 oblicza wartość xn dla zadanej liczby całkowitej x i liczby naturalnej n. Zapis n[0] oznacza najmniej znaczący bit liczby n. Jeśli jest on równy 0, to n jest parzysta, jeśli 1 – nieparzysta.
function Potęgowanie(int x, byte n)
1 wynik := 1 2 while (n != 0) do 3 4 5 6 7
if (n[0] != 0) then wynik := wynik * x n := n-1 endif x := x*x
8 n := n/2 9 end while 10 return wynik Listing 9.7. Szybkie potęgowanie Krok 1. Określenie przedmiotu i elementów testów. Elementem testów jest program Potęgowanie. ET1: Potęgowanie. Kroki 2 i 3. Wyprowadzenie warunków testowych i elementów pokrycia. Warunkami i elementami pokrycia są LSKiS. Aby je wyliczyć, musimy zbadać, w których liniach i do których linii programu może nastąpić skok. W linii 2., gdy warunek while nie będzie spełniony, nastąpi skok do linii 10. W linii 3., gdy warunek if nie będzie spełniony, nastąpi skok do 7. W linii 9. zawsze nastąpi skok do linii 2., na początek pętli while. W linii 10. następuje skok do linii 10. i wyjście z programu (instrukcje return traktuje się właśnie jako skok do tej samej linii i zakończenie działania modułu; symbolicznie skok reprezentujący koniec pracy oznacza się liczbą –1). A zatem miejsca do których może nastąpić skok to linie 2, 7 i 10. Te linie, razem z linią początkową 1., stanowią jedyne miejsca, z których może nastąpić start sekwencji LSKiS. Miejsca, z których może nastąpić skok, to linie 2, 3, 9 i 10. Te linie stanowią jedyne możliwe końce liniowych sekwencji. Możliwe skoki to 2 → 10, 3 → 7, 9 → 2 oraz 10 → –1. W tabeli 9.30 zawarto wszystkie możliwe LSKiS. Tabela 9.30. Możliwe LSKiS dla programu Potęgowanie
LSKiS id
Start
Koniec
Skok do
1
1
2
10
2
1
3
7
3
1
9
2
4
2
2
10
5
2
3
7
6
2
9
2
7
7
9
2
8
10
10
–1
Otrzymujemy
zatem
8
warunków
testowych
będących
jednocześnie
elementami pokrycia: WT1=EP1: LSKiS 1; WT4=EP4: LSKiS 4; WT7=EP7: LSKiS 7; WT2=EP2: LSKiS 2; WT5=EP5: LSKiS 5; WT8=EP8: LSKiS 8. WT3=EP3: LSKiS 3; WT6=EP6: LSKiS 6; Krok 4. Zdefiniowanie przypadków testowych. Chcemy pokryć każdą z LSKiS. Musimy zatem dobrać dane testowe tak, aby wymusić odpowiednie sterowanie w każdym przypadku. Aby pokryć EP1, pętla while nie może się wykonać ani razu. Można to wymusić, przyjmując na wejściu n = 3. W praktyce, w wielu przypadkach taki LSKiS będzie nieosiągalny, gdyż konstrukcje while często wymuszają co najmniej jedno wykonanie tej pętli. Zauważmy, że pokrycie EP1 automatycznie pokryje EP8. Tak naprawdę, EP8 będzie pokryte przez każdy przypadek testowy, gdyż program zawsze zakończy swoje działanie w linii 10. Spróbujmy pokryć kolejnym przypadkiem testowym więcej niż jeden element pokrycia. Rozważmy typowe wejście do programu, np. x = 3, n = 7. Spowoduje ono wykonanie następującej sekwencji instrukcji: 1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 10, 10, –1. Zauważmy, że sekwencja ta pokrywa następujące LSKiS: (1, 9, 2), (2, 9, 2), (7, 9, 2), (2, 3, 7), (2, 2, 10) oraz pokrytą wcześniej (10, 10, –1). Ilustruje to dobrze jeden
z zarzutów pod adresem techniki: skoro jeden, typowy przypadek testowy „załatwia” prawie wszystkie LSKiS, to po co marnować tyle czasu na ich obliczanie? Ponadto, LSKiS (2, 9, 2) oraz (7, 9, 2) są nadmiarowe (redundantne) wobec LSKiS (1, 9, 2). Została nam do pokrycia jeszcze jedna LSKiS: (1, 3, 7). Uzyskamy to, wchodząc do pętli while oraz nie wykonując instrukcji if. Warunkiem takiego działania programu jest to, aby n ≠ 0 oraz n[0] = 0. Wystarczy więc przyjąć jako n dowolną liczbę parzystą. Na przykład dla x = 3, n = 2 otrzymamy wykonanie 1, 2, 3, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 2, 10, 10, –1 Pokryje ono LSKiS (1, 3, 7) oraz pokryte wcześniej (2, 9, 2), (7, 9, 2), (2, 3, 7), (2, 2, 10) i (10, 10, –1). Otrzymujemy zatem zbiór przypadków testowych zebranych w tabeli 9.31. Pogrubioną czcionką zaznaczono elementy pokryte po raz pierwszy. Tabela 9.31. Przypadki testowe pokrywające LSKiS
Id
n x
Oczekiwany Pokryte LSKiS wynik
Pokryte EP
PT1 0 3 1
(1, 2, 10), (10, 10, –1)
EP1, EP8
PT2 7 3 2187
(1, 9, 2), (2, 9, 2), (7, 9, 2), (2, 3, 7), (2, 2, 10) i (10, 10, –1)
EP3, EP4, EP5, EP6, EP7, EP8
PT3 2 3 9
(1, 3, 7), (2, 9, 2), (7, 9, 2), (2, 3, 7), (2, 2, 10) i (10, 10, –1)
EP2, EP4, EP5, EP6, EP7, EP8
9.10.3. LSKiS a DD-ścieżki i bloki podstawowe Sylabus poziomu zaawansowanego ISTQB [79] twierdzi, że ścieżki LSKiS są równoważne DD-ścieżkom (patrz p. 5.4.1), co nie jest prawdą14. Po pierwsze, DDścieżki o długości większej od 1 nie mogą przechodzić przez instrukcje decyzyjne, co jest dozwolone w przypadku ścieżek LSKiS. Po drugie, ścieżka LSKiS zawsze kończy się skokiem, a DD-ścieżka kończy się wierzchołkiem, z którego dopiero może nastąpić skok. W przykładzie z punktu 9.10.2 mamy np. ścieżkę LSKiS (1, 2, 3, 7), w której liniową sekwencją jest 1, 2, 3, po czym następuje skok do linii 7. Zauważmy, że ta
ścieżka zawiera instrukcję decyzyjną (linia nr 2)! Nie może więc być DD-ścieżką. DD-ścieżkami w kodzie z przykładu są ścieżki: (1), (10) odpowiadające wierzchołkom początkowemu i końcowemu; (2), (3), (7), (9) odpowiadające wierzchołkom o stopniu wchodzącym lub wychodzącym większym niż 1; (4, 5, 6), (8) odpowiadające łańcuchom maksymalnym. DD-ścieżki zostały zdefiniowane w celu upraszczania grafów przepływu sterowania tak, aby jeden wierzchołek CFG odpowiadał jednej DD-ścieżce. Takie CFG nazywa się DD-grafami. Z grubsza rzecz ujmując, DD-ścieżki odpowiadają blokom podstawowym instrukcji kodu. „Z grubsza” – bowiem definicja DD-ścieżki „rozdrabnia” niektóre bloki podstawowe, wyróżniając osobny przypadek DDścieżki dla instrukcji z więcej niż jednym punktem wejścia lub wyjścia. W naszym przykładzie może być to linia 7. Stanowi ona razem z następującymi po niej liniami 8. i 9. blok podstawowy, ale (7, 8, 9) nie jest DD-ścieżką, bo – patrząc na kod, jak na CFG – stopień wejścia wierzchołka odpowiadającego linii 7. nie jest równy 1. Można nieco „ulepszyć” definicję DD-ścieżki, jeszcze bardziej minimalizując15 liczbę wierzchołków przez dopuszczenie, aby łańcuch maksymalny mógł zaczynać się od wierzchołka o dowolnym stopniu wchodzącym (lecz mającym wciąż stopień wychodzący = 1), a wierzchołkiem końcowym mógł być wierzchołek końcowy CFG. W takiej sytuacji DD-ścieżki będą równoważne blokom podstawowym.
Rysunek 9.6. DD-ścieżki a bloki podstawowe Wywód ten jest zilustrowany na rysunku 9.6. Graf (a) to CFG dla kodu z listingu 9.7, w którym wierzchołek odpowiada jednej linii kodu. DD-graf (b) pokazuje, jak koncepcja DD-ścieżek pozwala łączyć niektóre wierzchołki ze sobą. Nowymi wierzchołkami są szare prostokąty. Każdy z nich odpowiada jednej DD-ścieżce. Możemy jednak jeszcze bardziej uprościć graf, tworząc bloki podstawowe przez łączenie niektórych DD-ścieżek według reguł opisanych w poprzednim akapicie. Graf ten ma o jeden wierzchołek mniej niż graf z rysunku (b), dzięki połączeniu wierzchołków DD 6 i DD 7 w jeden.
9.11. Testowanie ścieżek pierwszych
9.11.1. Opis metody Ścieżką prostą w grafie przepływu sterowania G = (V, E, v0, VT) nazywamy ścieżkę (v1, v2, …, vn), vi ∈ V dla i = 1, 2, …, n, w której każdy wierzchołek pojawia się co najwyżej raz, przy czym dopuszczamy sytuację, w której ostatni wierzchołek jest równy pierwszemu: vn = v1 (mamy wtedy do czynienia z tzw. cyklem prostym). Koncepcja testowania ścieżek prostych prowadzi do zbyt dużych i redundantnych suit testowych. Aby
temu zaradzić, a
jednocześnie utrzymać kryterium
wymagające pokrycia ścieżek stanowiących „podstawowe bloki budulcowe” wszystkich możliwych sekwencji przebiegu sterowania, wprowadźmy pojęcie ścieżki pierwszej (ang. prime path). Nazwa metody ma pokazywać analogię ścieżek CFG do arytmetyki: tak, jak każda liczba naturalna daje się wyrazić jako iloczyn liczb pierwszych, tak każda ścieżka testowa daje się wyrazić jako złożenie ścieżek pierwszych. Ścieżka pierwsza to ścieżka prosta niebędąca właściwą podścieżką16 innej ścieżki prostej. Jeśli ścieżkę p możemy rozszerzyć, dodając na jej początku lub końcu wierzchołek, który nie występował w p i nie jest wierzchołkiem początkowym p, oznacza to, że p nie jest ścieżką pierwszą. podścieżka (ang. subpath) – 1) ciąg wykonywalnych instrukcji wewnątrz modułu; 2) ścieżka będąca częścią innej, większej ścieżki Rozważmy dla przykładu graf przepływu sterowania z rysunku 9.7. Ścieżka (1, 2, 5, 6) jest ścieżką pierwszą. Łatwo zauważyć, że każda ścieżka testowa, nieprzechodząca po żadnym wierzchołku więcej niż raz, jest ścieżką pierwszą. Innym przykładem ścieżki pierwszej może być (1, 3, 4). Rozszerzenie jej do (1, 3, 4, 3) spowoduje powtórzenie się wierzchołka 3. Ścieżka (4, 3) nie jest pierwsza, gdyż można ją rozszerzyć np. do ścieżki (4, 3, 4), (4, 3, 5, 1, 2) lub (4, 3, 5, 6). W szczególności pierwsza z nich jest ścieżką pierwszą, bo jest cyklem prostym.
Rysunek 9.7. Przykładowy CFG Technika ścieżek pierwszych wymaga, aby każda ścieżka pierwsza w CFG była pokryta przynajmniej przez jeden przypadek testowy. Warunkami testowymi są
elementy składowe grafu przepływu sterowania. Elementami pokrycia są wszystkie ścieżki pierwsze występujące w tym CFG. Szczególnym przypadkiem ścieżki pierwszej jest ścieżka cykliczna (ang. round trip), zwana też cyklem prostym. Jest to ścieżka pierwsza o niezerowej długości rozpoczynająca i kończąca się w tym samym wierzchołku. Z pojęciem ścieżki cyklicznej są związane dwa kryteria pokrycia. Podamy tu tylko ich definicje, ponieważ z praktycznego punktu widzenia lepiej jest stosować kryterium pokrycia ścieżek pierwszych, które je subsumuje. Kryterium prostego pokrycia ścieżek cyklicznych (ang. Simple Round Trip Coverage, SRTC) – wymaga, aby zbiór przypadków testowych zawierał przynajmniej jedną ścieżkę cykliczną dla każdego osiągalnego wierzchołka CFG, który leży na jakiejś ścieżce cyklicznej. Kryterium całkowitego pokrycia ścieżek cyklicznych (ang. Complete Round Trip Coverage, CRTC) – wymaga, aby zbiór przypadków testowych zawierał wszystkie ścieżki cykliczne dla każdego osiągalnego wierzchołka CFG, który leży na jakiejś ścieżce cyklicznej. W tabeli 9.32 podsumowano metodę testowania ścieżek pierwszych. Tabela 9.32. Podsumowanie metody testowania ścieżek pierwszych
W arunki testowe
W ierzchołki CFG
Elementy pokrycia
S ekwencje wierzchołków CFG s tanowiące ś cieżki pierws ze
Kryterium pokrycia
Każda ś cieżka pierws za powinna zos tać wykonana przynajmniej raz
Pokrycie
p=
liczba pokrytych ś cieżek pierws zych liczba ws zys tkich ś cieżek pierws zych
× 100%
9.11.2. Algorytm wyznaczania ścieżek pierwszych Istnieje prosta systematyczna metoda wyznaczania ścieżek pierwszych. Jest formalnie opisana na listingu 9.8.
WyznaczŚcieżkiPierwsze(CFG G = (V, E, v0, VT)) 1 P0 := wszystkie ścieżki o długości 0 (czyli wierzchołki CFG) 2
:= wierzchołki końcowe z P0
3 i := 0 4 while ( 5 6 7
) do
Pi+1 := wszystkie rozszerzenia Pi będące ścieżkami prostymi := nierozszerzalne ścieżki z Pi+1 i := i+1
8 PPcandidates := 9 PrimePaths:=PPcandidates \ podścieżki innych ścieżek z PPcandidates 10 return PrimePaths Listing 9.8. Wyznaczanie ścieżek pierwszych Działanie algorytmu opiszemy na przykładzie. Rozważmy ponownie CFG z rysunku 9.7. Wyznaczymy wszystkie ścieżki pierwsze dla tego grafu. Rozpoczynamy od znalezienia zbioru P0 wszystkich ścieżek prostych o długości 0. Są to oczywiście wszystkie wierzchołki CFG: 1) (1) 3) (3) 5) (5) 2) (2) 4) (4) 6) (6)* Gwiazdką oznaczamy elementy należące do zbiorów , czyli ścieżki, których nie można już rozszerzyć ze względu na to, że tworzą cykl prosty lub kończą się w wierzchołku końcowym CFG. Jedynie wierzchołek 6 jest ścieżką nierozszerzalną. W kolejnym kroku rozszerzamy na wszystkie możliwe sposoby ścieżki ze zbioru P0: 7) (1, 2) 11) (3, 5) 8) (1, 3) 12) (4, 3) 9) (2, 5) 13) (5, 1) 10) (3, 4) 14) (5, 6)* Elementy (7) – (13) wciąż da się rozszerzyć, zatem obliczamy zbiór P2:
15) (1, 2, 5) 18) (2, 5, 1) 21) (3, 5, 1) 24) (4, 3, 5) 16) (1, 3, 4)* 19) (2, 5, 6)* 22) (3, 5, 6)* 25) (5, 1, 2) 17) (1, 3, 5) 20) (3, 4, 3)* 23) (4, 3, 4)* 26) (5, 1, 3) Ścieżki (16) nie da się rozszerzyć do ścieżki prostej, bo powtórzyłby się element 3. Elementy (20) i (23) są cyklami prostymi, a (19) i (22) kończą się w wierzchołku końcowym CFG. Pozostałe 7 ścieżek jest rozszerzalnych, obliczamy więc zbiór P3: 27) (1, 2, 5, 1)* 31) (2, 5, 1, 2)* 35) (4, 3, 5, 1) 39) (5, 1, 3, 5)* 28) (1, 2, 5, 6)* 32) (2, 5, 1, 3)* 36) (4, 3, 5, 6)* 29) (1, 3, 5, 1)* 33) (3, 5, 1, 2)* 37) (5, 1, 2, 5)* 30) (1, 3, 5, 6)* 34) (3, 5, 1, 3)* 38) (5, 1, 3, 4)* Jedynie ścieżka (35) jest rozszerzalna, obliczamy więc P4: 40) (4, 3, 5, 1,2)* Żadnej ścieżki z P4 nie da się już rozszerzyć, więc kończymy pętlę 4.–7. algorytmu. Zbiór potencjalnych kandydatów na ścieżki pierwsze, PPcandidates, składa się wyłącznie z nierozszerzalnych ścieżek spośród 40 wcześniej wymienionych. Odwołując się do ich numeracji, mamy PPcandidates = {6, 14, 16, 19, 22, 23, 27, 28, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40} Ostatnim elementem jest usunięcie podścieżek innych ścieżek prostych ze zbioru PPcandidates: (4, 3, 5, 1, 2) jako podścieżkę zawiera (3, 5, 1, 2); (1, 2, 5, 6) jako podścieżki zawiera (2, 5, 6), (5, 6) oraz (6); (1, 3, 5, 6) jako podścieżkę zawiera (3, 5, 6); (5, 1, 3, 4) jako podścieżkę zawiera (1, 3, 4). Usuwając z PPcandidates podścieżki innych ścieżek, otrzymujemy ostatecznie zbiór złożony z 14 ścieżek pierwszych: 1) (4, 3, 5, 1, 2) 5) (1, 3, 5, 6) 9) (4, 3, 5, 6) 13) (3, 4, 3) 2) (1, 2, 5, 1) 6) (2, 5, 1, 2) 10) (5, 1, 2, 5) 14) (4, 3, 4) 3) (1, 2, 5, 6) 7) (2, 5, 1, 3) 11) (5, 1, 3, 4) 4) (1, 3, 5, 1) 8) (3, 5, 1, 3) 12) (5, 1, 3, 5)
Liczba ścieżek pierwszych jest zawsze skończona, gdyż maksymalna długość ścieżki pierwszej jest równa liczbie (skończonej) wierzchołków grafu przepływu sterowania. Algorytm wyznaczania ścieżek pierwszych jest poprawny, tzn. w szczególności ma więc własność stopu. Kryterium pokrycia ścieżek pierwszych subsumuje kryterium pokrycia instrukcji, gałęzi, decyzji, LSKiS oraz pętli (w wersji wymagającej wejścia do pętli 0 oraz więcej niż 0 razy). Jest ono mocnym kryterium, aczkolwiek często zdarza się, że wymaga pokrycia ścieżek nieosiągalnych. Można wtedy je osłabić lub zastosować metody z podrozdziału 9.14.
9.11.3. Przykład Przetestujmy za pomocą kryterium pokrycia ścieżek pierwszych program sprawdzający, czy podana na wejściu nieujemna liczba jest, nomen omen, pierwsza. Na listingu 9.9 przedstawiono testowaną funkcję.
function CzyPierwsza(int n) 1 if (n >) jest zamieniany na każdy inny; ponadto każde wyrażenie jest
zamieniane na jego lewy operand. Na przykład, dla instrukcji x=ab, x=a>>>b, x=a. Zamiana bitowego operatora logicznego – każdy operator bitowy (bitowe and &, bitowe or |, xor ^) jest zamieniany na każdy inny; ponadto każde wyrażenie zamieniane jest na jego lewy oraz prawy operand. Na przykład, dla instrukcji p=x&y możliwe mutanty to: p=x|y, p=x^y, p=x, p=y. Zamiana operatora przypisania – każdy operator przypisania (+=, –=, *=, /=, %=, &=, |=, ^=, =, >>>=) jest zamieniany na każdy inny. Na przykład, dla instrukcji x+=1 możliwe mutanty to: x-=1, x*=1, x/=1, x%=1, x&=1, x|=1, x^=1, x=1, x>>>=1. Wstawienie operatora unarnego – każdy operator unarny (arytmetyczne +, –, warunkowy !, logiczny ~) jest wstawiany przed każde wyrażenie odpowiedniego typu. Na przykład, dla instrukcji x=y+1 możliwe mutanty to: x=+y+1, x=-y+1, x=y++1, x=y+-1. Usunięcie operatora unarnego – każdy operator unarny jest usuwany. Na przykład, dla instrukcji if (a>-b+c) możliwe mutanty to: if ! (a>-b+c), if (a>b+c). Zamiana zmiennej – każda referencja zmiennej jest zamieniana na inną zmienną tego samego typu widzialną w bieżącym zakresie. Na przykład, zakładając że w bieżącym zakresie widzialne są tylko zmienne x, y, z i są one tego samego typu, dla instrukcji x=y+z możliwe mutanty to: y=y+z, z=y+z, x=x+z, x=z+z, x=y+x, x=y+y. Bomba – każda instrukcja jest zamieniana na instrukcję Bomb(). Operatory mutacyjne dla mutacji integracji Zamiana zmiennej w wywołaniu metody – każdy parametr wywołania metody jest zamieniany przez każdy inny o zgodnym typie, widoczny w zakresie metody. Wstawienie operatora unarnego – każde wyrażenie w wywołaniu metody jest modyfikowane przez wstawienie wszystkich możliwych operatorów unarnych przed i za tym wyrażeniem. Przykładem takich operatorów mogą być ++ i –– używane w Javie czy C++.
Zamiana parametrów – każdy parametr wywołania metody jest zamieniany na każdy inny o zgodnym typie. Na przykład, wywołanie power(double x, double y)
może
być
zmutowane
na
power(double y, double x). Usunięcie wywołania metody – każde
wywołanie
metody
jest
usuwane. Jeśli metoda zwraca wartość i jest użyta w wyrażeniu, to wywołanie metody jest zamieniane przez odpowiednią stałą wartość. Jeśli język programowania ma zdefiniowane domyślne wartości typów prostych, to należy je użyć. Jeśli metoda zwraca obiekt, to wywołanie powinno być zamienione na takie, które wywołuje konstruktor właściwej klasy. Modyfikacja zwracanej wartości – każde wyrażenie w każdej instrukcji return w metodzie jest modyfikowane przy użyciu zamiany operatora arytmetycznego lub wstawienia operatora unarnego. Operatory mutacyjne dla mutacji programów zorientowanych obiektowo Zmiana operatora dostępu – np. każdy z operatorów dostępu: private, package, protected, public jest zamieniany na każdy inny. Usunięcie ukrycia zmiennej – każda deklaracja nadpisująca lub ukrywająca zmienną jest usuwana, przez co referencja do zmiennej dotyczy zmiennej zdefiniowanej w nadklasie lub podklasie. Wstawienie ukrycia zmiennej – deklaracja jest dodawana, aby ukryć deklarację każdej zmiennej zdefiniowanej w podklasie. Usunięcie metody nadpisującej – każda deklaracja metody nadpisującej jest usuwana, przez co referencja do metody będzie dotyczyć metody w nadklasie. Zmiana nazwy metody nadpisującej – zmieniana jest nazwa metody w nadklasie, która to metoda jest nadpisana w podklasie, przez co nadpisanie nie wpływa na metodę nadklasy. Usunięcie słowa kluczowego „super” – każde wystąpienie słowa „super” jest usuwane, przez co referencja będzie dotyczyć lokalnej wersji obiektu, a nie wersji w podklasie. Usunięcie konstruktora nadklasy – każde wywołanie do konstruktora „super” jest usuwane, przez co użyty będzie domyślny konstruktor nadklasy (lub podklasy). Aby zabić takiego mutanta, należy znaleźć test,
w którym domyślny konstruktor nadklasy tworzy nieprawidłowy stan początkowy. Zmiana typu aktualnego – aktualny typ nowego obiektu jest zmieniany w instrukcji new(), przez co referencja obiektu wskazuje na obiekt typu innego od oryginalnego typu aktualnego; nowy typ aktualny musi być w tej samej rodzinie typów co oryginalny typ aktualny. Zmiana typu deklarowanego – deklarowany typ każdego nowego obiektu jest zmieniany w deklaracji. Zmiana typu parametru – deklarowany typ każdego parametru obiektu jest zmieniany w deklaracji. Zmiana typu referencji – obiekty z prawej strony instrukcji przypisania są zmieniane tak, by wskazywać obiekty zgodnego typu, np. jeśli w Javie Integer jest przypisane do referencji typu Object, mutacja może zmienić go na String, ponieważ zarówno Integer, jak i String dziedziczą po klasie Object. Zmiana przeciążonej metody – dla każdej pary metod o tej samej nazwie ich ciała są zamieniane. Usunięcie przeciążonej metody – każda deklaracja przeciążonej metody jest usuwana (za każdym razem jedna), co sprawdza, czy każda przeładowana metoda jest wywoływana przynajmniej raz. Zmiana kolejności parametrów wywołania – kolejność argumentów w wywołaniu metody jest zmieniana na kolejność innej metody przeciążonej (jeśli istnieje). Zmiana liczby argumentów – liczba argumentów wywołania zmieniana jest na liczbę argumentów wywołania innej przeciązonej metody (jeśli istnieje). Usunięcie słowa kluczowego „this” – każde wystąpienie „this” jest usuwane, przez co sprawdzane jest, czy zmienne składowe metody są używane poprawnie. Zmiana modyfikatora „static” – każde wystąpienie „static” jest usuwane oraz dodawane do instancji zmiennych. Usunięcie inicjalizacji zmiennej składowej jest usuwana.
–
każda
inicjalizacja
zmiennej
Usunięcie domyślnego konstruktora – każda deklaracja domyślnego konstruktora (bez parametrów) jest usuwana.
Operatory mutacyjne dla mutacji specyfikacji wyrażanych maszynami stanowymi Zmiana przejścia – przejście ze stanu X do Y jest zamieniane na przejście z X do Z ≠ Y. Zmiana zdarzenia – każde zdarzenie jest zamieniane na inne. Zmiana akcji – każda akcja jest zamieniana na inną. Mutacja warunków – do każdego warunku przy zdarzeniu są stosowane odpowiednie operatory mutacji dla kodu źródłowego. Operatory mutacyjne dla mutacji opartych na gramatykach formalnych Zamiana symbolu nieterminalnego – każdy symbol nieterminalny21 w każdej produkcji jest zamieniany na inny nieterminal, np. A → Bc może być zamienione na A → Ac. Zamiana symbolu terminalnego – każdy symbol terminalny w każdej produkcji jest zamieniany na inny terminal, np. A → Bc może być zamienione na A → Bd. Usunięcie terminala/nieterminala – każdy terminal oraz nieterminal jest usuwany z każdej produkcji, np. A → Bc może być zamienione na A → B lub A → c. Powielenie terminala/nieterminala – każdy terminal oraz nieterminal jest duplikowany (np. dla produkcji A → Bc mutantami mogą być produkcje A → BBc oraz A → Bcc).
Rysunek 9.15. Hierarchia subsumpcji dla białoskrzynkowych kryteriów pokrycia
9.16. Subsumpcja kryteriów Na rysunku 9.15 przedstawiono relacje subsumpcji między opisanymi białoskrzynkowymi kryteriami pokrycia. Linie przerywane oznaczają, że dana subsumpcja jest warunkowa, tzn. zachodzi tylko w pewnych specjalnych okolicznościach. Kryterium MUMCUT subsumuje kryterium MC/DC (pokrycia warunków znaczących), jeśli dla każdego predykatu w postaci IDNF istnieje UN-para testów dla każdej zmiennej w tym predykacie. Kryterium LCSAJ (pokrycia liniowych sekwencji kodu i skoku) subsumuje MC/DC, jeśli są spełnione następujące trzy warunki [129]: żaden predykat w kodzie nie zawiera silnie związanych warunków (ang. strongly coupled conditions); każda decyzja w kodzie tworzy część struktury rozgałęzienia (ang. branching construct); żadna decyzja w kodzie nie zawiera mieszaniny operatorów ∧ oraz ∨, lecz tylko jeden z tych typów (z dopuszczeniem negacji pojedynczych klauzul). Kryterium AUC (pokrycia wszystkich użyć) subsumuje kryterium pokrycia decyzji, jeśli są spełnione następujące trzy warunki: każde użycie jest poprzedzone definicją; każda definicja osiąga przynajmniej jedno użycie; w wierzchołku o wielu krawędziach wychodzących na każdej krawędzi wychodzącej jest przynajmniej jedna użyta zmienna i ta sama zmienna jest użyta na wszystkich pozostałych krawędziach wychodzących.
1 W praktyce, ze względu na właściwość bloków podstawowych CFG, kryteria pokrycia dla metod białoskrzynkowych odnosi się zwykle do elementów modelu, a nie pojedynczych instrukcji czy innych fragmentów kodu. 2 Predykat to zdanie logiczne, które może przyjąć wartość logiczną prawdy lub fałszu. 3 Zdanie to jest prawdą tylko dla elementów testowych z jednym punktem wejścia. W praktyce większość testowanych obiektów ma tę właściwość. 4 Pierwiastkiem (inaczej: miejscem zerowym) funkcji f(x) nazywamy taką wartość x*, dla której f(x*) = 0. 5 Gdyby kompilator nie stosował semantyki short-circuit, a tablica miałaby n elementów indeksowanych od 0 do n – 1, to w przypadku, gdy np. i = n warunek T [i] ≠ 0 i tak byłby sprawdzany. Mogłoby to skutkować błędem, bo następowałoby odwołanie do wartości spoza tablicy. 6 Tak jak w poprzednich kryteriach, formalnie wymagamy, aby zmodyfikowane kryterium MC/DC spełniało kryterium pokrycia instrukcji. 7 Tablica prawdy podaje wartość logiczną predykatu (decyzji) dla wszystkich możliwych wartości logicznych klauzul (warunków). Dla n warunków tablica ma 2n wierszy. 8 Choć w większości języków programowania nie ma wprost zdefiniowanego operatora równoważności ⇔, można go uzyskać, korzystając z następujących praw logiki: p ⇔ q ≡ (p ⇒ q) ∧ (q ⇒ p) ≡ (q ∨ ~p) ∧ (p ∨ ~q) ≡ (p ∧ q) ∨ (~p ∧ ~q) 9 W przypadku pętli typu do-while, gdzie warunek pętli jest sprawdzany po jej wykonaniu, zawsze wykona się ona co najmniej raz, dlatego w tym przypadku kryterium może mówić o wykonaniu pętli raz oraz dokładnie dwa razy. 10
Liczba ta wyraża minimalną liczbę hiperpłaszczyzn leżących na hiperpłaszczyźnie reprezentującej tzw. wzorzec pętli H. Jeśli ścieżka p → q → r zawiera pętlę q → q i funkcje reprezentujące obliczenia ścieżek p, q, r oznaczymy odpowiednio przez f pq, h, f qr , to n-krotne wykonanie pętli na tej ścieżce jest reprezentowane przez złożenie funkcji f n = f pq ◦ hn ◦ f qr . Kładąc f 0 = f pq ◦ f qr oraz f n = f n–1◦ H, mamy Założyliśmy, że obliczenia
są
Funkcję H nazywamy wzorcem pętli. liniowe, dlatego wszystkie funkcje są
przekształceniami liniowymi. W szczególności więc funkcja
odwrotna do
f qr , jest dobrze zdefiniowana. 11 Liczba ta jest analogiczna do liczby punktów wyznaczających jednoznacznie hiperpłaszczyznę w metodzie punktów ON-OFF. 12 Subsumpcja istnieje także dla programów złożonych z jednego bloku bazowego, bez rozgałęzień. Jest tak dlatego, że LSKiS wymaga w szczególności pokrycia ścieżek zaczynających się na początku modułu oraz kończących w jego punkcie wyjścia. 13 Niektórzy autorzy uważają, że metoda sztucznego wymuszania sterowania powinna być stosowana w testowaniu, w szczególności po to, aby pokryć nieosiągalne elementy pokrycia. Jednak tester testuje funkcjonalnie program w taki sam sposób, w jaki użytkownik będzie go używał – jeśli użytkownik nie może sztucznie zmienić w środku działania aplikacji jakiejś zmiennej, to po co wymuszać takie warunki podczas testowania funkcjonalnego? 14 LSKiS są natomiast równoważne pojęciu „JJ-ścieżki” (ang. jump-to-jump path) [129]. Prawdopodobnie autorzy sylabusa mieli na myśli JJ-ścieżki, a nie DDścieżki. 15 Wielu autorów, podając definicję DD-ścieżki, popełnia błąd, nie uwzględniając tych zasad, choć tak naprawdę implicite je zakładając, bo cała idea DD-ścieżek polega na tym, że powinny odpowiadać blokom podstawowym, aby zminimalizować liczbę wierzchołków CFG w jak największym stopniu. 16 Dla ścieżki (v1, v2, …, vn) jej podścieżkami są wszystkie ścieżki postaci (vi, vi+1, …, vj), 1 ≤ i ≤ j ≤ n. Podścieżka (vi, vi+1, …, vj) jest właściwa, jeśli i > 1 lub j < n. 17 W teorii języków formalnych wektory takie nazywa się wektorami Parikha. 18 Określenie „krawędź bazowa” pochodzi stąd, że krawędź ta tworzy kolumnowy wektor bazowy w macierzy zawierającej wektory ścieżek bazowych. 19 W języku Ada parametr formalny (zdefiniowany w sygnaturze funkcji) może przekazywać dane do parametru aktualnego (zmiennej, która występuje w fizycznym wywołaniu procedury). W takim przypadku do parametrów tych, opisanych słowem kluczowym „out” można się odwoływać tylko tak, aby były one po lewej stronie instrukcji podstawienia. 20 Sygnatura funkcji to opis typów parametrów jej wywołania oraz ich kolejności. 21 Symbole nieterminalne symbolicznie oznacza się zwykle wielkimi literami A, B, C, …, a terminale – małymi. W praktycznych zastosowaniach terminale pisze się w cudzysłowach, a nieterminale wielkimi literami bądź w nawiasach kwadratowych (patrz p. 8.14.2).
10. Techniki oparte na defektach i na doświadczeniu
W tym rozdziale omówimy techniki mniej sformalizowane niż w przypadku metod czarno- i białoskrzynkowych. Mimo ich mniejszego ustrukturalizowania są one wciąż pełnoprawnymi i wartościowymi technikami. Co więcej, niektóre z nich (np. testowanie eksploracyjne), w wykonaniu profesjonalnych, doświadczonych testerów sprawdzają się często o wiele lepiej niż techniki bardziej formalne, narzucające z góry określony sposób postępowania. Z technikami opartymi na defektach mamy do czynienia wtedy, gdy testujemy oprogramowanie, myśląc o konkretnych typach defektów. Zwykle modelem jest tutaj jakaś mniej lub bardziej szczegółowa lista możliwych defektów, które np. występowały w poprzednich projektach. Tworzenie testów polega na dostarczeniu takich danych wejściowych, aby wywołać awarię ujawniającą określony typ usterki. W technikach opartych na doświadczeniu zwykle tester ma wolną rękę, tzn. sam decyduje, co i w jaki sposób testować. Swoje decyzje podejmuje na podstawie własnej wiedzy, doświadczenie oraz obserwacji działania programu. technika (projektowania testów) oparta na defektach (ang. defect-based (test design) technique) – procedura projektowania i/lub wyboru przypadków testowych ukierunkowana na jeden lub więcej typów defektów, w której testy projektuje się na podstawie wiedzy o określonych typach defektów; patrz także: taksonomia defektów technika projektowania testów oparta na doświadczeniu (ang. experiencebased test design technique) – procedura projektowania i/lub wyboru przypadków testowych na podstawie doświadczenia, wiedzy i intuicji testera
typ usterki, kategoria usterki (defect type, defect category) – element systematyki usterek. Systematyka ta może być identyfikowana w zależności od różnych okoliczności, włączając w to, ale nie ograniczając się do: fazy lub czynności deweloperskiej, w której usterka została wytworzona (np. błąd w specyfikacji lub błąd kodowania), charakterystki usterki (np. tzw. błąd o jeden), pomyłki (np. niepoprawny operator relacji, błąd składni języka programowania lub niewłaściwe założenia), problemy wydajnościowe (np. zbyt długi czas wykonania, niedostateczna dostępność); patrz także punkt 27.2.2 – ODC (Orthogonal Defect Classification) W technikach opartych na defektach i doświadczeniu zwykle nie stosuje się kryteriów pokrycia, gdyż nie da się ich dobrze, precyzyjnie zdefiniować. W przypadku stosowania list kontrolnych można oczywiście przyjąć, że stopień pokrycia jest związany z odsetkiem pokrytych testami punktów tej listy, jednak zwykle nie da się ocenić w jakim stopniu pojedynczy test „pokrywa” określony typ defektu z listy.
10.1. Wstrzykiwanie błędów Technika wstrzykiwania błędów (ang. fault injection) polega na celowym wprowadzaniu do oprogramowania usterek i zwykle dotyczy testowania odporności. Po wprowadzeniu usterki badamy zachowanie programu i to, jak sobie z daną usterką poradzi. Metody te można podzielić ze względu na czas wstrzyknięcia usterki na trzy kategorie: wstrzyknięcie przed kompilacją [130] – polega na wstrzykiwaniu usterek do kodu źródłowego; formą tego typu techniki jest testowanie mutacyjne omówione w podrozdziale 9.15; wstrzyknięcie po kompilacji, ale przed uruchomieniem [131] – następuje po tym, jak kod źródłowy zostaje przetransformowany do kodu obiektowego lub bajtkodu i polega na modyfikacji kodu wynikowego; wstrzyknięcie w trakcie działania programu [132] – przeprowadzany zwykle niskopoziomowo przez wprowadzanie zmian w kodzie
maszynowym lub bajtkodzie rezydującym w pamięci. posiew błędów, posiew usterek (ang. error seeding, fault seeding) – proces celowego dodawania defektów do już istniejących w module lub systemie w celu monitorowania efektywności ich wykrywania i usuwania oraz szacowania defektów niewykrytych; posiew usterek jest zwykle częścią rozwoju oprogramowania i może być wykonywany na każdym poziomie testów [7] wstrzykiwanie błędów, wprowadzanie błędów (ang. fault injection, bebugging) – proces zamierzonego dodawania defektów do systemu w celu wykrycia, czy system może wykryć defekt i pracować mimo jego występowania; wstrzykiwanie błędów stara się imitować błędy, które mogą wystąpić w produkcji [133]; patrz także: tolerowanie usterek Wstrzykiwanie błędów wymaga posiadania zbioru usterek, które chcemy posiać w programie. Każda aplikacja ma różne tryby awarii (ang. failure modes), czyli sposoby, na które może objawić się jej nieprawidłowe działanie. Przykładowy model awarii może dzielić nieprawidłowe wykonanie programu na następujące cztery typy: zawieszenie (ang. hang) – następuje, gdy w wyniku wywołania metody nie otrzymujemy odpowiedzi; ten typ awarii może być symulowany za pomocą wstrzyknięcia w odpowiednie miejsce nieskończonej pętli; testowanie wymaga zdefiniowania limitu czasu, w którym musi nastąpić odpowiedź; po tym czasie możemy założyć, że komponent się zawiesił; anormalne zakończenie działania (ang. abend) – ta awaria powoduje rzucenie wyjątku w miejscu, gdzie istnieje kod, który go obsługuje; wstrzyknięta usterka może mieć postać kodu wywołującego wyjątek umieszczonego w miejscu, w którym wyjątek ten może być obsłużony; crash – podobnie do anormalnego zakończenia działania, następuje gdy jest rzucony wyjątek, ale region, w którym ma to miejsce, nie zawiera kodu do jego obsłużenia; zwykle powoduje to propagację awarii do komponentu lub metody wywołującej; błędne zachowanie (ang. erroneous behavior) – następuje, gdy wywołana metoda nie rzuca wyjątku, ale zwraca nieprawidłową
wartość; klasycznym przykładem jest tu tzw. błąd o jeden (tzn. przyjęcie przez jakąś zmienną wartości o jeden mniejszej lub większej od prawidłowej). Klasyfikacja
trybów
awarii
pozwala
na
systematyczne
tworzenie
„generycznych” usterek, które symulują dane awaryjne zachowanie. Usterki te są zwykle dostępne dla testera w odpowiednim oprogramowaniu do posiewu usterek i tester może zdecydować, które z nich wybrać do procesu wstrzykiwania. Przykładami typów wstrzykiwanych usterek mogą być: błędne dane wejściowe; wymuszenie rzucenia wyjątku; wstrzyknięcie zmiany stanu programu; wymuszenie opóźnienia w działaniu programu, wątku lub procesu; wymuszenie błędu pamięci. Na rysunku 10.1 przedstawiono podejście do techniki wstrzykiwania usterek, która pozwala testerowi ulepszyć zbiór przypadków testowych przez ewentualne dodanie do niego testów zdolnych do wykrycia określonych typów usterek, przez co można osiągnąć odpowiedni stopień pokrycia usterek. Tak jak w przypadku testowania mutacyjnego, rozpoczynamy z testowanym programem, początkowym zbiorem przypadków testowych oraz modelem usterki. Na podstawie tego modelu wybieramy typy usterek i wstrzykujemy je w wybrane miejsca programu przy użyciu odpowiednich narzędzi. Na tak zmodyfikowanych wersjach programu wykonujemy testy i sprawdzamy, które usterki nie zostały wykryte. Tworzymy dla nich dodatkowe testy, które będą w stanie je wykryć. Gdy wszystkie usterki są wykryte, proces się kończy. Gdy zachodzi potrzeba wstrzykiwania usterek podczas działania programu, można posłużyć się narzędziami do debugowania. Debugery (np. unixowy gdb) oferują wiele możliwości, takich jak śledzenie i podmiana wartości zmiennych, ustawianie pułapek oraz wiele innych.
Rysunek 10.1. Proces wstrzykiwania usterek i ulepszania zestawu testów (za [131]) Wstrzykiwanie błędów jest techniką wykorzystywaną również wobec sprzętu (np. mikrokontrolerów czy różnego typu systemów wbudowanych). W takim przypadku polega na wymuszaniu podawania błędnych sygnałów na wejście oraz na symulowaniu rzeczywistych awarii wywołanych np. interferencją elektromagnetyczną, zmianą napięcia czy promieniowaniem1.
10.2. Taksonomie Testowanie oparte na taksonomii błędów to metoda, w której testujemy oprogramowanie pod kątem występowania określonych błędów. Zwykle są one zapisane w postaci tzw. taksonomii, czyli hierarchicznego systemu pojęć zaprojektowanego w celu ułatwienia klasyfikacji obiektów opisywanych tymi pojęciami. Taksonomia może być wynikiem doświadczenia naszego lub innych pracowników organizacji, tzn. opisywać błędy, które pojawiały się w przeszłości, przy innych projektach.
taksonomia defektów, taksonomia błędów (ang. defect taxonomy,
bug
taxonomy) – system hierarchicznych kategorii ułatwienia klasyfikacji defektów
celu
zaprojektowany
w
W tabeli 10.1 jest opisana przykładowa taksonomia, pochodząca z [93], a oryginalnie oparta na pracy Beizera [14]. Tabela 10.1. Tasonomia defektów oparta na przyczynie źródłowej (root-cause)
Kategoria
Funkcjonalnoś ć
T yp defektu
Kategoria
S pecyfikacja
Typ
Funkcja
S truktura
Tes t
Dane
Wewnętrzne interfejs y Hardware S ys tem
Proces
T yp defektu
Wartoś ć początkowa Inne
Kod
S ys tem operacyjny Dokumentacja Architektura oprog ramowania
S tandardy
Zarządzanie zas obami
Inne
Arytmetyka
Duplikat
Inicjalizacja
To nie problem
S terowanie lub kolejnoś ć
Zewnętrzny
Log ika s tatyczna
Potrzebna analiza przyczyny
Inne
Niewytłumaczalny
Taksonomia składa się z ośmiu głównych kategorii oraz pięciu dodatkowych (Duplikat, To nie problem, Zewnętrzny, Potrzebna analiza przyczyny, Niewytłumaczalny), które są pomocne przy klasyfikacji w systemach do śledzenia defektów. Dalej opisujemy każdy z typów tej taksonomii: Specyfikacja – specyfikacja funkcjonalna (analiza wymagań, projekt architektury itp.) jest błędna; Funkcja – specyfikacja jest poprawna, ale jej implementacja błędna; Test – istnieje jakiś problem z danymi testowymi, przypadków testowych lub implementacją testu; Wewnętrzne interfejsy nieprawidłowa;
–
komunikacja
wewnątrz
projektem
systemu
jest
Hardware – problem ze sprzętem; System operacyjny – błąd dotyczy złego działania operacyjnego, pod którym działa testowany program;
systemu
Architektura oprogramowania – projekt architektury (struktury) oprogramowania jest błędny; Zarządzanie zasobami – architektura jest poprawna, ale jej implementacja jest błędna, np. powoduje opóźnienia w przesyłaniu lub aktualizacji danych; Arytmetyka – obliczenia wykonywane przez program są błędne, np. na skutek niepoprawnego wykonywania działań, złego zaokrąglania lub stosowania zbyt małej lub zbyt dużej dokładności; Inicjalizacja – następuje błąd przy pierwszym wykonaniu danej operacji; Sterowanie lub kolejność – nastąpiła zła sekwencja akcji; Logika statyczna – występują błędy dla wartości brzegowych, klasy równoważności nie zawierają właściwych elementów itp.; Inne (w kategorii Proces) – nastąpił inny niż powyższe błąd dotyczący przepływu sterowania lub przetwarzania danych; Typ – zły typ danych;
Struktura – struktura danych jest niewłaściwa lub źle użyta; Wartość początkowa – wartość elementu jest inicjalizowana na niewłaściwą wartość lub źle używana, np. iteracja pętli od indeksu 1 zamiast od 0; Inne (w kategorii Dane) – wystąpił inny niż powyższe błąd związany z danymi; Kod – pomyłki w kodzie, np. literówki; zwykle wyłapywane przez kompilator, ale w niektórych sytuacjach, np. związanych ze skryptami w przeglądarkach, mogą się zdarzać; Dokumentacja – system działa poprawnie, ale dokumentacja przewiduje inne jego zachowanie w danych okolicznościach; Standardy – system nie spełnia określonych norm, standardów czy innych regulacji; Inne – inna niż powyższe przyczyna defektu; Duplikat – dwa raporty o defektach opisują ten sam błąd; To nie problem – zaobserwowane zachowanie jest poprawne; Zewnętrzny – błąd pochodzi z zewnątrz i jest niezależny od testowanego programu (np. rzadka awaria sprzętu); Potrzebna analiza przyczyny – defekt został zamknięty, ale nie przeprowadzono analizy przyczyny podstawowej; Niewytłumaczalny – nikt nie wie, jaki jest powód awarii. Black [93] podaje również inny przykład taksonomii, skupiony na symptomach błędów, w przeciwieństwie do powyższej, skupionej raczej na przyczynach defektów: błędna funkcjonalność; brakująca funkcjonalność; zła użyteczność; nieudana kompilacja (build); zły projekt/architektura systemu; problem z niezawodnością; utrata danych; problem z efektywnością; przestarzały kod; odchylenie od specyfikacji;
zła dokumentacja użytkownika. Przedstawiona taksonomia jest dosyć ogólna, ale prawdopodobnie lepiej niż wcześniejsza pozwala na projektowanie przypadków testowych. W razie potrzeby lub jeśli organizacja ma dokładne dane historyczne z poprzednich projektów oraz wykonywała analizę przyczyny podstawowej, można zaadoptować którąś z istniejących taksonomii i ją uszczegółowić lub zbudować swoją własną. Taksonomie defektów służą nie tylko do projektowania testów lecz także wykorzystuje się je także w procesie udoskonalania procesu testowego (patrz podrozdz. 33.2).
10.3. Zgadywanie błędów 10.3.1. Opis metody Zgadywanie błędów to technika projektowania przypadków testowych na podstawie intuicji i doświadczenia testera. Po raz pierwszy została opisana przez Myersa [10]. Polega na domyśleniu się (zgadnięciu), jakie pomyłki mogły zostać popełnione przez programistę i w jaki sposób można je ujawnić. Metodę można wykorzystywać, wspomagając się taksonomią błędów. Skuteczność metody zależy od indywidualnych cech testera – jego intuicji, doświadczenia, znajomości systemu oraz programistów i tego, jakie błędy najczęściej popełniają. zgadywanie błędów – technika projektowania testów, gdzie, bazując na doświadczeniu testera, przewiduje się, jakie defekty, będące efektem wykonanych pomyłek, mogą być obecne w testowanym module lub systemie i projektuje się testy tak, aby je ujawnić Technika zgadywania błędów może być elementem formalnego, zaplanowanego procesu testowego. Wynikiem stosowania techniki są bowiem konkretne przypadki testowe podlegające udokumentowaniu. Zgadywanie błędów jest również metodą stosowaną podczas analizy ryzyka (patrz rozdz. 19).
10.3.2. Przykład
Rozważmy system ELROJ z Dodatku A. Dalej są opisane testy dla poszczególnych funkcji systemu, jakie mógłby wymyślić tester, „zgadując” możliwe defekty. void setInfo(String info) – funkcja pozwala na ustawienie komunikatu. Przykładem testu mogłoby być wywołanie funkcji dla pustego łańcucha znaków, łańcucha dłuższego niż miejsce na ekranie służące do jego wyświetlenia czy też łańcucha zawierającego znaki specjalne. void removeInfo() – funkcja usuwająca aktualnie wyświetlany komunikat. Test mógłby polegać na dwukrotnym wywołaniu funkcji, zmuszając program do usunięcia komunikatu w sytuacji, gdy komunikatu nie ma. void addLine(int line) – funkcja dodająca do rozkładu linię line. Test mógłby polegać na wywołaniu funkcji dla linii, która już istnieje w rozkładzie i ma jakieś kursy. void addBus(int line, int mm) – funkcja dodająca do rozkładu czas mm przyjazdu autobusu linii line. Przykładem testu mogłaby być próba wywołania funkcji z ujemną wartością dla parametru line oraz mm, wywołanie dla mm = 60 oraz dla mm > 60. Przy okazji, tester mógłby zgłosić niekompletność specyfikacji, bo nie wynika z niej, jak ma się zachować program dla takich parametrów mm – czy traktować je jako błędne, czy brać je modulo 60. void setActualDate(int day, int month, int year) – funkcja ustawia aktualną
datę pokazywaną
u góry
ekranu elektronicznego. Tester może
spróbować wywołać ją np. dla 29 lutego dla roku nieprzestępnego, który jest podzielny przez 100 i nie dzieli się przez 400.
10.4. Testowanie oparte na liście kontrolnej Testowanie oparte na liście kontrolnej (ang. checklist) jest w swej koncepcji nieco podobne do testowania opartego na taksonomii. Różnica jest taka, że taksonomia opisuje defekty, a lista kontrolna – cechy czy właściwości programu, które należy zbadać. Ponadto lista kontrolna jest zwykle skoncentrowana wokół jednego
tematu, np. kwestii bezpieczeństwa, wydajności, standardów, danych. Lista kontrolna może być uaktualniana o nowe aspekty wraz z nabywaniem przez zespół doświadczenia w wytwarzaniu i testowaniu oprogramowania. Listy kontrolne mogą również powstawać jako efekt uboczny analizy ryzyka jakościowego (patrz rozdz. 19). Tabela 10.2. Lista kontrolna dla sprawdzenia użyteczności strony www
Nr Element 1
Nawig acja – czy nawig acja między s tronami jes t widoczna i łatwa w użyciu dla użytkownika?
2
Etykiety – czy opis y i ikony przycis ków s ą zrozumiałe?
3
Kolorys tyka – czy kolorys tyka s trony jes t s pójna i wyg odna dla użytkownika?
4
Liczba obiektów – czy s trona nie jes t przeładowana zbyt wieloma elementami?
5
S truktura – czy s truktura drzewias ta s trony jes t wyważona?
6
S pójnoś ć – czy analog iczne elementy s trony s ą przeds tawiane w taki s am s pos ób?
7
Ods yłacze – czy ws zys tkie ods yłacze działają i s ą widoczne na s tronie?
8
Tytuł – czy s trona g łówna i ws zys tkie pods trony mają poprawne tytuły?
9
Drukowanie – czy każda s trona ma s woją wers ję do druku i czy drukuje s ię poprawnie?
Efektywnoś ć – czy s trona oraz jej pods trony wczytują s ię 10 odpowiednio s zybko? Czy s trony nie s ą przeładowane g rafiką mog ącą opóźniać wczytywanie s trony?
Lista kontrolna pomaga w systematycznym testowaniu, dzięki czemu tester pamięta o wszystkich aspektach programu, które należy przetestować. Można zdefiniować kryterium pokrycia, wymagając, aby dla każdego elementu listy kontrolnej stworzono co najmniej jeden test. Często jednak listy kontrolne mogą być zdefiniowane bardzo ogólnie i często właściwe przetestowanie jednego tylko elementu tej listy wymaga stworzenia wielu testów. Hipoteza błędów w tej technice mówi, że błędy pojawiają się najczęściej w obszarach opisanych listą kontrolną. Tabela 10.2 jest przykładową listą kontrolną dla testowania użyteczności strony www.
10.5. Testowanie eksploracyjne 10.5.1. Opis metody Testowanie eksploracyjne (nazywane również testowaniem ad hoc) należy do metod opartych na doświadczeniu. Może wykorzystywać taksonomię lub listę kontrolną, jednak zasadniczo różni się od tych technik pod jednym względem: jest metodą dynamiczną, w której występuje bardzo duża interakcja między testerem a testowanym programem. Polega ona na jednoczesnym uczeniu się programu, obserwowaniu jego zachowania, projektowania i przeprowadzania testów. Tester na bieżąco decyduje, którą „ścieżką” użytkowania programu podążyć tak, aby z możliwie jak największym prawdopodobieństwem doprowadzić do awarii lub błędu. Skuteczność techniki zależy od umiejętności, intuicji i doświadczenia testera oraz umiejętnego wyszukiwania, rozpoznawania oraz podążania za śladami mogącymi wskazywać na jakieś nieprawidłowości w testowanym programie. testowanie eksploracyjne (ang. exploratory testing) – nieformalna technika projektowania testów, w której tester projektuje testy w czasie, gdy są one wykonywane i wykorzystuje informacje zdobyte podczas testowania do projektowania nowych i lepszych testów [134] W testowaniu eksploracyjnym nie projektuje się testów na podstawie modeli, tak jak ma to miejsce w klasycznych metodach opartych na specyfikacji czy strukturze. Również element planowania nie występuje tak silnie, jak w innych technikach. Testowanie odbywa się „tu i teraz”, choć zwykle w ustalonych
uprzednio ramach czasowych. Jeśli mówimy o planowaniu, to raczej odbywa się ono na wyższym poziomie niż planowanie przypadków testowych i jest związane raczej z określaniem, kiedy i w jakim zakresie mają zostać przeprowadzone sesje testowania eksploracyjnego. Na testowanie eksploracyjne składa się wiele aktywności [135]: eksploracja testowanego produktu – zdefiniuj cel oraz funkcje produktu, typ przetwarzanych danych, obszary potencjalnej niestabilności; zdolność do przeprowadzenia testów eksploracyjnych zależeć będzie od znajomości technologii, informacji o produkcie i jego potencjalnych testowanie;
użytkownikach
oraz
czasu
projektowanie testów – określ strategię obserwowania i oceny testowanego programu;
przeznaczonego sposobu
na
używania,
wykonanie testów – zacznij pracę z programem, obserwuj jego zachowanie, użyj tej informacji do sformułowania hipotez o tym, jak produkt działa; heurystyki – czyli wskazówki lub inżynierskie reguły pomagające w zdecydowaniu, co powinniśmy jako testerzy zrobić w danej sytuacji; pomagają odpowiedzieć na pytanie, co przetestować i w jaki sposób; ocenialne rezultaty – tester musi udokumentować przebieg sesji testowania eksploracyjnego, czyli jakie czynności wykonał i jakie awarie znalazł. Sesja testowania eksploracyjnego często zaczyna się od tzw. karty testu (ang. test charter), zwanej czasem kartą opisu testu lub kartą opisu, w której opisuje się cel sesji oraz – być może – opis techniki, którą chcemy zastosować. Karta testu może być stworzona przez testera lub może zostać narzucona mu przez kierownika testów. Poziom formalizacji w stosunku do karty testów może być bardzo różny – od formalnych, spisywanych dokumentów, po reprezentowanie ich za pomocą dokumentacji opisującej na wysokim poziomie przypadki i procedury testowe. karta opisu testu, karta testu, karta opisu (ang. test charter) – dokument zawierający deklarację celów testu oraz ewentualne pomysły na testowanie. Karty testów są często wykorzystywane w testowaniu eksploracyjnym
sesja testowa nieprzerywalny
(ang. okres
test session) – w testowaniu eksploracyjnym czasu poświęcony testowaniu. Każda sesja jest
zorientowana na kartę testu, ale tester może w tym czasie także odkrywać nowe możliwości lub problemy z oprogramowaniem. Tester tworzy i wykonuje przypadki testowe w locie oraz zapisuje ich postęp Testowanie eksploracyjne jest często techniką niedocenianą. Wynikać to może co najmniej z dwóch powodów: po pierwsze, jej skuteczność najpełniej objawia się, gdy jest stosowana przez doświadczonych testerów. Po drugie, nie jest metodą wymagającą głębokiego planowania, projektowania przypadków testowych przed ich wykonaniem, silnej formalizacji oraz przesadnie dużego narzutu dokumentacyjnego. To może rodzić u kierowników testów obawy, że sesje testowania eksploracyjnego trudno poddają się kontroli oraz ocenie skuteczności. Z punktu widzenia menedżera coś, czego nie da się zaplanować, kontrolować i ocenić, jest działaniem ryzykownym, stąd niechętnie wykorzystywanym. Jak zobaczymy w podrozdziale 23.3, większość tych obaw jest bezzasadna. zarządzanie testowaniem w sesjach (ang. session-based test management) – metoda pomiaru i zarządzania testowaniem w sesjach, np. w testowaniu eksploracyjnym Testowanie eksploracyjne najlepiej stosować, gdy [136]: w krótkim czasie i w rozsądny sposób chcemy ocenić poziom jakości oprogramowania; wymagania są niekompletne lub jest ich brak; struktura programu jest skomplikowana, ma on wiele opcji, trybów działania i nie do końca jesteśmy w stanie zastosować systematyczną technikę projektowania testów; projekt jest prowadzony w metodyce zwinnej, z krótkimi sprintami i często zmieniającymi się wymaganiami; system jest gotowy, istnieje GUI, można przeprowadzać testy end-to-end i chcemy przeprowadzać testy systemowe lub akceptacyjne. Testowanie eksploracyjne nie sprawdzi się jednak, jeśli:
oczekujemy odkrycia błędów określonego typu (np. przez stosowanie tablic decyzyjnych w celu wykrycia błędów w logice biznesowej); testy mają być wartościowymi elementami suity testów regresyjnych; chcemy mieć informację o stopniu pokrycia testami testowanego systemu; system nie jest jeszcze gotowy, nie da się przeprowadzić testów end-toend, a chcemy już testować pojedyncze moduły lub przeprowadzać testy małej integracji.
10.5.2. Przykład sesji testowania eksploracyjnego Pokażemy teraz, jak może wyglądać sesja testowania ekspoloracyjnego w praktyce. Tester jest członkiem zespołu wykonującego testy systemowe programu MS Word 2007. Decyduje się na przetestowanie funkcji tworzenia odsyłaczy, czyli podpisów pod tabelami czy rysunkami. Karta testu przed rozpoczęciem sesji może wyglądać jak na rysunku 10.2.
Id sesji: TEXP.025.001 T ester: Jan Kowals ki Data: 13.07.2014 Czas rozpoczęcia: 11:45 Czas zakończenia: Cel: przetes towanie funkcjonalnoś ci tworzenia etykiet ods yłaczy, ws tawiania ods yłaczy oraz odwoływania s ię do nich w dokumencie Znalezione błędy: Kwestie do dalszej analizy: Rysunek 10.2. Karta testu przed rozpoczęciem sesji testowania eksploracyjnego Tester rozpoczyna sesję od przejrzenia menu Worda w celu odnalezienia wszystkich funkcji mogących mieć związek z celem naszego testu. Znajduje następujące elementy:
Wstawianie > Obraz oraz Wstawianie > Tabela (możemy sprawdzić, czy po wstawieniu rysunku można wstawić podpis, używając menu kontekstowego i czy podpis wstawi się we właściwym miejscu); Odwołania > Wstaw podpis (sprawdzimy funkcjonalność wstawiania podpisów); Odwołania > Odsyłacz (sprawdzimy, czy odwołania do elementów są poprawne); Odwołania > Wstaw spis ilustracji (sprawdzimy, czy są poprawnie tworzone spisy na podstawie istniejących w dokumencie podpisów). Tester wybiera opcję wstawienia podpisu. Zauważa, że jest możliwe stworzenie nowej etykiety, zatem tworzy etykietę o nazwie „Tablica” (rys. 10.3). Następnie tworzy w dokumencie podpis pod wyimaginowaną tablicą (rys. 10.4).
Rysunek 10.3. Stworzenie etykiety w MS Word
Rysunek 10.4. Stworzenie podpisu tablicy w MS Word Kolejny krok to stworzenie w tekście odsyłacza do tej tablicy (rys. 10.5). Intencją testera jest, aby tekst odwoływał się do tabeli przez jej numer: „Jak podano w tablicy 1.”. Niestety, po dodaniu odsyłacza tekst brzmi „Jak podano w tablicy Tablica 1”. Tester, po sprawdzeniu rodzajów odsyłaczy zauważa, że w programie MS Word nie da się tak zdefiniować odsyłaczy, by możliwe było odwołanie się jedynie do numeru tabeli, bez dodawania nazwy etykiety „Tabela”. Jest to pierwszy zauważony problem. Przeprowadzamy sesję eksploracyjną, dlatego tester postanawia przyjrzeć się mu dokładniej. Próbuje zmienić nazwę podpisu z „Tablica 1. Przykładowa tablica” na „1. Przykładowa tablica”, usuwając nazwę etykiety „Tablica”. Teraz odwołanie jest poprawne, ale z kolei podpis zaczyna się od liczby – nie ma w nim słowa „Tablica”. Tester próbuje następnie w podpisie zmienić wyrażenie „Przykładowa tablica” na „Przykładowa Tablica”. Okazuje się, że teraz tekst odsyłacza po wstawieniu go w tekście zmienił się na „1. Przykładowa Tablica”! Tester podejrzewa, że ma to związek z tym, że w nazwie tablicy użyto dokładnej nazwy etykiety („Tablica”, pisane z wielkiej litery). Uznaje to za błąd. Ostatecznie, po zakończeniu sesji karta testów zostaje uzupełniona i może wyglądać tak jak na rysunku 10.6.
Rysunek 10.5. Stworzenie w tekście odsyłacza do tablicy w MS Word
Id sesji: TEXP.025.001 T ester: Jan Kowals ki Data: 13.07.2014 Czas rozpoczęcia: 11:45 Czas zakończenia: 12:20 Cel: przetes towanie funkcjonalnoś ci tworzenia etykiet ods yłaczy, ws tawiania ods yłaczy oraz odwoływania s ię do nich w dokumencie Znalezione błędy: Użycie w podpis ie nazwy etykiety powoduje, w przypadku tworzenia ods yłacza w trybie „ tylko etykieta i numer” , umies zczenie w ods yłaczu całeg o podpis u, zamias t jedynie etykiety i numeru. MS Word nie potrafi odróżnić nazwy etykiety od teg o s ameg o s łowa, ale wys tępująceg o w treś ci podpis u. Kwestie do dalszej analizy: MS Word nie pozwala odwoływać s ię do etykiet tylko za pomocą numeru – w takim przypadku wymus za również wykorzys tanie nazwy etykiety. Zatem definiując podpis „ Tablica 1. XYZ” , nie uda nam s ię s tworzyć podpis u typu „ Jak
pokazano w tablicy 1” , lecz tylko „ Jak pokazano w tablicy Tablica 1.” . Rozwiązaniem może być us uwanie w treś ci podpis u nazwy etykiety, ale wtedy typy etykiet (rys unki, tabele, lis ting i itp.) będą nierozróżnialne, bo np. każdy pierws zy rys unek, tabela oraz lis ting będą zaczynały s woje nazwy od liczby „ 1” . Rysunek 10.6. Karta testu po zakończeniu sesji testowania eksploracyjnego
10.6. Ataki usterkowe Technika ataków usterkowych jest w pewnym sensie połączeniem opisanych we wcześniejszych rozdziałach technik opartych na defektach i doświadczeniu. W technice ataków usterkowych skupiamy się na testowaniu programu pod kątem wykrywania ściśle określonych typów błędów, podobnie jak w metodzie taksonomii. Atak usterkowy jest jednak metodą bardziej ustrukturyzowaną, gdyż opiera się na konkretnym modelu usterki, który opisuje, w jaki sposób usterki mogą pojawić się w programie i dlaczego objawiają się jako awarie. Bazując na modelu usterki, możemy wykorzystać istniejące listy możliwych do przeprowadzenia ataków, co upodabnia metodę do techniki opartej na liście kontrolnej. Przykłady takich list podał Whittaker w swoich znakomitych książkach: How to Break Software [137], How to Break Software Security [138] oraz How to Break Web Security [139]. atak, atak usterek, atak na oprogramowanie (ang. attack, fault attack, software attack) – ukierunkowane działanie mające na celu ocenę jakości, w szczególności niezawodności obiektu testów, przez wymuszenie wystąpienia określonej awarii Przykładowy model usterki jest pokazany na rysunku 10.7.
Rysunek 10.7. Model usterki powodowanej interakcją programu i środowiska Na rysunku 10.7 przedstawiono testowany program w jego otoczeniu. Każda interakcja programu z elementem otoczenia może być źródłem usterek i powodem awarii. Użytkownik bezpośrednio uczestniczy w nikłym odsetku interakcji zachodzących w systemie. Na przykład wejście wprowadzone przez użytkownika z klawiatury może przechodzić przez różne warstwy architektury systemu za pomocą API (ang. Application Programming Interface), powodować komunikację programu z bazą danych, systemem plików czy systemem operacyjnym (np. żądanie zasobów takich, jak miejsce na dysku, pamięć czy czas procesora). Dlatego model usterki niekoniecznie musi dotyczyć wyłącznie bezpośredniej interakcji użytkownika z testowanym programem. Wbrew temu, co większość ludzi myśli, ta komunikacja nie jest głównym aspektem testowania [137]. Whittaker [137] proponuje m.in. następujące kategorie ataków usterkowych: Ataki na interfejs użytkownika – wejścia i wyjścia wymuszenie pojawienia się wszystkich możliwych komunikatów o błędach, co zapewnia, że kod obsługuje sytuacje wyjątkowe; wymuszenie użycia przez program wartości domyślnych, co zapewni przetestowanie możliwie jak największej ilości wartości domyślnych
oraz sprawdzenie, czy program poprawnie inicjalizuje wartości zmiennych; użycie różnych zestawów znaków, typów kodowań (np. ASCII zamiast Unicode), słów zarezerwowanych, typów danych itp., co sprawdzi, jak program kontroluje poprawność wprowadzanych danych; wymuszenie przepełnienia bufora wejściowego, co sprawdza jak program obsługuje wejścia o zbyt dużym rozmiarze; przetestowanie kombinacji wejść, które mogą wchodzić ze sobą w interakcje; wielokrotne powtórzenie tego samego wejścia; wymuszenie wygenerowania różnych wyjść dla tego samego wejścia, podanego w różnych kontekstach; wymuszenie wygenerowania nieprawidłowych wyjść. Ataki na interfejs użytkownika – dane i obliczenia wprowadzenie wejścia w różnych okolicznościach (np. zapisanie pliku po zmianach oraz zapisanie pliku bez wprowadzania żadnych zmian); wymuszenie przechowania w strukturze danych zbyt wiele lub zbyt mało wartości; wymuszenie zmiany strukturę danych;
wewnętrznych
warunków
nałożonych
na
użycie niepoprawnych kombinacji operatorów i operandów (np. dzielenie przez zero, obliczenie pierwiastka z liczby ujemnej, wymuszenie błędów zaokrągleń – np. wykonanie w kalkulatorze Windows ciągu operacji: 2, √, ^2, ^2, –4 powinno dać w rezultacie 0, a daje 5.38 · 10–37); wymuszenie wywołania rekurencyjnego funkcji; wymuszenie zbyt dużego lub zbyt małego wyniku; znalezienie elementów źle współpracujących ze sobą lub nieprawidłowo współdzielących dane. Ataki na interfejs systemowy – system plików całkowite wypełnienie systemu plików, co sprawdzi, jak program radzi sobie w sytuacji, gdy dysk jest pełny;
wymuszenie zajętości lub niedostępności urządzenia lub innego obiektu;
procesu,
programu,
uszkodzenie pliku, programu, urządzenia lub innego obiektu; przypisanie nieprawidłowej nazwy pliku; zmiana praw dostępu do plików; zmiana lub wprowadzenie błędnej zawartości do pliku. Ataki na interfejs systemowy – interfejsy aplikacji/systemu operacyjnego wstrzykiwanie błędów wymuszających rzucanie wyjątków, naśladujących rzeczywiste awarie, błędy pamięci, błędy sieci itp.
10.7. Testowanie ad hoc Testowanie ad hoc polega na nieformalnym wykonywaniu testów. Nie stosuje się żadnych technik projektowania przypadków testowych, nie występuje tu żaden element planowania bądź raportowania. Wykonaniem testu kieruje zupełna dowolność. Zwykle też nie wytwarza się żadnej dokumentacji (np. o incydentach). Niektórzy autorzy używają nazwy „testowanie ad hoc” na określenie testowania eksploracyjnego, jednak o ile w testowaniu eksploracyjnym tester postępuje jednak według pewnych reguł, o tyle w testowaniu ad hoc nie ma absolutnie żadnych formalnych zasad. W szczególności testowanie ad hoc nie jest planowane i często nie powstają żadne raporty z przeprowadzenia takiej sesji testowej. testowanie ad hoc (ang. ad hoc testing) – testy wykonywane nieformalnie. Nie ma miejsca na żadne formalne przygotowanie testu, nie jest użyta żadna rozpoznawalna technika projektowania przypadków testowych, brak jest oczekiwań co do rezultatów, wykonaniem testu kieruje dowolność
1 Niektóre półprzewodniki są wysoce podatne na promieniowanie neutronowe czy promieniowanie alfa, wywołane np. radioaktywnymi izotopami U-238, Th232 lub Pb-210.
11. Wybór odpowiednich technik
Każdy projekt jest odmienny i ma indywidualne, niepowtarzalne cechy. Dlatego nie istnieje jeden, uniwersalny model wytwarzania oprogramowania, którym można skutecznie opisać wszystkie projekty. Kierownik projektu musi wybrać odpowiedni model, ale może też nieco zmodyfikować istniejącą metodykę i dostosować ją do swoich potrzeb. Znane są np. projekty wytwarzania oprogramowania o znaczeniu krytycznym oparte na metodykach zwinnych. W takich przypadkach należy dodać do modelu pewien niezbędny narzut dokumentacyjny, wprowadzić zarządzanie ryzykiem czy zdecydować się na dodatkowe etapy, takie jak przeglądy formalne. Proces testowy wynika z przyjętego modelu wytwórczego i testerzy muszą dostosować się do wybranego podejścia do cyklu życia oprogramowania (podrozdz. 4.1 ma na celu pomóc testerom w zorientowaniu się, jak proces testowy jest umiejscowiony w różnych modelach oraz co z punktu widzenia testowania jest specyficzne dla każdego z nich). Uwagi te odnoszą się również do samego wyboru technik testowania. Nie istnieje jedna, uniwersalna technika. Każda z przedstawionych w poprzednich rozdziałach metod jest skuteczna tylko w pewnych granicach i pozwala uzyskać przypadki testowe wyczerpujące jedno, określone kryterium pokrycia. Niektórzy autorzy są orędownikami konkretnych technik i w swojej pracy ograniczają się tylko do nich. Niestety, nie jest to dobre podejście. Na przykład testowanie eksploracyjne jest bardzo efektywną techniką, zwłaszcza, jeśli jest przeprowadzane przez doświadczonego testera. Jednak technika ta będzie zupełnie nieprzydatna w testowaniu pod kątem wykrywania tzw. koni trojańskich czy bomb logicznych, czyli fragmentów kodu mających na celu umożliwienie użytkownikowi dostęp do systemu z obejściem istniejących w nim zabezpieczeń lub wykonanie szkodliwych akcji. Tego typu defekty można
natomiast wykryć, stosując jedno z najprostszych kryteriów – kryterium pokrycia instrukcji. Doświadczony tester potrafi dobrać odpowiednie techniki (być może nawet tylko jedną lub dwie) i na ich podstawie skonstruować niezwykle skuteczne testy. Czasami przyjęty model wytwarzania oprogramowania jest bardzo specyficzny, np. łączy w sobie cechy kilku innych modeli. Wtedy pewną pomocą w wyborze podejścia do testowania może być Heurystyczny model strategii testowej opracowany przez Bacha [140]. Jest to zbiór wzorców dla projektowania strategii testowej (rys. 11.1).
Rysunek 11.1. Heurystyczny model strategii testowej Środowisko projektu to wszelkiego rodzaju zasoby oraz ograniczenia w projekcie mogące wpłynąć na proces testowania (np. na jego opóźnienie, utrudnienie). Elementy składowe produktu to rzeczy podlegające testowaniu. Odpowiadają one z grubsza warunkom testowym i elementom pokrycia w normie ISO/IEEE 29119-2 [32]. Kryteria jakości to wszystkie reguły, warunki, cechy i parametry produktu i procesu, które pozwalają nam stwierdzić, czy w produkcie
występują jakieś problemy, tzn. pozwalają nam ocenić go pod kątem jakości. Należy pamiętać, że jakość jest pojęciem wielowymiarowym i często trudnym do zdefiniowania. W formułowaniu kryteriów jakości bierze udział wielu interesariuszy – od klienta począwszy, przez analityków biznesowych, kierownika projektu i deweloperów, na testerach skończywszy. Techniki testowania wynikają z trzech opisanych wcześniej części składowych strategii. Bach nazywa je heurystykami, za pomocą których tworzy się przypadki testowe. Techniki te mogą również być bardzo ścisłe i dobrze sformułowane, często nie pozwalając testerowi na zbytnią swobodę w „inwencji twórczej” przy wymyślaniu testów. Stosując techniki testowania, uzyskujemy tzw. postrzeganą jakość. Nie jest to rzeczywista jakość produktu, bo tej nigdy nie możemy poznać w stu procentach. Jakość postrzegana to jakość, której poziom szacować możemy na podstawie tego, jakie testy zaprojektowaliśmy i jaki jest ich wynik. Bach wyróżnia następujące kategorie heurystyk testowych: testowanie funkcji, czyli tego, co program robi; testowanie stwierdzeń, czyli zdań opisujących produkt zawartych w dokumentacji, wymaganiach, podręcznikach użytkownika itp.; testowanie dziedziny, czyli danych oraz relacji między nimi; testowanie przez użytkownika, czyli zaangażowanie końcowego użytkownika produktu do testowania lub też symulowanie działania określonego typu użytkownika na rzeczywistych danych; testowanie wytrzymałościowe (ang. stress testing), czyli to, jak efektywnie program działa w warunkach różnego rodzaju obciążeń (zbyt mało pamięci, niska przepustowość sieci, duży rozmiar danych wejściowych itp.); testowanie ryzyka, tzn. wyobrażenie sobie jakiego rodzaju problemy mogą wystąpić podczas użytkowania, a następnie przetestować program pod tym kątem (jest to forma testowania opartego na defektach lub atakach usterkowych); testowanie sekwencji, czyli wykonywanie czynności w różnej kolejności i czasie, bez resetowania aplikacji między poszczególnymi akcjami; sprawdzanie automatyczne, czyli generacja dużej ilości różnych danych, próba pokrywania różnego rodzaju warunków
testowych/elementów
pokrycia,
automatyzacja,
wykorzystanie
wyroczni, generatorów danych itp.; testowanie scenariuszy, czyli forma testowania systemowego (end-toend) i akceptacyjnego.
12. Priorytetyzacja przypadków testowych
12.1. Wprowadzenie W większych projektach IT często zdarza się, że zestaw testów regresyjnych osiąga bardzo duże rozmiary i wykonanie go może być problematyczne. W [141] jest podany przykład rzeczywistej aplikacji przemysłowej liczącej 20 000 linii kodu, której testy mające spełniać kryterium MC/DC wykonywały się przez 7 tygodni (!). Dlatego zachodzi potrzeba priorytetyzacji i optymalizacji (redukcji) rozmiaru suit testowych przez wykonywanie najważniejszych testów na początku oraz usuwanie testów nieefektywnych. Ważność testu może wynikać z wielu czynników, ale najczęściej dotyczy jego skuteczności, tzn. tego jak dużo defektów jest w stanie wykryć. Formalnie, problem priorytetyzacji testów jest zdefiniowany w następujący sposób: mając suitę testową T, zbiór PT wszystkich możliwych permutacji T oraz funkcję f: PT → R, należy znaleźć taką kolejność wykonywania testów T′ ∈ PT, że ∀T″ ∈ PT, T″ ≠ T′ f(T″) ≥ f(T′). Należy więc znaleźć kolejność wykonywania testów, dla której wartość funkcji f jest najmniejsza. Funkcja f, nazywana funkcją kosztu, jest ilościowym wyrażeniem kryteriów priorytetyzacji, które przyjmujemy. Istnieje wiele praktycznych powodów, aby priorytetyzować testy: zwiększenie współczynnika wykrywania defektów przez suitę testową, tzn. zwiększenie prawdopodobieństwa, że defekty będą znajdowane możliwie jak najwcześniej (na początkowych etapach testów regresji); zwiększenie szybkości pokrycia określonego kryterium pokrycia tak, aby pokrycie kodu dokonywało się jak najwcześniej w całym procesie testowym;
wczesne zwiększenie pewności co do niezawodności systemu; przyspieszenie znajdowania najpoważniejszych defektów (tak, by były znajdowane przed defektami mniej istotnymi); zwiększenie prawdopodobieństwa wykrywania defektów związanych ze zmianami w kodzie we wczesnych fazach testów regresyjnych. W tym rozdziale interesować nas będzie pierwszy powód, mianowicie jak najwcześniejsze wykrywanie defektów. Priorytetyzacja wymaga wiedzy o tym, jak efektywne w wykrywaniu defektów są poszczególne testy. Informacja ta zwykle nie jest dostępna, gdy do tworzenia testów wykorzystuje się techniki nieoparte na defektach (np. dotyczące pokrycia kodu). W takim przypadku stosuje się heurystyki, np. rozmiar pokrycia, szacowaną podatność kodu na błędy, złożoność kodu, zmiany w wymaganiach lub kodzie, profil operacyjny czy analizę ryzyka. Jeśli testy są tworzone za pomocą techniki opartej na defektach, możliwości wykrywania określonych defektów przez poszczególne testy są znane. Informacja ta może być wykorzystana przy priorytetyzacji. Priorytetyzację można stosować w przypadku testów regresji, ale też we wczesnych fazach testowania. Różnica polega na tym, że testy regresji są uruchamiane wielokrotnie, zatem mamy dodatkową informację z poprzednich przebiegów o tym, jak efektywne były poszczególne testy. We wczesnych fazach testowania nie dysponujemy takimi danymi i możemy opierać się jedynie na heurystykach lub danych historycznych z innych projektów.
12.2. Ocena priorytetyzacji – miara APFD Najczęściej wykorzystywaną miarą do oceny skuteczności kolejności testów w suicie jest tzw. miara APFD (ang. Average Percentage of Faults Detected). Jest ona zdefiniowana następująco: niech będzie dana suita testowa T zawierająca n testów i wykrywająca m defektów. Niech T′ oznacza ustaloną kolejność testów z T, a Ti niech będzie numerem pierwszego (w kolejności wykonywania) testu w uporządkowanym zbiorze T′, który wykrywa i-ty defekt. Wtedy
Miara APFD przyjmuje wartości od 0 do 11. Im większa jej wartość, tym szybciej dane uporządkowanie testów wykrywa defekty. Rozważmy przykładowy program z suitą testową T zawierającą 6 testów, które w sumie wykrywają 10 defektów. W tabeli 12.1 pokazano, który test wykrywa które defekty. Tabela 12.1. Przypadki testowe i usterki przez nie wykrywane
Usterka Tes t
1
2
PT1
X
X
3
4
5
6
7
X
X
X
X
X
8
9
X
X
10
X
PT2 PT3
X
PT4
X
PT5
X
PT6
X
X
X
X
X
Rozważmy następujące dwie kolejności przypadków testowych z tej suity: T′ = (PT1, PT2, PT3, PT4, PT5, PT6); T″ = (PT3, PT5, PT6, PT1, PT2, PT4). W T′ pierwszy test, PT1, wykrył 30% usterek (3 z 10). Drugi nie wykrył nic, trzeci wykrył dodatkowe 4 usterki, zatem pierwsze trzy testy w sumie wykryły 70% usterek. Test czwarty nie wykrył żadnej nowej usterki – oba wykryte przez niego defekty zostały znalezione już wcześniej. Test piąty ujawnił 2 nowe usterki, a test ostatni – jedną nową usterkę. Usterki 1, 2 i 4 zostały po raz pierwszy znalezione w teście nr 1, zatem T1 = T2 = T4 = 1. Usterki 3, 5, 6, 7 zostały po raz pierwszy w teście trzecim, stąd T3 = T5 = T6 = T7 = 3. Podobnie, mamy T8 = T9 = 5 oraz T10 = 6. Zatem APFD(T″) =
W przypadku kolejności T″ mamy
. Zatem kolejność T″ jest lepsza od T′, ponieważ szybciej znajdziemy wszystkie 10 defektów, ustawiając testy w kolejności T″ niż
stosując kolejność T′. Miarę tę można zinterpretować geometrycznie. Na rysunku 12.1 przedstawiono miary dla T′ i T″ jako pola figur (oznaczone szarym kolorem) wyznaczonych funkcjami opisującymi, jak szybko zostaną wykryte nowe defekty w kolejnych testach. Numery testów są opisane na osi X. Z kolei na osi Y jest zaznaczona liczba wykrytych defektów. Dla zadanej kolejności funkcja jest zdefiniowana następująco: w zerze przyjmuje wartość zero, a w punkcie i przyjmuje wartość z punktu i – 1 powiększoną o liczbę nowych defektów znalezionych przez i-ty test. Na przykład, rysunek po lewej stronie odpowiada kolejności T′. Pierwszy test znalazł 3 nowe defekty, zatem w pierwszym kroku funkcja skacze o 3 jednostki do góry. Drugi test nie znalazł żadnego nowego defektu, zatem w drugim kroku funkcja nie zmienia swojej wartości. W trzecim teście znaleziono 4 nowe defekty, więc funkcja skacze o 4 jednostki do góry i tak dalej. Im szybciej funkcja osiągnie wartość 10 (równą liczbie wszystkich wykrytych przez suitę testową defektów), tym większe będzie pole pod jej wykresem. Można sprawdzić, że pole to jest dokładnie równe wartości metryki APFD.
Rysunek 12.1. Geometryczna interpretacja miary APFD
Mając dane historyczne lub z poprzedniego przebiegu testu regresji, możemy wykorzystać miarę APFD do priorytetyzacji przy użyciu prostego algorytmu zachłannego: w kolejnych krokach wybieramy ten test, który znajduje najwięcej „niepokrytych” dotąd defektów. Dzięki temu wzrost funkcji z rysunku 12.1 będzie najszybszy. W naszym przykładzie algorytm zachłanny zwróciłby kolejność T″. W takim przypadku moglibyśmy zdecydować o zredukowaniu suity testowej do testów PT3, PT5 i PT6, ponieważ trzy pozostałe nie wykrywają żadnych nowych defektów. Takie decyzje należy jednak zawsze podejmować z ostrożnością, gdyż może się okazać, że np. w wyniku wadliwych zmian w kodzie defekt mógłby być znaleziony tylko przez jeden z usuniętych testów.
12.3. Techniki priorytetyzacji Miara APFD jest tylko jedną z możliwych ocen priorytetyzacji. W literaturze przedmiotu (np. [142]) zdefiniowano wiele różnych kryteriów służących do ustalania kolejności testów, takich jak: kolejność losowa (służy głównie do porównań z innymi technikami); kolejność optymalizująca wykrywanie defektów (miara APFD); kolejność według pokrycia instrukcji; kolejność według pokrycia niepokrytych dotąd instrukcji; kolejność według prawdopodobieństwa wykrycia defektów (można wykorzystać technikę testowania mutacyjnego do oszacowania tzw. potencjału wykrywania usterek [143]); jw., ale dodatkowo uwzględniając dane z poprzednich przypadków testowych; kolejność według pokrycia funkcji; kolejność według niepokrytych dotąd funkcji; kolejność wykorzystująca taksonomię opartego na specyfikacji ([144]).
defektów
dla
testowania
i wiele innych. Eksperymenty pokazują, że stosowanie technik priorytetyzacji może efektywnie zredukować rozmiar suity testowej do ok. 70% jej początkowego rozmiaru bez dużej utraty mocy wykrywania defektów.
1 Ściśle rzecz biorąc, miara APFD przyjmuje wartości od do . Można ją jednak znormalizować tak, aby przyjmowała wartości od 0 do 1, tzn. zdefiniować .
Część III Testowanie charakterystyk jakościowych
Jakość nie jest działaniem, lecz nawykiem Arystoteles Be a yardstick of quality. Some people aren’t used to an environment where excellence is expected Steve Jobs
Część
trzecia
książki
jest
poświęcona
testowaniu
aspektów
jakościowych
oprogramowania. Atrybuty jakościowe są często pomijane w specyfikacjach, które ograniczają się głównie do wymagań biznesowych. Klient zwykle zwraca uwagę na aspekt funkcjonalności oprogramowania, ale jakość niezwiązana bezpośrednio z funkcjonalnością ma równie wielki wpływ na postrzeganie przez niego końcowej jakości produktu. Dlatego testowanie charakterystyk jakościowych (czyli tzw. testowanie niefunkcjonalne) jest tak samo istotne jak testowanie funkcjonalne. W rozdziale 13 przedstawimy krótko model jakości wg ISO 9126. Jest on punktem odniesienia dla wielu metod i technik zapewniania jakości. Od około 2010 roku jest zastępowany przez rodzinę norm ISO 25000, ale ze względu na jego historyczne znaczenie (a także fakt, że do modelu tego odwołują się sylabusy ISTQB) poświęcimy mu nieco miejsca. Rozdział 14 stanowi wprowadzenie w zagadnienia jakości oprogramowania. Przedstawione zostaną tu modele jakości proponowane przez normę ISO 25010. W kolejnych rozdziałach omówimy szczegółowo te modele. W rozdziale 15 skupimy się na jakości użytkowej, czyli – mówiąc ogólnie – na tym, jak użytkownik postrzega oprogramowanie jako całość. Rozdział 16 jest poświęcony omówieniu jakości produktu. To właśnie charakterystyki jakościowe produktu, takie jak wydajność, użyteczność czy niezawodność, są najczęściej kojarzone z pojęciem testowania niefunkcjonalnego. Na koniec, w rozdziale 17 omówimy charakterystyki jakościowe danych. Obecnie systemy informatyczne kładą bardzo duży nacisk na przetwarzanie danych, stąd potrzeba potraktowania ich jakości jako odrębnego zagadnienia.
13. Model jakości według ISO 9126
Norma ISO 9126 składa się z 4 części: ISO 9126-1: Quality model – opisująca model jakości w postaci charakterystyk jakościowych oprogramowania; ISO 9126-2: External metrics – opisująca tzw. metryki zewnętrzne, odnoszące się do jakości zewnętrznej, czyli wymagań na oprogramowanie wyprowadzonych z potrzeb użytkownika; ISO 9126-3: Internal metrics – opisująca tzw. metryki wewnętrzne, odnoszące się do jakości wewnętrznej, czyli wymagań technicznych na oprogramowanie; ISO 9126-4: Quality in use metrics – opisująca metryki dla jakości użytkowej, odnoszące się do: efektywności, produktywności, zabezpieczeń (ang. security) i bezpieczeństwa (ang. safety). Na rysunku 13.1 przedstawiono model jakości ISO 9126. Nie będziemy dokładnie omawiać modelu ISO 9126, ponieważ w następnym rozdziale opiszemy dokładnie jego następcę, model ISO 25000. Normy ISO 9126-2, 9126-3 i 9126-4 zawierają obszerny katalog metryk pozwalających ilościowo mierzyć charakterystyki jakościowe oprogramowania. Niektóre z tych metryk opiszemy w rozdziale 42.
Rysunek 13.1. Model jakości według ISO 9126
14. Modele jakości według ISO 25010
Testowanie charakterystyk jakościowych opiszemy na podstawie nowej rodziny norm ISO 25000, która zastępuje starą normę ISO 9126 [3]. Twórcy normy ISO 9126 inspirowali się tzw. modelem jakości McCalla [145]. Jest to chyba pierwszy model jakości w historii inżynierii oprogramowania. Jest przedstawiony na rysunku 14.1. Zanim przejdziemy do omówienia ISO 25010, opiszemy model McCalla, ze względu na jego historyczne znaczenie.
Rysunek 14.1. Model jakości McCalla Model jakości McCalla został stworzony dla Sił Powietrznych USA, aby wypełnić lukę między użytkownikami a twórcami systemów. Model stara się wiązać punkt widzenia użytkownika z priorytetami programistów. Definiuje 3 perspektywy charakteryzujące jakość tworzonego systemu: rewizję projektu (czyli zdolność do zmian w produkcie), przekazanie projektu (czyli możliwość adaptacji do nowego środowiska) oraz używanie projektu (związane z podstawowymi charakterystykami operacyjnymi). Każdej perspektywie jest przypisanych kilka czynników jakości (w sumie 11). Model ten, choć został zdefiniowany w 1977 roku, nie stracił na aktualności. Jak zobaczymy, idee zawarte w modelu McCalla są stale
obecne we wszystkich innych, bardziej współczesnych modelach jakości. Jest tak dlatego, że model McCalla opisuje w pewnym sensie uniwersalne cechy jakościowe systemów, które nie zmienią się niezależnie od postępu technologicznego. Norma ISO 25010 [2] wyróżnia dwa modele jakości (ang. quality), znane również w literaturze jako „duże Q” i „małe q”. Pierwszy dotyczy cech produktu istotnych z punktu widzenia użytkownika końcowego, stąd jego nazwa: jakość użytkowa (ang. quality in use). Opisuje jakość z punktu widzenia interakcji człowiek– komputer. Model drugi jest związany z jakością produktu z punktu widzenia procesu wytwórczego. Dotyczy „technicznych” cech produktu. Norma ISO 25010 nazywa ten typ jakości jakością produktu (ang. system/software product quality). Model jakości produktu odnosi się do statycznych własności oprogramowania i dynamicznych charakterystyk systemów komputerowych. Norma ISO 25012 [146] opisuje z kolei model jakości danych, dzieląc poszczególne charakterystyki dotyczące danych na dwie zachodzące na siebie grupy: wrodzone oraz zależne od systemu. Na rysunku 14.2 przedstawiono model jakości użytkowej, na rysunku 14.3 – model jakości produktu, a na rysunku 14.4 – model jakości danych.
Rysunek 14.2. Model jakości użytkowej według ISO 25010
Model jakości użytkowej opisuje pięć głównych charakterystyk, które mogą podlegać testowaniu: efektywność, wydajność, satysfakcję, wolność od ryzyka oraz kontekst charakterystyk w działaniu,
użycia.
Model jakości
produktu opisuje osiem
głównych
technicznych: funkcjonalną odpowiedniość, wydajność zgodność, użyteczność, niezawodność, bezpieczeństwo,
utrzymywalność oraz przenaszalność. Większość z charakterystyk
Rysunek 14.3. Model jakości produktu według ISO 25010
jest złożona z podcharakterystyk. W kolejnych rozdziałach opiszemy dokładnie wszystkie charakterystyki i podcharakterystyki, a także metody ich testowania. Model jakości danych nie jest rozłączny z modelami jakości w użyciu oraz jakości produktu. Jak widać, niektóre charakterystyki modelu jakości danych występują także w pozostałych modelach, np. wydajność, poufność czy dostępność. Jest tak dlatego, że jakość danych jest elementem jakości całego oprogramowania lub systemu. Niektóre cechy jakościowe właściwe danym (np. dokładność czy precyzja) mogą być traktowane jako składowa użyteczności lub funkcjonalnej odpowiedniości systemu.
Rysunek 14.4. Model jakości danych według ISO 25012 Model jakości służy testerowi do definiowania celów testowania (głównie niefunkcjonalnego). Można go traktować jak listę kontrolną charakterystyk podlegających weryfikacji, na podstawie których tworzy się kolejno testy sprawdzające poszczególne cechy systemu. Model jakości opisuje całościowo jakość oprogramowania bądź systemu komputerowego. Charakterystyki jakości
można mierzyć za pomocą metryk jakości. W części VII opiszemy przykładowe metryki dla różnych atrybutów jakościowych oprogramowania. atrybut
jakościowy,
charakterystyka
jakości
oprogramowania,
charakterystyka jakościowa, charakterystyka oprogramowania (ang. quality attribute, software quality characteristic, quality characteristic, software product characteristic) – cecha lub właściwość, która wpływa na jakość obiektu [7]
15. Testowanie jakości użytkowej
Jakość użytkowa jest cechą gotowego oprogramowania, dotyczy bowiem aspektów jakościowych interakcji człowiek–komputer. Dlatego testowanie jakości użytkowej ma zazwyczaj charakter testów akceptacyjnych (testy alfa, testy beta). Ze względu na to, że poziom jakości użytkowej jest odczuwany przez człowieka, testowanie jej charakterystyk odbywa się zwykle za pomocą kwestionariuszy i ankiet. W ramach jakości użytkowej można wyróżnić trzy podcharakterystyki: efektywność, wydajność oraz satysfakcję. Każda charakterystyka jakościowa produktu (patrz rozdz. 16) oraz danych (patrz rozdz. 17) ma wpływ na jakość użytkową.
15.1. Testowanie efektywności (effectiveness) Efektywność to dokładność, z jaką użytkownik osiąga za pomocą testowanego programu założone cele. Zauważmy, że nie chodzi tu o dokładność „techniczną” (np. dokładność obliczeń czy danych). Jest to cecha dotycząca celów użytkownika. Jeśli na przykład użytkownik systemu ELROJ (patrz Dodatek A) może wykonać wszystkie operacje opisane w wymaganiach R01–R07, to możemy uznać, że osiąga swoje cele z wysoką dokładnością, czyli oprogramowanie jest dla niego efektywne. Testowanie efektywności można przeprowadzić, używając obustronnego śledzenia testów do wymagań, które testy te pokrywają. efektywność (ang. efficiency) – zdolność procesu do tworzenia zamierzonych wyników w zależności od ilości użytych zasobów
15.2. Testowanie wydajności (efficiency) Wydajność jest to ilość zasobów, jakie wykorzystuje użytkownik w stosunku do dokładności, z którą osiąga założone cele. Przykładem takich zasobów może być czas do ukończenia zadania, materiały lub finansowy koszt użycia. Jeśli usunięcie kursu linii autobusowej w systemie ELROJ trwa średnio 10 sekund, to taki system jest wydajniejszy, niż gdyby trwało to 1 minutę, np. ze względu na konieczność „przeklikiwania się” przez aplikację. Jeśli komunikacja programu z ekranem na przystanku autobusowym wymaga dodatkowego sprzętu np. w postaci jakiegoś elektronicznego interfejsu, to wydajność związana z użyciem materiałów jest niższa niż gdyby system takiego interfejsu nie wymagał. Wydajność można testować, mierząc np. czas zużyty przez użytkowników na wykonanie określonych akcji czy zadań w programie. wydajność, charakterystyka czasowa (ang. performance, time behavior) – stopień, w jakim system lub moduł realizuje swoje wyznaczone funkcje w założonych ramach czasu przetwarzania i przepustowości [7]
15.3. Testowanie satysfakcji (satisfaction) Satysfakcja jest to stopień, w jakim potrzeby użytkownika są spełnione w wyniku użytkowania oprogramowania. Jest to cecha bardzo zindywidualizowana, tzn. każdy użytkownik może odczuwać inny poziom satysfakcji, korzystając z tego samego oprogramowania w taki sam sposób. Satysfakcję trudno zmierzyć, gdyż dotyczy ona indywidualnych osób. Można ją testować np. przez przeprowadzenie wśród użytkowników ankiet dotyczących różnych aspektów satysfakcji, takich jak: przydatność, zaufanie, przyjemność czy komfort.
15.3.1. Przydatność (usefulness) Przydatność to stopień satysfakcji użytkownika wypływającej z postrzeganego przez niego stopnia osiągnięcia celów. Cel dotyczy zwykle efektów działania programu i jest związany z konkretnymi funkcjonalnościami oferowanymi przez program.
15.3.2. Zaufanie (trust) Zaufanie to przekonanie o tym, że system będzie zachowywał się poprawnie. Na poziom zaufania wpływ ma wiele czynników, takich jak: odporność na błędy (jeśli użytkownik co chwilę otrzymuje komunikaty o błędach lub ostrzeżenia, jego zaufanie do oprogramowania będzie niskie), informowanie użytkownika o stanie systemu i aktualnie wykonywanych akcjach, możliwość odzyskania danych po awarii (np. dzięki funkcji autozapisu) itd. Wysoki poziom zaufania do oprogramowania objawia się tym, że użytkownik nie boi się wykonać określonych akcji w programie, bo wie („czuje”), że system czuwa nad bezpieczeństwem danych oraz całego procesu. Zaufanie jest kluczowym atrybutem w systemach o znaczeniu krytycznym, np. takich, jak oprogramowanie maszyny
Therac-25
(patrz
podrozdz.
1.3)
wykorzystywanej
do
terapii
nowotworów. Pacjent poddawany terapii musi być przekonany, że system będzie działał poprawnie i np. nie spowoduje omyłkowo podania śmiertelnej dawki promieniowania.
15.3.3. Przyjemność (pleasure) Przyjemność to uczucie radości z użytkowania oprogramowania. Być może najlepszym przykładem tej charakterystyki jest tzw. grywalność gier komputerowych. Wysokim poziomem przyjemności cechuje się np. bardzo popularna w ostatnich latach seria gier Angry Birds czy też gry sieciowe, takie jak Call of Duty. Przyjemność jest bardzo ważnym atrybutem jakości, bo to ona sprawia, że użytkownik ma ochotę używać danej aplikacji. W przypadku produktów z półki przekłada się to oczywiście na większą sprzedaż, lepsze wyniki finansowe i lepsze postrzeganie marki producenta.
15.3.4. Komfort (comfort) Komfort to przyjemność odczuwana z powodu użytkowania systemu. Pracując z dobrze zaprojektowanym programem o wysokim poziomie komfortu, użytkownik ma wrażenie, że system ten skonstruowano specjalnie dla niego; sposób jego użytkowania jest zgodny z intencją użycia. Zarówno komfort, jak i pozostałe opisane charakterystyki są funkcją całkowitej jakości produktu.
15.4. Testowanie wolności od ryzyka (freedom from risk) Testowanie wolności od ryzyka ma na celu wykrywanie awarii mogących skutkować zaistnieniem ryzyk określonego typu. Norma ISO 25010 wyróżnia trzy rodzaje ryzyk: ekonomiczne; związane ze zdrowiem i bezpieczeństwem; związane ze środowiskiem. Możliwe ryzyka definiuje się w fazie identyfikacji ryzyka (patrz podrozdz. 19.4), a jego wpływ na otoczenie – w fazie analizy ryzyka (podrozdz. 19.6). Testowanie wolności od ryzyka polega na wykonywaniu testów, które odnoszą się do poszczególnych ryzyk (co wiemy dzięki wykorzystaniu obustronnego śledzenia). Wykorzystuje się tu cały arsenał technik testowania, dobieranych w zależności od rodzaju i stopnia ryzyka. Dalej opiszemy krótko wspomniane rodzaje ryzyk. Metody ich łagodzenia są opisane w podrozdziale 19.7.
15.4.1. Ryzyko ekonomiczne (economic risk) Ryzyko ekonomiczne to każde ryzyko, którego zaistnienie powoduje straty ekonomiczne. Straty te najczęściej mają charakter finansowy, ale równie dobrze może to być np. utrata reputacji czy dobrego wizerunku firmy, co nierzadko może skutkować bankructwem.
15.4.2. Ryzyko dotyczące zdrowia i bezpieczeństwa (health and safety risk) Ryzyko dotyczące zdrowia i bezpieczeństwa występuje zwykle w systemach o znaczeniu krytycznym. Są to systemy, od których poprawnego działania zależy zdrowie lub życie ludzkie. Przykładem aplikacji o znaczeniu krytycznym może być np. oprogramowanie maszyny dawkującej promieniowanie w terapii nowotworów czy też systemy awioniki w lotnictwie. W przypadku wystąpienia ryzyk tego typu proces testowania powinien być o wiele bardziej restrykcyjny niż w innych systemach. Szczególnie istotną rolę odgrywają techniki statyczne (przeglądy i inspekcje) oraz techniki wykorzystujące mocne kryteria pokrycia (np. MC/DC, LSKiS).
bezpieczeństwo (ang. safety) – zdolność oprogramowania do osiągania akceptowalnego poziomu ryzyka wystąpienia szkody w stosunku do ludzi, biznesu, oprogramowania, majątku lub środowiska w określonym kontekście użycia [3]
15.4.3. Ryzyko związane ze środowiskiem (environmental risk) Wystąpienie ryzyka związane ze środowiskiem powoduje straty takie jak skażenie bądź zanieczyszczenie wody, gleby czy powietrza. Przykładem systemu, w którym takie ryzyko można zidentyfikować, jest oprogramowanie sterujące przesyłem gazu w gazociągu. Słynna awaria takiego oprogramowania była prawdopodobnie1 przyczyną wybuchu gazociągu na Syberii w 1982 roku.
15.5. Testowanie kontekstu użycia (context coverage) Pokrycie kontekstu to stopień, w jakim oprogramowanie może być użyte w określonym kontekście użycia bądź w innych niż oryginalnie zdefiniowany kontekstach użycia z odpowiednią efektywnością, wydajnością, satysfakcją i wolnością od ryzyka. Testowanie bada stopień spełnienia przez oprogramowanie charakterystyk jakościowych w różnych kontekstach użycia.
15.5.1. Zupełność kontekstu (context completeness) Zupełność kontekstu odnosi się do wszystkich zdefiniowanych kontekstów użycia. Przykładem zdefiniowanego kontekstu użycia może być sytuacja, w której używamy aplikacji na smartfonie, z niskim poziomem sygnału Wi-Fi, z małą ilością wolnej pamięci. Przykładami podobnych, ale odmiennych kontekstów może być obsługa przez system zarządzania pracami dyplomowymi prac magisterskich oraz prac licencjackich.
15.5.2. Elastyczność (flexibility) Elastyczność dotyczy kontekstów użycia niezdefiniowanych oryginalnie w wymaganiach. Przykładem takiego kontekstu może być np. używanie oprogramowania polskojęzycznego przez użytkowników anglojęzycznych.
Elastyczność umożliwia pełne wykorzystywanie oprogramowania z uwzględnieniem warunków i okoliczności nieprzewidzianych podczas projektowania systemu. Może być ona osiągnięta przez adaptację oprogramowania dla dodatkowych typów użytkowników bądź kultur. Przykładem braku elastyczności jest błąd w programie MS Word 2007 polegający na tym, że w odwołaniu do rysunku opisanego przez podpis z etykietą „Rysunek” nie można użyć samego numeru rysunku. Numer musi występować razem z nazwą etykiety. W języku angielskim nie stanowi to problemu, bo w tekście zawsze można napisać „[...] in Figure 5 [...]”, ale w języku polskim, gdzie występuje odmiana przez przypadki, nie zawsze chcemy użyć sformułowania „Rysunek 5”. Czasami chcielibyśmy napisać np. „Na Rysunku 5” (miejscownik zamiast mianownika). Projektanci Worda 2007 nie uwzględnili, że w niektórych językach występuje odmiana rzeczowników przez przypadki, dlatego oprogramowanie to nie jest elastyczne w kontekście użycia go przez polskojęzycznych użytkowników w spolszczonej wersji edytora tekstu (jest to błąd związany z internacjonalizacją).
1 Tak naprawdę do końca nie wiadomo, jaki był prawdziwy powód awarii. Jedna z teorii mówi, że w wykradzionym Kanadyjczykom przez Sowietów oprogramowaniu sterującym pracą gazociągu był defekt celowo zasiany przez wywiad USA (tzw. bomba logiczna).
16. Testowanie jakości produktu
Jakość produktu łączy w sobie kategorie tzw. wewnętrznej oraz zewnętrznej jakości (ang. internal quality, external quality) opisywane przez standard ISO 9126. Jakość wewnętrzna odnosi się do projektu oprogramowania. Grupą najbardziej zainteresowaną wysoką jakością wewnętrzną są deweloperzy. Cechy jakości wewnętrznej nie są zwykle bezpośrednio widoczne dla użytkownika. Jakość zewnętrzna to stopień, w jakim oprogramowanie spełnia swoje cele, czyli stopień wypełnienia wymagań użytkownika. Pojęcia jakości zewnętrznej i wewnętrznej nie mają ostrych granic. Na przykład, można sobie wyobrazić, że jednym z podstawowych wymagań użytkownika jest przenaszalność, tradycyjnie rozumiana jako cecha jakości wewnętrznej. Ponadto sprawę komplikuje fakt, że pojęcie „użytkownika” nie zawsze jest precyzyjnie określone. Można je rozumieć jako klienta końcowego, ale użytkownikiem może być też tester, dział utrzymania, firmy wykorzystujące oprogramowanie stworzone dla innych firm itp. Stąd cechy jakości wewnętrznej z punktu widzenia jednych użytkowników dla innych mogą stać się cechami jakości zewnętrznej. Niezależnie jednak od istniejących i charakterystyk jakości należy dbać o nią całościowo.
podziałów
16.1. Testowanie funkcjonalnej przydatności (functional suitability) Testowanie funkcjonalnej przydatności polega na sprawdzeniu, czy i w jakim stopniu oprogramowanie dostarcza funkcjonalności pozwalającej na wykonanie czynności lub spełnienie potrzeb zawartych w dokumencie wymagań. Należy pamiętać, że tworzenie aplikacji nigdy nie jest celem samym w sobie. Na końcu
całego procesu jest zawsze klient-użytkownik, dla którego oprogramowanie jest przeznaczone. Testowanie funkcjonalnej przydatności to sprawdzenie, jak bardzo to oprogramowanie jest przydatne dla użytkownika. funkcjonalność (ang. functionality) – zdolność oprogramowania do zapewnienia funkcji odpowiadających zdefiniowanym i przewidywanym potrzebom, gdy oprogramowanie jest używane w określonych warunkach [3] dopasowanie (ang. suitability) – zdolność oprogramowania do dostarczenia odpowiedniego zestawu funkcji dla określonych zadań i celów użytkownika [3]; patrz także: funkcjonalność W ramach funkcjonalnej przydatności można wyróżnić trzy podcharakterystyki: zupełność, poprawność oraz stosowność funkcjonalności.
16.1.1. Zupełność funkcjonalności (functional completeness) Zupełność funkcjonalności to stopień, w jakim zbiór oferowanych przez oprogramowanie funkcji pokrywa określone potrzeby użytkownika. Potrzeby te są zwykle opisane w dokumencie wymagań lub w formie historyjek użytkownika. Na przykład, dla programu ELROJ można wyróżnić następujące funkcjonalności: F01: możliwość zdefiniowania komunikatu (wymaganie R01); F02: możliwość usunięcia komunikatu (R01); F03: dodanie nowej linii do rozkładu jazdy (R02);
wyświetlanego
na
ekranie
F04: dodanie kursu do rozkładu jazdy danej linii (R03); F05: jednorazowe dodanie większej liczby kursów (R04); F06: możliwość usunięcia kursu (R05); F07: wyświetlenie kursów na ekranie (R06); F08: możliwość ustawienia daty na ekranie (R07). Stopień zupełności funkcjonalności można mierzyć jako liczbę pokrytych funkcjonalności spełniających potrzeby użytkownika w stosunku do wszystkich wymaganych funkcjonalności. Na przykład zakładając, że F01–F08 to wszystkie potrzebne użytkownikowi funkcjonalności, jeśli program ELROJ pozwalałby na
dodawanie wyłącznie pojedynczych kursów, funkcjonalność F05 nie byłaby pokryta. Wtedy funkcjonalność byłaby zupełna tylko w 7/8 = 87,5%. Tester mógłby zauważyć, że na liście tej brakuje funkcjonalności usunięcia całej linii z rozkładu (co wydaje się być sensownym wymaganiem z punktu widzenia użytkownika).
16.1.2. Poprawność funkcjonalności (functional correctness) Poprawność funkcjonalności to stopień, w jakim oprogramowanie dostarcza poprawny (prawidłowy) wynik. Jeśli na przykład wypisywanie danych na ekran wyglądałoby tak, jak na rysunku 16.1, to należałoby uznać, że funkcjonalność F07 jest błędna. Po pierwsze, komunikat jest wyświetlany w pierwszej, a nie ostatniej linii ekranu. Po drugie, niektóre czasy przybycia są podawane ze zbyt dużą dokładnością (wymagana jest dokładność do jednej minuty). Po trzecie, wyświetlane kursy nie są posortowane ze względu na czasy przybycia autobusów.
12.04.2013 LINIA
Czas przyjazdu
111
0,66666 min
112
45 min
113
5 min
110
58 min
KOMUNIKAT
Rysunek 16.1. Ekran programu ELROJ – przykład niepoprawnej funkcjonalności
16.1.3. Stosowność funkcjonalności (functional appropriateness) Stosowność funkcjonalności to stopień, w jakim funkcjonalność programu pomaga w osiągnięciu celów użytkownika. Rozważmy ponownie system ELROJ oraz funkcję dodawania nowych kursów. Jeśli aplikacja umożliwia dodanie wielu kursów za jednym razem, to funkcjonalność pomaga użytkownikowi osiągnąć
jego cel w większym stopniu niż gdyby umożliwiała jedynie wprowadzanie pojedynczych kursów. Różnica między zupełnością a stosownością jest taka, że zupełność dotyczy istnienia funkcji wykonującej określone zadanie, stosowność natomiast dotyczy jakości tego wykonania.
16.2. Testowanie wydajności w działaniu (performance efficiency) Testowanie wydajności bada oprogramowanie w dwóch obszarach: zachowania w czasie oraz zużycia zasobów. Kwestie dotyczące wydajności często są pomijane w dokumencie wymagań, tzn. są założone implicite, czyli niejawnie. Jeśli działanie testowanego programu bądź systemu polega na wykonywaniu prostych czynności, niewymagających wiele czasu czy zasobów, a ponadto nie ma potrzeby zachowania skalowalności, to testowanie wydajności może okazać się zbędne. Zwykle jednak program nie działa w próżni i najczęściej ma do czynienia np. z ładowaniem danych z zewnątrz oraz ich wewnętrznym przetwarzaniem. W takich sytuacjach, jeśli wymagania dopuszczają znaczące ilości danych lub wysokie natężenie użytkowania systemu (duża liczba równoległych użytkowników, krótki czas między zgłoszeniami itp.), to testowanie wydajności jest obowiązkowym typem testu, jaki należy przeprowadzić. Z testowaniem wydajności jest związany tzw. efekt próbnika1. Na przykład, jeśli mierzymy szybkość działania systemu, to aplikacja dokonująca pomiaru sama również zużywa zasoby systemu, powodując tym samym wolniejsze działanie analizowanej aplikacji. Poziom zmierzonej wydajności tego systemu będzie zatem nieznacznie mniejszy od rzeczywistego. efekt próbnika (ang. probe effect) – efekt wpływu elementu pomiarowego na moduł lub system podczas dokonywania pomiaru, np. przez narzędzie do testów wydajnościowych
16.2.1. Zachowanie w czasie (time behaviour) Można wyróżnić następujące metryki związane z zachowaniem w czasie:
czas odpowiedzi (ang. response time) – czas między wykonaniem polecenia rozpoczęcia transakcji a otrzymaniem odpowiedzi. Ma zastosowanie w systemach interaktywnych. Przykładem może być czas między kliknięciem przycisku „Złóż zamówienie” w sklepie internetowym, a otrzymaniem komunikatu potwierdzającego złożenie zamówienia. Czas odpowiedzi jest sumą czasu przetwarzania oraz czasu transmisji. Czasami czas transmisji jest pomijalny, ale np. w przypadku aplikacji sieciowych może mieć znaczący udział w czasie odpowiedzi. czas przetwarzania (ang. processing time) – czas między otrzymaniem przez system polecenia a zwróceniem wyniku. czas transmisji (ang. transmission time) – czas potrzebny na przesłanie komunikatu. czas operacji (ang. turn around time) – czas potrzebny na uzyskanie odpowiedzi na żądanie użytkownika. Zwykle składa się z wielu czasów odpowiedzi, gdyż dotyczy wielu różnych akcji. Na przykład wypłata pieniędzy z bankomatu jest operacją, na którą składają się czasy: od włożenia karty do prośby o PIN, od wpisania kodu PIN do wyświetlenia menu, od wyboru opcji wypłaty do wyświetlenia ekranu z kwotami, od wyboru kwoty do zwrócenia karty i od zwrócenia karty do wypłaty pieniędzy. Czas można mierzyć zarówno w standardowych jednostkach czasu, takich jak sekundy, minuty czy godziny, jak i w cyklach procesora. Czasami testera interesuje jeden, konkretny czas na wykonanie jakiejś operacji, a czasami jego wartość średnia (np. średni czas odpowiedzi bankomatu na wpisanie kodu PIN i naciśnięcie klawisza OK). Wymagania odnoszące się do zachowania w czasie muszą być precyzyjnie zdefiniowane. Nie należy stosować pojęć typu „czas akcji X powinien być krótki”, „system Y powinien działać szybko” itd. Każde odwołanie do czasu powinno być wyrażone w konkretnych wartościach liczbowych, np.: „generacja raportu tygodniowego nie powinna zajmować więcej niż 5 minut czasu przy normalnym obciążeniu systemu (40% obciążenia procesora)”.
16.2.2. Zużycie zasobów (resource utilization)
Zasoby używane przez systemy lub aplikacje to: pamięć; czas procesora; miejsce na dysku; urządzenia peryferyjne; zasoby ludzkie; zewnętrzne oprogramowanie; miejsce (np. powierzchnia zajmowana przez układ scalony z systemem wbudowanym na płytce obwodu drukowanego, powierzchnia zajmowana przez bankomat itp.). zużycie zasobów, składowanie danych (ang. resource utilization, storage) – zdolność oprogramowania do wykorzystania odpowiedniej ilości i typu zasobów, np. pamięci głównej i dodatkowej wykorzystywanej przez program oraz rozmiaru plików tymczasowych podczas działania oprogramowania w ustalonych warunkach [3] Testowanie zużycia zasobów może polegać na weryfikacji wymagań pod kątem: zużycia czasu procesora na wykonanie określonych funkcji; zużycia dostępnej pamięci na wykonanie określonej funkcji; poziomu wycieków pamięci (stopień wycieku w zależności od czasu i obciążenia systemu); obecności, odpowiedniości i dostępności zasobów ludzkich, urządzeń peryferyjnych, zewnętrznego oprogramowania, miejsca.
16.2.3. Pojemność (capacity) Pojemność to stopień, w jakim maksymalne limity parametrów systemu lub produktu spełniają wymagania. Przykładem może być wymaganie „system jest w stanie obsłużyć do 1000 klientów jednocześnie” lub „system może przechowywać dane o 100 milionach klientów”. Zwykle sam fakt możliwości osiągnięcia założonego stopnia pojemności jest niewystarczający – ta charakterystyka ma sens dopiero w połączeniu z dwoma poprzednimi, tzn. z zachowaniem systemu w czasie oraz ze zużyciem zasobów. Testowanie pojemności z uwzględnieniem
czasu i zużycia zasobów wykorzystuje specjalne techniki obciążania, przeciążania oraz weryfikacji skalowalności. Techniki te omówimy w kolejnym podrozdziale.
16.2.4. Techniki testowania wydajności Techniki
testowania
wydajności
polegają
na
generowaniu
obciążenia
i obserwowaniu jak system zachowuje się pod tym obciążeniem, pod kątem zarówno czasu działania, jak i zużycia zasobów. Testy wydajnościowe muszą być zautomatyzowane, gdyż dotyczą sytuacji, których wykonanie manualne byłoby bardzo trudne lub niemożliwe. Przykładem może być np. wygenerowanie bardzo dużej liczby użytkowników jednocześnie logujących się do systemu, symulowanie małej ilości dostępnej pamięci itp. Testowanie wydajności dla punktu odniesienia. Ten rodzaj testowania sprawdza zachowanie się systemu w normalnych warunkach (ang. baseline performance testing). Wyniki takiego testu służą jako punkt odniesienia dla innych testów, takich jak testowanie skalowalności. Pozwalają również określić średni czas działania i zużycie zasobów podczas normalnego użytkowania systemu, co może być wykorzystane w różnego rodzaju symulacjach. Testowanie obciążeniowe (ang. load testing). Mierzy zachowanie się programu przy wzrastającym obciążeniu, ale poziom obciążenia nie powinien przekraczać maksymalnej dopuszczalnej wartości przewidzianej w specyfikacji. Istnieje kilka rodzajów testów obciążeniowych, pokazanych na rysunku 16.2.
Rysunek 16.2. Przykłady testowania obciążeniowego Na diagramie (a) zilustrowano sytuację, w której testowany system został poddany dużemu, stałemu i niezmiennemu w czasie obciążeniu. Na diagramie (b) pokazano tzw. testowanie naciskowe (ang. peak load). Pik oznacza poziom maksymalnego obciążenia (np. natężenia ruchu na stronie internetowej, liczby jednoczesnych zgłoszeń do systemu). Maksymalne obciążenie stosuje się przez krótki czas, po czym poziom obciążenia spada do zera. Cykl ten jest powtarzany kilkakrotnie. Najciekawsze momenty zachowania się systemu to czas między pikami – ten typ testu sprawdza nie to, jak system radzi sobie z dużym obciążeniem, ale przede wszystkim jaka jest wydajność systemu przy gwałtownie zmieniającym się obciążeniu. Na diagramie (c) zilustrowano obciążenie schodkowe, w którym sukcesywnie zwiększa się poziom obciążenia. Ten typ testu może być wykorzystany w testowaniu skalowalności. Wyniki testu pozwalają na
stworzenie wykresu zależności wydajności systemu w funkcji obciążenia. Kształt funkcji pokazuje szybkość spadku wydajności wraz ze wzrostem obciążenia. Ekstrapolacja tej funkcji pozwala przewidzieć zachowanie systemu przy jeszcze większych obciążeniach, których np. nie byliśmy w stanie wygenerować z przyczyn technicznych2. Na diagramie (d) przedstawiono testowanie dla obciążenia narastającego. Jest ono podobne do obciążenia schodkowego, przy czym każdy cykl rozpoczyna się od coraz większego obciążenia. Na diagramie (e) opisano testowanie przy obciążeniu losowym. Jego rozkład najczęściej wynika z analizy profilu operacyjnego i przedstawia rzeczywisty, typowy rozkład obciążenia programu. Metoda ta pozwala przetestować program w symulowanych warunkach normalnego użytkowania systemu. Testowanie przeciążające (ang. stress testing). W swej idei jest ono podobne do testowania obciążeniowego, przy czym w testowaniu przeciążającym idziemy jeszcze dalej, obciążając system do momentu, w którym nie jest on w stanie poprawnie funkcjonować. Pozwala na znalezienie punktu krytycznego, tzn. maksymalnej wartości obciążenia, przy którym system działa na zadowalającym nas poziomie (np. odpowiada w odpowiednio krótkim czasie lub zużywa odpowiednią ilość zasobów). Testowanie skalowalności (ang. scalability testing). Testowanie skalowalności pozwala zbadać relację między wydajnością systemu a poziomem obciążenia. W zależności od kształtu funkcji opisującej ten związek można wnioskować o tym, na ile system jest skalowalny oraz tworzyć modele wydajności dla poziomów obciążeń niewykorzystanych w procesie testowania. skalowalność (ang. scalability) – zdolność oprogramowania rozbudowywanym w celu obsłużenia wzrastającego obciążenia [59]
do
bycia
Rozważmy przykład systemu rozpoznającego numer rejestracyjny samochodu na podstawie zdjęcia z fotoradaru. Wydajność systemu (definiowana jako czas między wysłaniem zdjęcia do systemu a zwróceniem numeru tablicy rejestracyjnej wraz z danymi o właścicielu pojazdu) zależeć będzie od liczby aktualnie przechowywanych danych o właścicielach pojazdu. Tester przygotował pięć sztucznie wygenerowanych baz danych zawierających odpowiednio 102, 103, …, 106 zdjęć. Następnie, dla każdej bazy wykonał 100 zapytań przez wysłanie stu różnych zdjęć pojazdów. Na rysunku 16.3 pokazano wykres średnich wartości czasu odpowiedzi dla stu zapytań dla każdej z sześciu baz. Skale na obu osiach są
wykładnicze, a zależność między rozmiarem bazy a czasem odpowiedzi jest również wykładnicza. Oznacza to, że system źle się skaluje. Oczekiwalibyśmy przynajmniej zależności liniowej (a najlepiej subliniowej). Na podstawie danych z rysunku 16.3 można zbudować model, który pozwoli przewidzieć czas odpowiedzi systemu dla baz o rozmiarach większych niż 106. Stosując np. metodę regresji nieliniowej dla modelu wykładniczego, moglibyśmy na podstawie danych z rysunku otrzymać zależność t(n) = 41 ⋅ (1,005)0,001 ⋅ n gdzie n jest liczbą rekordów w bazie, a t(n) – średnim czasem odpowiedzi dla bazy o rozmiarze n. Wtedy, dla n = 107 otrzymalibyśmy t(n) = 41 ⋅ (1,005)10000 ms ≈ 1,87 ⋅ 1023 ms, co oznacza, że praktycznie baza nie będzie w stanie działać mając więcej niż 1–2 miliony rekordów.
Rysunek 16.3. Przykład złej skalowalności systemu
16.3. Testowanie zgodności (compatibility) Zgodność to stopień, w jakim testowany produkt, system lub moduł może wymieniać informacje z innymi produktami, systemami lub modułami dzieląc jednocześnie to samo środowisko. Przez środowisko należy rozumieć zarówno sprzęt, jak i oprogramowanie. Jako specjalny przypadek należy wyróżnić aplikacje internetowe, które muszą współdziałać z różnymi przeglądarkami. Niezależnie od typu przeglądarki, aplikacja zawsze powinna wyglądać i zachowywać się tak samo.
16.3.1. Współistnienie (co-existence) Współistnienie to stopień, w jakim testowany produkt może wykonywać swoje funkcje, dzieląc środowisko oraz zasoby z innymi aplikacjami nie wpływając negatywnie na te aplikacje. Na rysunku 10.7 zilustrowano przykład współistnienia: testowany program oraz współistniejące aplikacje nie są bezpośrednio ze sobą związane – nie muszą się ze sobą w ogóle komunikować. Jednak wszystkie te aplikacje mogą wykorzystywać np. tę samą bazę danych lub ten sam system plików. Niewłaściwe działanie testowanego programu powodujące błędy w bazie lub w systemie plików może wpływać na błędne działanie innych programów wykorzystujących te same zasoby. współistnienie, koegzystencja (ang. co-existence) – zdolność produktu oprogramowania do działania z innym, niezależnym oprogramowaniem we wspólnym środowisku, podczas dzielenia wspólnych zasobów [3]
16.3.2. Współdziałanie (interoperability) Współdziałanie to stopień, w jakim testowany produkt może wymieniać informacje z innymi programami i używać tych informacji. Przykładem może być urządzenie pamięci masowej USB, które współpracuje z każdym współczesnym komputerem wyposażonym w port USB. Jest tak dlatego, że wymiana danych przez port USB odbywa się według ściśle określonego standardu. Należy jednak pamiętać, że współdziałanie zachodzi niekoniecznie dlatego, że jest
spełniony jakiś z góry narzucony standard. Może być ono zapewnione indywidualnie, np. dla konkretnych dwóch programów. Metoda wymiany informacji nie musi być zgodna z żadnym istniejącym standardem. współdziałanie
(ang.
interoperability)
–
zdolność
oprogramowania
do
współdziałania z jednym lub większą liczbą modułów lub systemów [3] Współdziałanie dotyczy charakterystyki obejmuje
przepływu danych, zatem testowanie tej swym zakresem testowanie protokołów
komunikacyjnych. Wymiana informacji może następować przez wywoływanie odpowiednich funkcji API testowanego programu, dlatego testowanie współdziałania ma zazwyczaj charakter testów integracyjnych.
16.3.3. Przykład Testowanie współdziałania między programami rodziny MS Office 2000 a Open Office może polegać na następujących czynnościach: wygenerowanie i wydrukowanie dokumentu w programie MS Word, arkusza kalkulacyjnego w MS Excel i prezentacji w MS PowerPoint; otworzenie i wydrukowanie tych plików w programach Open Office; porównanie plików i wydruków z obu wersji programów; dokonanie zmian w dokumentach otworzonych w Open Office, wczytanie ich w programach MS Office i porównanie. W powyższym procesie testowania mamy do czynienia z wymianą danych, którymi są: dokument tekstowy, arkusz kalkulacyjny oraz prezentacja. Należy zweryfikować zgodność możliwie jak największej liczby funkcji i operacji, jakie można dokonywać w tych plikach. W szczególności, dla dokumentu tekstowego należy sprawdzić współdziałanie pod kątem zachowania się następujących cech tekstu: typy, kroje i wielkości czcionek; akapity, interlinie, paginacja, stopki, nagłówki, marginesy; osadzone rysunki, tabele, tryb wielokolumnowy; odsyłacze, podpisy pod rysunkami; eksport dokumentu do PDF;
numeracja stron, inne pola specjalne (np. data); komentarze, śledzenie zmian; ochrona dokumentu; równania matematyczne itd. W przypadku arkusza kalkulacyjnego: typy, kroje i wielkości czcionek; ukryte kolumny; format danych w komórkach; odwołania między komórkami; funkcje matematyczne, statystyczne, operacje na tekstach itd.; wykresy; eksport dokumentu do PDF; zaznaczanie obszaru drukowania itd. W przypadku prezentacji: typy, kroje i wielkości czcionek; osadzone rysunki, clipart; animacje pokazu slajdów; elementy interaktywne (przyciski sterowania); osadzone pliki muzyczne (mid, wav, mp3) i wideo (avi, mpeg); wzorce slajdów itd.
16.4. Testowanie użyteczności (usability) Użyteczność to stopień, w jakim oprogramowanie może być używane przez określonych użytkowników dla osiągnięcia założonych celów. Innymi słowy, jest to charakterystyka opisująca jak użyteczne dla klienta jest oprogramowanie. W punktach 16.4.1–16.4.6 opiszemy podcharakterystyki użyteczności, natomiast w podrozdziale 16.5 podamy przykłady konkretnych wskazówek (tzw. heurystyki Nielsena) mogących pomóc w tworzeniu systemów o wysokiej użyteczności. Istnieją trzy główne techniki oceny użyteczności [93]:
Inspekcja. Jeśli dysponujemy prototypami lub makietami interfejsu, inspekcja może przybrać formę przeglądu dokonywanego przez użytkowników. Inspekcja może być przeprowadzona również na podstawie listy kontrolnej zawierającej znane aspekty oprogramowania wpływające na poziom użyteczności. Ta technika pozwala wykrywać błędy użyteczności na wczesnych poziomach testowania. Walidacja implementacji. Polega ona na przeprowadzeniu testów użyteczności według założonych scenariuszy testowych. Różni się od klasycznego testowania co najmniej w dwóch kwestiach. Po pierwsze, proces testowania bardziej niż na cechy funkcjonalne zwraca uwagę na takie atrybuty, jak szybkość uczenia się, zrozumiałość, efektywność użycia oprogramowania itp. Po drugie, przed testami i po testach użytkownicy, którzy je przeprowadzają są pytani o oczekiwania oraz wrażenia
z
użytkowania
programu.
Proces
przepytywania
ma
najczęściej formę wywiadu. Przed testowaniem użytkownicy otrzymują ponadto instrukcje opisujące, jak przeprowadzić test. Instrukcje mogą zawierać informacje o tym, jakie czynności należy wykonać, w jakim czasie należy się zmieścić, a nawet, jak krok po kroku wykonać zadanie. Kwestionariusze (ankiety). Kwestionariusze służą do zbierania od użytkowników informacji na temat używanego oprogramowania. Ankietując większą liczbę użytkowników, dostajemy bardziej miarodajną ocenę użyteczności. Istnieją ogólnie dostępne ankiety, takie jak SUMI (Software Usability Measurement Inventory) czy WAMMI (Website Analysis and Measurement Inventory). Wykorzystując tego typu standardowe ankiety, organizacje mogą porównać wyniki testów użyteczności dla różnych produktów lub ich wersji. SUMI jest kwestionariuszem zawierającym 50 pytań. Na każde z nich należy odpowiedzieć „zgadzam się”, „nie zgadzam się” lub „nie wiem”. Oto przykładowe pytania: ten program odpowiada zbyt wolno na dane wejściowe; nauka używania aplikacji na początku sprawia dużo problemów; dokumentacja jest dokładna, wyczerpująca i przydatna; nie chciał(a)bym używać tego oprogramowania na co dzień.
Po zebraniu danych są one kodowane, scalane i przekształcane na tzw. skalę globalną oraz pięć dodatkowych skal wyrażających wydajność, wpływ (ang. affect), pomocność, kontrolowalność i łatwość nauki. Wyniki są skalowane tak, że średni wynik każdej z tych skal w populacji ma wartość 50 z odchyleniem standardowym 10. Wynik testów własnego oprogramowania można więc porównać ze średnią przemysłową i zobaczyć, jak nasz produkt wypada na tle innych. Rezultaty pomiarów są również pomocne w określeniu obszarów, które wymagają usprawnienia z punktu widzenia użyteczności. WAMMI to metoda podobna do SUMI, przy czym dotyczy użyteczności stron internetowych. Jest to kwestionariusz złożony z 20 pytań, na które odpowiada użytkownik po wizycie na stronie. Główną częścią wyniku analizy jest tzw. profil strony, składający się z oceny pięciu charakterystyk: atrakcyjności, kontrolowalności, wydajności, pomocności i łatwości nauki. Wynik zawiera także ocenę łączną. Wszystkie te metryki są tak przeskalowane, że średnia wynosi 50, a ocena maksymalna to 100. Raport z badań zawiera również inne elementy, takie jak: spriorytetyzowane cechy strony według konieczności ich poprawy; analizę dodatkowych pytań z zamkniętymi odpowiedziami; indywidualne profile użytkowników strony itp. Na rysunku 16.4 jest przedstawiony przykładowy wynik analizy WAMMI dla pewnej strony internetowej. Jak widać, strona działa wydajnie, jest pomocna dla użytkownika i łatwa w obsłudze (te trzy wyniki są powyżej średniej), natomiast istnieje problem z jej atrakcyjnością i kontrolowalnością. Te dwie charakterystyki jakościowe są pierwszymi kandydatami do poprawy.
Rysunek 16.4. Przykładowy wynik analizy WAMMI (źródło: www.wammi.com/whatis.html) inwentarz analizy i pomiaru stron internetowych (ang. Website Analysis and MeasureMent Inventory, WAMMI) – oparta na kwestionariuszach technika pomiaru użyteczności stron internetowych, służąca do pomiaru jakości stron internetowych z punktu widzenia użytkownika inwentarz pomiarów użyteczności oprogramowania (ang. Software Measurement Inventory, SUMI) – oparta na kwestionariuszach technika stosowania użyteczności do pomiarów jakości oprogramowania z punktu widzenia końcowego użytkownika
16.4.1. Zrozumiałość (appropriateness recognizability) Zrozumiałość jest cechą
mierzącą
stopień, w jakim użytkownik potrafi
rozpoznać, czy testowany system, program lub komponent jest odpowiedni dla jego potrzeb. Charakterystyka ta wynikać może z dwóch źródeł: pierwszego wrażenia, jakie produkt wywarł na użytkowniku oraz z dokumentacji użytkowej. Zrozumiałość jest zwiększana przez dołączenie do programu demonstracji, przykładowych
danych
wejściowych,
przewodników
i
innych
tego
typu
informacji pozwalających użytkownikowi na szybką ocenę, czy program będzie w stanie pomóc mu w rozwiązaniu jego problemu. Testowanie zrozumiałości może sprawdzać ilość i jakość pomocniczych informacji dołączonych do aplikacji. zrozumiałość (ang. understandability) – zdolność oprogramowania do umożliwienia użytkownikowi zrozumienia, czy jest ono odpowiednie i jak może być użyte do realizacji określonych zadań [3]
16.4.2. Łatwość nauki (learnability) Łatwość nauki to stopień, w jakim użytkownik może opanować zasady użytkowania oprogramowania. Aplikacje o wysokiej łatwości nauki mogą być efektywnie używane bez jakiegokolwiek wstępnego szkolenia. W przypadku aplikacji o niskiej łatwości nauki użytkownik może mieć wrażenie, że program używa nieznanych mu pojęć lub metod i odczuwa brak niezbędnych informacji potrzebnych do używania oprogramowania. łatwość nauki (ang. learnability) – zdolność oprogramowania do wspierania użytkownika w procesie nauki użycia [3]; patrz także: użyteczność
16.4.3. Łatwość użycia (operability) Łatwość użycia to stopień, w jakim oprogramowanie umożliwia użytkownikowi intuicyjne korzystanie z aplikacji. W testowaniu łatwości użycia weryfikuje się m.in. następujące cechy oprogramowania: spójność (w tym spójność interfejsu polegająca na używaniu takiego samego stylu graficznego, układu menu, czcionek itp. w różnych
modułach aplikacji oraz spójność terminologii, podobnych określeń w podobnych sytuacjach); jasne i zrozumiałe komunikaty, zarówno i ostrzegawcze oraz komunikaty o błędach;
czyli
używanie
informacyjne,
jak
możliwość cofania wprowadzonych zmian (im więcej zmian da się cofnąć, tym lepiej); możliwość
konfigurowania
oprogramowania
do
indywidualnych
potrzeb użytkownika (ang. customization), np. możliwość organizacji pulpitu, ustalania kolorystyki, używania zakładek, dodawania wtyczek (w przeglądarkach). łatwość użycia (ang. operability) – zdolność oprogramowania do zapewnienia użytkownikowi możliwości jego obsługi i kontroli [3]; patrz także: użyteczność
16.4.4. Ochrona przed błędami użytkownika (user error protection) Ochrona przed błędami użytkownika to stopień, w jakim system chroni użytkownika przed popełnianiem błędów. Popularnymi rozwiązaniami tego typu są tzw. techniki poka-yoke, czyli metody zapobiegające powstawaniu defektów. Mogą to być zarówno rozwiązania sprzętowe, jak i software’owe. W tabeli 16.1 pokazano przykłady dobrych i złych rozwiązań pod kątem zapobiegania defektom. Należy odróżnić charakterystykę ochrony przed błędami użytkownika od tolerancji na błędy, która jest podcharakterystyką niezawodności, opisaną w punkcie 16.6.2. Pierwsza dotyczy działań użytkownika, natomiast druga – działań samego oprogramowania, które mają zapewnić poprawność działania systemu w sytuacji nastąpienia błędu bądź awarii. Tabela 16.1. Ochrona przed błędami użytkownika – przykłady dobrych i złych rozwiązań
Poziom s przętowy
Przykład dobrego rozwiązania
Przykład złego rozwiązania
wtyczka US B, którą – dzięki jej kons trukcji – da s ię włożyć do
płyta CD, którą można włożyć do
g niazda US B tylko na jeden s pos ób Poziom okno wyboru daty za pomocą s oftware’owy zaznaczania dnia w kalendarzu
czytnika na dwa s pos oby okno teks towe umożliwiające ręczne wpis anie daty
16.4.5. Estetyka interfejsu użytkownika (user interface aesthetics) Estetyka interfejsu użytkownika pozwala na zwiększenie (lub zmniejszenie) odczuwania przez użytkownika satysfakcji z użytkowania oprogramowania. Na estetykę interfejsu mają wpływ takie czynniki jak: dobór kolorystyki elementów GUI; dobór kroju czcionki oraz jej rozmiaru; forma graficzna komponentów GUI; układ elementów na ekranie, logika ich umiejscowienia oraz ich zagęszczenie; animacje podczas wykonywania przez użytkownika akcji (np. kliknięcie przycisku, przesunięcie palcem po ekranie dotykowym, zaznaczenie opcji powodujące pojawienie się dodatkowego panelu).
16.4.6. Dostęp (accessibility) Dostęp to stopień, w jakim oprogramowanie może być używane przez ludzi o różnych cechach psychofizycznych. W szczególności dotyczy to ludzi z różnymi poziomami niepełnosprawności. Testowanie dostępu polega na weryfikacji, czy produkt jest dostosowany do odpowiedniej grupy użytkowników. Na przykład, jeśli oprogramowanie ma być dostosowane dla osób niedowidzących, to powinno umożliwiać wykorzystanie kontrastowych kolorów oraz powiększenie rozmiaru czcionki. Dostosowanie do potrzeb osób niewidomych może np. wymagać, aby komunikaty wyświetlane przez program mogły być odczytane przez specjalistyczne oprogramowanie przetwarzające tekst pisany na głos. Przykładem oprogramowania dbającego o wysoki poziom dostępu jest system operacyjny Windows. Ma on takie narzędzia jak lupa (powiększająca fragment
ekranu), czy tzw. funkcję klawiszy trwałych, dzięki której skróty klawiaturowe takie jak CTRL+ALT+DEL czy CTRL+S można wykonywać, naciskając te klawisze pojedynczo. Przykładem standardu opisującego wymagania dotyczące dostępu jest Rehabilitation Act [147], a dokładniej jego część, Section 508, opublikowana przez U.S. Environmental Protection Agency. Opisuje ona wymagania dla następujących grup produktów i systemów: aplikacje oraz systemy operacyjne; aplikacje internetowe; produkty telekomunikacyjne; produkty video i multimedialne; systemy wbudowane; komputery przenośne i desktopowe.
16.5. Heurystyki dotyczące użyteczności Efektywność, wydajność i satysfakcja opisują użyteczność systemu z punktu widzenia użytkownika. Tworzenie użytecznych systemów jest trudne – nie ma zbioru sztywnych reguł opisujących krok po kroku konkretne akcje, jakie należy wykonać, aby system był efektywny i wydajny, a użytkownik odczuwał satysfakcję z jego użycia. Dlatego zaproponowano wiele różnego rodzaju heurystyk pozwalających zwiększyć użyteczność systemów. Jednymi z najbardziej znanych są tzw. heurystyki Nielsena ([148], [149], [150]). Oprócz nich, z ważniejszych zasad można wyróżnić: 18 kryteriów ergonomii Bastiena–Scapina [151], 10 reguł inżynierii kognitywnej Gerhardta–Powalsa [152], klasyfikację Weinschenka–Barkera [153], 30 zasad użyteczności Connella–Hammonda [154] czy 944 wskazówki dla projektantów interfejsów użytkownika Smitha–Mosiera [155].
16.5.1. Heurystyki Nielsena Jakob Nielsen sformułował w 1990 roku 10 heurystyk dotyczących użyteczności opisanych poniżej. Heurystyki te można wykorzystać jako swoistą listę kontrolną do testowania efektywności, wydajności i satysfakcji. Omówimy je teraz po kolei.
ocena
heurystyczna
(ang.
heuristic
evaluation)
–
technika
statycznego
testowania użyteczności mająca na celu określenie zgodności interfejsu użytkownika lub jego projektu; wykorzystując tę technikę, przeglądający sprawdzają
interfejs
i
oceniają
jego
zgodność
z
uznanymi
zasadami
użyteczności (tzw. heurystykami) 1. Pokazuj status systemu. Użytkownik powinien być na bieżąco informowany o tym, w jakim miejscu systemu się znajduje, gdzie może pójść dalej, skąd przyszedł oraz co system robi w danym momencie.
Rysunek 16.5. Informowanie użytkownika o stanie systemu Na rysunku 16.5 jest pokazany panel użytkownika konta w banku internetowym. Widać zaznaczoną opcję „Moje Finanse” oraz pod spodem podkreśloną i pogrubioną opcję „Rachunki”, a jeszcze niżej podopcję „Twoje rachunki”. Użytkownik dokładnie wie, w którym miejscu systemu się znajduje. Często wykorzystuje się również tzw. okruszki (ang. breadcrumbs), pokazujące ścieżkę, którą przeszedł użytkownik, np.: Jesteś tutaj: Strona główna → Sklep → Książki → Komiksy. Nazwy linków (hiperłącz) prowadzących do podstron lub innych części systemu powinny być zgodne z tytułami tych podstron lub części systemu. Klikalne elementy powinny być wyraźnie oznaczone, aby wiadomo było, że są odnośnikami. hiperłącze (ang. hyperlink) – wskaźnik w dokumencie elektronicznym, który stanowi odwołanie do innego dokumentu elektronicznego
Gdy system przetwarza jakieś zadanie i operacja trwa nie dłużej niż kilka sekund, nie ma konieczności informowania użytkownika o tym, co się aktualnie dzieje. Jeśli jednak akcja trwa dłużej niż kilka sekund, to tok myślowy użytkownika może zostać przerwany, a jego irytacja wzrośnie. Dlatego w takich sytuacjach system powinien wyświetlać komunikaty opisujące na bieżąco, jakie akcje system wykonuje. 2. Zachowaj zgodność między systemem a rzeczywistością. Zasada ta najczęściej jest rozumiana w ten sposób, że system powinien posługiwać się językiem zrozumiałym dla użytkownika oraz adekwatnym do rzeczywistości.
Rysunek 16.6. Brak zgodności między systemem a rzeczywistością Na rysunku 16.6 jest przedstawiony zrzut ekranu ze strony sklepu internetowego luluforkids.pl. Pomijając fakt użycia całkowicie nieczytelnej czcionki oraz źle dobranej kolorystyki, strona używa pojęcia „wózka” zamiast powszechnie używanego „koszyka” kojarzącego się z zakupami. Akurat w tym przypadku jest to sklep z artykułami dla dzieci, więc odwołanie się do wózka dziecięcego zamiast koszyka może być celowe, jednak w ogólności należy uznać to za przejaw złej użyteczności. 3. Daj użytkownikowi pełną kontrolę. Użytkownik powinien mieć zapewnioną kontrolę oraz jak największą swobodę nad wszystkimi akcjami, które wykonuje. Na rysunku 16.7 przedstawiono stronę sklepu internetowego merlin.pl w chwili wyświetlania szczegółów towaru. Użytkownik ma w tym momencie do wyboru wiele możliwych opcji: może wrócić na stronę główną, klikając logo w lewym
górnym rogu, zalogować się na swoje konto, zobaczyć stan koszyka, rozwinąć szczegółowy opis towaru, dodać towar do koszyka itd. Wszystkie te akcje może wykonać z tego samego ekranu („opis produktu”).
Rysunek 16.7. Kontrola użytkownika nad systemem 4. Trzymaj się standardów i zachowaj spójność. System powinien zachowywać spójność formy elementów graficznych, kolorystyki, ikon itd. Aplikacja powinna używać jednego kroju czcionki; wszystkie łącza powinny wyglądać w ten sam sposób; w nowej wersji oprogramowania (lub w obrębie rodziny aplikacji) ikony oznaczające te same akcje powinny wyglądać tak samo; wszystkie komunikaty powinny mieć tę samą formę graficzną, itd.
Rysunek 16.8. Spójność interfejsów w obrębie rodziny produktów MS Office Na rysunku 16.8 są przedstawione paski narzędzi (tzw. wstążki) trzech programów z rodziny MS Office 2007: MS PowerPoint, MS Excel oraz MS Word. Ikony oznaczające te same akcje wyglądają dokładnie tak samo. Jest również zachowana spójność struktury menu oraz kolorystyki. 5. Zapobiegaj błędom. Aplikacja powinna w możliwie jak największym stopniu zapobiegać potencjalnym błędom użytkownika przez prowadzenie z nim odpowiednio opracowanej komunikacji. Należy stosować odpowiednie pola dialogowe minimalizujące ryzyko popełnienia pomyłki. Na przykład, jeśli użytkownik jest proszony o podanie daty wylotu, nie powinien wpisywać tej daty „z palca”, ale raczej wybrać ją przy użyciu kalendarza; jeśli należy wpisać swój numer PESEL, okno dialogowe powinno umożliwiać wpisanie wyłącznie liczb (dokładnie jedenastu) itd. Na rysunku 16.9 przedstawiono zrzut ekranu ze strony skyscanner.pl. Użytkownik wybiera datę wylotu z wbudowanego kalendarza, dzięki czemu system nie będzie miał problemów z rozpoznaniem formatu daty. Nie wystąpi również ryzyko wpisania błędnej daty (np. 32-15-1983 czy 29 lutego w roku nieprzestępnym).
Rysunek 16.9. Zapobieganie błędom: okno kalendarza zamiast pola na ręczne wpisanie daty 6. Pozwalaj wybierać zamiast zmuszać do zapamiętywania. Wszystkie niezbędne do podjęcia decyzji informacje powinny być widoczne na ekranie. Tam, gdzie to możliwe, należy stosować rozwijalne listy wyboru zamiast zmuszać użytkownika do wpisywania wartości z klawiatury. Jeśli należy wskazać ścieżkę w drzewie katalogów, lepiej umożliwić użytkownikowi nawigację po tym drzewie zamiast zmuszać do wpisywania ścieżki z klawiatury. Użytkownik nie powinien zapamiętywać jakichkolwiek informacji przy przechodzeniu z jednego okna dialogowego do drugiego. Instrukcje dotyczące użycia poszczególnych części systemu powinny być łatwo widoczne lub dostępne.
Rysunek 16.10. Wybór lokalizacji zamiast ręcznego wpisywania ścieżki Na rysunku 16.10 przedstawiono dobrze znane okno wyboru lokalizacji. Dzięki jego wykorzystaniu użytkownik nie musi pamiętać ścieżki do danej lokalizacji. 7. Zapewnij elastyczność i efektywność użycia. System powinien oferować metody przyspieszające pracę z systemem, np. skróty klawiszowe. Powinien pozwalać również na dopasowanie przez użytkownika sposobu wykonywania typowych zadań. Akceleratory, czyli opcje pozwalające na bardziej zaawansowane użytkowanie systemu przez doświadczonych użytkowników nie powinny być widoczne dla użytkowników początkujących. Ma to związek z tzw. jakością odwrotną (ang. reverse quality) występującą w modelu Kano (patrz podrozdz. 38.2).
Rysunek 16.11. Zaawansowane opcje niewidoczne dla początkującego użytkownika Początkujący użytkownik wyszukiwarki Google widzi przed sobą prosty ekran, złożony z pola, w które może wpisać słowo lub frazę do wyszukania (rys. 16.11 po lewej). Użytkownik zaawansowany może przełączyć się na widok zaawansowany (rys. 16.11 po prawej), który pozwala mu na efektywne konstruowanie złożonych zapytań. 8. Dbaj o estetykę i umiar (ang. minimalistic design). Okna dialogowe nie powinny zawierać informacji nadmiarowych, irrelewantnych do aktualnej sytuacji lub zwyczajnie niepotrzebnych. Interfejs użytkownika powinien być czytelny i przejrzysty, nieprzeładowany funkcjami. Poszczególne elementy interfejsu powinny być łatwo identyfikowalne. Kolorystyka powinna być dostosowana do charakteru aplikacji. Należy unikać stosowania „gryzących się” kolorów i wyszukanych krojów czcionek.
Rysunek 16.12. Prosty i estetyczny interfejs elektronicznego kalkulatora Na rysunku 16.12 jest pokazany interfejs działającej na smartfonie aplikacji typu elektroniczny kalkulator. Jak widać interfejs nie jest przeładowany żadnymi nadmiarowymi opcjami, nazwy przycisków są czytelne, a kolorystyka –
stonowana. Bieżący wynik działania jest wyświetlany – tak jak w przypadku rzeczywistych kalkulatorów – na górze ekranu, ponad panelem z przyciskami, czytelną czcionką o dużym rozmiarze i tym samym kroju, co etykiety przycisków. 9. Zapewnij skuteczną obsługę błędów. Błędy nie powinny się zdarzać, lecz nie da się ich niestety uniknąć. Dlatego należy jak najbardziej zminimalizować ich negatywne oddziaływanie na system i na użytkownika. Komunikaty o błędach powinny być zrozumiałe i pisane bez użycia jakichkolwiek kodów bądź innych niezrozumiałych oznaczeń. Powinny dokładnie opisywać problem oraz sugerować sposób jego rozwiązania. Na rysunku 16.13 pokazano cztery złe przykłady komunikatów o błędach. Komunikat o błędzie w Excelu („Nie można wyjść z programu Microsoft Office Excel”) w ogóle nie informuje użytkownika o powodach jego wystąpienia i nie tłumaczy, dlaczego nie można zamknąć aplikacji. Komunikat o błędzie w serwisie iTunes mówi wprost, że wystąpił „nieznany błąd”. Tak samo nieinformacyjny jest komunikat o błędzie na stronie facebook.com. Z kolei komunikat o błędzie w jednej z bibliotek Microsoft Visual C++ zawiera kody trudne do rozszyfrowania przez zwykłego użytkownika. Żaden z tych komunikatów nie proponuje użytkownikowi efektywnego sposobu enigmatycznym „spróbuj później”).
rozwiązania
Rysunek 16.13. Przykłady złych komunikatów o błędach
problemu
(poza
10. Dostarcz system pomocy oraz dokumentację. Dokumentacja oraz wbudowana pomoc powinny pozwolić na szybkie znalezienie potrzebnej informacji. Język dokumentacji i pomocy musi być zrozumiały dla użytkownika, nie może zawierać żargonu technicznego.
16.5.2. Laboratorium badania użyteczności (usability lab) Aby jak najlepiej zbadać użyteczność, testowanie tej charakterystyki oprogramowania przeprowadza się często w tzw. laboratoriach użyteczności. Są to specjalne pomieszczenia, najczęściej złożone z dwóch części. W jednej znajduje się stanowisko użytkownika. Część ta powinna wyglądać możliwie podobnie do docelowego środowiska, w którym użytkownik pracuje z oprogramowaniem. Chodzi o to, aby stworzyć warunki jak najbardziej odpowiadające rzeczywistości. Wtedy użytkownik będzie zachowywał się naturalnie, przez co wyniki badania będą bardziej wiarygodne. Druga część pomieszczenia, zwykle umieszczona za lustrem weneckim, jest przeznaczona dla badaczy. Mają oni do dyspozycji sprzęt monitorujący akcje użytkownika (może to być obraz z kamer ukrytych w pierwszej części pomieszczenia, obraz z monitora użytkownika Przykładowe laboratorium użyteczności jest pokazane na rysunku 16.14.
itd.).
Eksperyment polega na zadaniu użytkownikowi jakiejś czynności do wykonania, a następnie obserwowaniu na żywo, jak sobie z tym radzi. Wszystkie podjęte przez użytkownika akcje są nagrywane i analizowane. Nierzadko sprzęt zainstalowany w laboratorium użyteczności jest bardzo wyrafinowany – można np. używać narzędzi do tzw. śledzenia wzroku (ang. eye tracking), pozwalających stwierdzić, które elementy interfejsu przyciągają uwagę użytkownika i jak rozkłada się intensywność obserwowania poszczególnych części ekranu. Bada się czas wykonania zadań, sposób używania oprogramowania, a nawet emocje towarzyszące wykonywanym zadaniom (np. złość, irytację lub radość).
Rysunek 16.14. Laboratorium badania użyteczności (źródło: www.designperspectives.com/usability.html) Na rysunku 16.15 przedstawiono przykład analizy śledzenia wzroku (tzw. ciepła, ang. heat map). Rysunek pochodzi z bloga
mapę
www.dreamscapemultimedia.com/seo-michigan/seo-case-studies-by-a-michiganseo-company. Pokazuje on rezultat wyszukiwania frazy „holiday villa spain” w wyszukiwarce google. Plamy na rysunku oznaczają miejsca, na które użytkownik zwracał większą uwagę. Im ciemniejsza plama, tym dłużej użytkownik skupiał swój wzrok na danym elemencie strony. Jak widać, największą uwagę użytkownika przykuwają pierwsze trzy rezultaty wyszukiwania (nieprzypadkowo są to linki płatne...). Użytkownik prawie w ogóle nie zwraca uwagi na linki organiczne znajdujące się poniżej, czy też reklamy umieszczone w prawej części strony.
Rysunek 16.15. Mapa ciepła (ang. heat map) dla śledzenia wzroku Wyniki badań użyteczności pozwalają ocenić, w jaki sposób oprogramowanie jest używane, jak zachowuje się użytkownik podczas pracy z aplikacją, gdzie napotyka na trudności lub problemy, które czynności wykonuje szybciej, a nad którymi spędza więcej czasu itd. W tego typu badaniach można również stosować tzw. testy A/B polegające na tym, że różnym grupom użytkowników prezentuje się dwie wersje programu (np. różniące się układem menu lub kolorystyką przycisków GUI), a nastęnie bada różnice w ich sposobie użycia. Pozwala to stwierdzić, która z wersji jest lepsza pod kątem jakiegoś parametru użyteczności. Zwykle eksperymenty dotyczące użyteczności są projektowane zgodnie z klasycznym schematem przeprowadzania kontrolowanego eksperymentu.
Polega to na sformułowaniu hipotezy badawczej (np.: „umieszczenie menu kontekstowego u góry ekranu powoduje szybsze wyszukiwanie opcji menu niż umieszczenie go z boku ekranu”), wybraniu losowej próby badanych użytkowników3, dobraniu grupy kontrolnej, przeprowadzeniu badania oraz analizie wyników i wyciągnięciu wniosków. Próba musi być odpowiednio duża tak, aby wyniki badania były statystycznie istotne.
16.6. Testowanie niezawodności (reliability) Niezawodność określa stopień, w jakim oprogramowanie poprawnie spełnia swoje funkcje w określonym czasie. Jeśli awarie systemu zdarzają się rzadko, to czas bezawaryjnego działania jest długi i niezawodność jest wysoka. Jest to charakterystyka istotna zwłaszcza dla systemów o znaczeniu krytycznych oraz intensywnie używanych. niezawodności to:
Typowe
przyczyny
wpływające
na
spadek
wycieki pamięci; brak zasobów; nieefektywne algorytmy; brak obsługi sytuacji wyjątkowych; problemy z infrastrukturą. niezawodność (ang. reliability) – zdolność oprogramowania do wykonywania wymaganych funkcji w określonych warunkach przez określony czas lub dla określonej liczby operacji [3] Poziom niezawodności można dobrze określić dopiero po dłuższym użytkowaniu oprogramowania bądź systemu, co wymaga długiego czasu. Nie zawsze mamy go do dyspozycji tyle, ile potrzeba. Dlatego w testowaniu niezawodności stosuje się często metody symulujące „przyspieszone” użytkowanie systemu. W przypadku hardware’u jest wykorzystywana tzw. metoda HALT (ang. Highly Accelerated Life Tests) [156], gdzie systemy elektroniczne poddaje się testom wytrzymałościowym (np. wysoka lub niska temperatura czy natężenie prądu). W przypadku oprogramowania można wykorzystać testowanie automatyczne oparte na profilu operacyjnym. Dzięki automatyzacji oprogramowanie jest używane o wiele intensywniej niż w przypadku normalnej
pracy, co pozwala symulować długoterminowe użytkowanie w relatywnie krótkim czasie. W ramach niezawodności można wyróżnić cztery podcharakterystyki: dojrzałość, odporność na błędy, odtwarzalność oraz dostępność. Omówimy je teraz po kolei.
16.6.1. Dojrzałość (maturity) Dojrzałość to stopień, w jakim oprogramowanie spełnia wymogi niezawodności podczas normalnego użytkowania. Przez „normalne” rozumiemy tu użytkowanie typowe, zgodne z profilem operacyjnym. pozwalającymi wyrazić dojrzałość ilościowo są:
Standardowymi
metrykami
MTBF (ang. Mean Time Between Failures), czyli średni czas między awariami; MTTR (ang. Mean Time To Repair), czyli średni czas do naprawy awarii; MTTF (ang. Mean Time To Failure), czyli średni czas do awarii; współczynnik awarii. Między pierwszymi trzema metrykami zachodzi następujący, dosyć oczywisty związek: MTBF = MTTR + MTTF Po wystąpieniu awarii musi upłynąć pewien czas, w którym system powróci do normalnego działania (np. przez restart, czy wykonanie jakichś operacji naprawczych). Po upływie tego czasu system działa do momentu nastąpienia kolejnej awarii. Suma tych czasów jest czasem między awariami. Sytuacja ta zilustrowana jest na rysunku 16.16. Średnia z tych wartości to właśnie MTBF, która jest podstawową metryką dojrzałości.
Rysunek 16.16. Relacje między metrykami MTTR, MTTF i MTBF średni czas między awariami (ang. Mean Time Between Failures, MTBF) – średnia arytmetyczna czasów między awariami systemu w określonym przedziale czasowym średni czas do naprawy (ang. Mean Time To Repair, MTTR) – średnia arytmetyczna czasu, po którego upływie system jest uruchamiany po wystąpieniu awarii Współczynnik awarii jest definiowany jako iloraz liczby awarii (często ograniczonych do określonego typu lub typów) na jednostkę wykonania, np. przedział czasowy, liczbę cykli procesora, liczbę uruchomień. współczynnik awarii (ang. failure rate) – stosunek liczby awarii w danej kategorii do określonej jednostki miary, np. awarie na jednostkę czasu, liczbę transakcji czy na liczbę uruchomień komputera [7] Dojrzałość można również rozumieć jako wydajność systemu po długim okresie użytkowania go. Posiadacze systemów operacyjnych typu Windows często obserwują sukcesywny spadek wydajności systemu, co po pewnym czasie skutkuje podjęciem decyzji o reinstalacji lub „wyczyszczeniu” systemu. dojrzałość (ang. maturity) – zdolność oprogramowania do uniknięcia awarii jako rezultatu defektów [3]
Metryki dojrzałości, takie jak wspomniane wcześniej, mogą również służyć do oceny jakości produktu i podejmowania decyzji o terminie wypuszczenia produktu na rynek. Na rysunku 16.17 przedstawiono rzeczywiste dane o niezawodności jednego z projektów pochodzących z bazy SLED (Software Life Cycle Empirical/Experience Database) [157]. Widać, że wraz z upływem czasu fazy wytwórczej czasy między awariami stają się coraz dłuższe, co oznacza, że awarie następują coraz rzadziej.
Rysunek 16.17. Średni czas między awariami
16.6.2. Odporność na błędy (fault tolerance, robustness) Odporność na błędy to stopień, w jakim oprogramowanie jest w stanie spełniać poprawnie swoje funkcje mimo obecności błędów i wystąpień awarii, zarówno sprzętowych, jak i programowych. Testowanie odporności na błędy może polegać m.in. na:
inspekcji modułów pod kątem obsługi wyjątków (czy każdy możliwy wyjątek jest odpowiednio obsłużony?); testowaniu działania programu w sytuacji błędów pamięci; testowaniu
działania
programu
w
sytuacji
błędów
w
plikach
zewnętrznych; testowaniu działania programu w sytuacji problemów z urządzeniami peryferyjnymi (np. co się stanie, gdy podczas drukowania odłączymy drukarkę?) itp. Naturalną techniką stosowaną w testowaniu odporności na błędy są ataki usterkowe na oprogramowanie (patrz podrozdz. 10.6). Aby dobrze przetestować poziom tej charakterystyki jakościowej, tester musi dobrze znać system i znać potencjalne przyczyny wystąpień awarii. Często, aby wymusić wystąpienie określonych, specyficznych błędów, stosuje się instrumentację kodu, np. za pomocą debugera. Dzięki temu tester może modyfikować wartości różnych zmiennych podczas działania programu, wymuszając powstawanie awarii i obserwować zachowanie systemu. odporność (ang. fault tolerance, robustness) – stopień, w jakim system lub moduł może działać prawidłowo przy nieprawidłowych danych lub przy dużym obciążeniu [7] tolerowanie błędów (ang error tolerance) – zdolność modułu lub systemu do kontynuowania prawidłowego działania mimo podania błędnych danych wejściowych [7] tolerowanie usterek (ang. fault tolerance) – zdolność oprogramowania do utrzymania określonego poziomu wydajności w przypadku występowania usterek (defektów) lub naruszenia jego interfejsów [3]; patrz także: niezawodność, odporność
16.6.3. Odtwarzalność (recoverability) Odtwarzalność to stopień, w jakim oprogramowanie jest w stanie przywrócić stan systemu, w tym wszystkie dane oraz operacje utracone lub przerwane na skutek wystąpienia awarii. Przykładowymi testami mogą być: zwykłe wyłączenie
zasilania komputera podczas działania programu, zabicie procesu testowanego programu, symulacja uszkodzenia dysku itp. Odtwarzalność często testuje się w systemach z redundancją (np. RAID – ang. Redundant Array of Indepedent Disks), w których dane są zapisywane redundantnie, na wielu nośnikach. W przypadku awarii jednego z nich, system przełącza się na wykorzystywanie innego. Testowanie może sprawdzać, czy przełączenie to zadziałało poprawnie. Innym przykładem jest testowanie, czy system właściwie tworzy oraz wykorzystuje kopie zapasowe plików. Metryką ilościową mierzącą poziom odtwarzalności może być MTTR (średni czas do naprawy). Częściej jednak interesuje nas aspekt jakościowy tej charakterystyki, czyli po prostu to, czy system potrafi sam doprowadzić się do stanu sprzed awarii. odtwarzalność (ang. recoverability) – zdolność oprogramowania do osiągania określonego poziomu wydajności i przywracania danych uszkodzonych przez awarię [3]
16.6.4. Dostępność (availability) Dostępność to czas, w którym testowany system jest dostępny, podzielony przez całkowity czas użytkowania tego systemu. Licznik tej metryki może być mniejszy od mianownika na skutek występowania różnego rodzaju awarii oraz akcji, jakie system musi podjąć, aby odtworzyć swój poprawny stan. Dostępność jest więc kombinacją dojrzałości (częstość awarii), odporności na błędy oraz odtwarzalności (czas naprawy). Stanowi ona raczej charakterystykę związaną z oceną jakości niż samym testowaniem. Stopień dostępności pośrednio określa jakość oprogramowania i pozwala wnioskować, czy system był wystarczająco przetestowany. dostępność (ang. availability) – stopień, w jakim moduł lub system działa i jest dostępny, gdy jest wymagane jego użycie, często wyrażany w procentach [7]
16.7. Testowanie zabezpieczeń (security) Zabezpieczenie to stopień, w jakim system lub program chroni dane oraz informacje przed dostępem do nich osób niepowołanych. Z punktu widzenia
testowania
można
wyróżnić
funkcjonalne
oraz
techniczne
testowanie
zabezpieczeń. To pierwsze nazywa się również testami penetracyjnymi. Funkcjonalne testowanie zabezpieczeń sprawdza bezpieczeństwo programu na poziomie i w ramach samej aplikacji (np. poprawność przydziału praw dostępu oraz ich egzekwowanie). Techniczne testowanie zabezpieczeń dotyczy bardziej ataków na oprogramowanie, w tym ataków hakerskich. zabezpieczenie (ang. security) – atrybuty oprogramowania określające jego zdolność do zapobiegania nieautoryzowanym przypadkowym lub umyślnym dostępem do programu i do danych [3] Wymagania dotyczące zabezpieczeń zwykle nie są wyszczególnione w dokumencie wymagań, co sprawia, że problemy z bezpieczeństwem powracają nieustannie przy okazji tworzenia kolejnych systemów. Być może powodem takiego stanu rzeczy jest fakt, że testowanie zabezpieczeń jest w zasadzie obszarem zupełnie odmiennym od pozostałych technik testowania. Wymaga wiedzy eksperckiej i często przeprowadzenie testów zabezpieczeń zleca się zewnętrznym firmom specjalizującym się w tej dziedzinie. Najpopularniejsze techniki testowania zabezpieczeń to ataki oraz taksonomie defektów. Inżynier jakości powinien pamiętać, że często zapewnienie wzrostu bezpieczeństwa powoduje spadek innych charakterystyk jakości, takich jak użyteczność czy efektywność. Na przykład zbyt mocno zabezpieczony system, wymagający wprowadzania wielu długich, skomplikowanych haseł oraz wymuszający ich częstą zmianę powoduje spadek użyteczności oprogramowania. Istnieją różne organizacje zajmujące się poprawą zabezpieczeń oprogramowania. Na ich stronach internetowych można znaleźć wiele ciekawych i przydatnych informacji na temat bezpieczeństwa, popularnych ataków oraz sposobów obrony przed nimi. Przykładem takiej organizacji jest OWASP (Open Web Application Security Project) [158]. W ramach charakterystyki zabezpieczeń można wyróżnić pięć podcharakterystyk: poufność, integralność, niezaprzeczalność, odpowiedzialność oraz uwierzytelnienie. Omówimy je teraz po kolei.
16.7.1. Poufność (confidentiality)
Poufność to stopień, w jakim system zapewnia, że dane są dostępne tylko dla osób do tego upoważnionych. Poufność można uzyskać, stosując system nadawania ról i uprawnień użytkownikom. Dostęp do danych może być także zabezpieczany różnego rodzaju hasłami.
16.7.2. Integralność (integrity) Integralność to zapewnianie dokładności i spójności danych. Wysoki poziom integralności oznacza, że dane nie mogą być modyfikowane w nieautoryzowany sposób.
16.7.3. Niezaprzeczalność (non-repudiation) Niezaprzeczalność oznacza, że użytkownik nie może wyprzeć się wykonania pewnych akcji w systemie. Niezaprzeczalność można realizować zarówno za pomocą logowania wszystkich czynności wykonywanych przez użytkownika, jak i przy użyciu technik kryptograficznych, np. szyfrowania z kluczem prywatnym i publicznym. Testowanie niezaprzeczalności może polegać na weryfikacji tego, czy system rzeczywiście loguje określone akcje lub czy wykorzystuje właściwy algorytm szyfrowania asymetrycznego. Przykładem ataku kryptologicznego związanego z niezaprzeczalnością może być tzw. atak człowiek pośrodku. atak człowiek pośrodku (ang. man in the middle attack) – przechwycenie, naśladowanie lub modyfikowanie oraz następnie przesłanie wiadomości (np. transakcji kartą kredytową) przez stronę trzecią w ten sposób, że użytkownik pozostaje nieświadomy istnienia strony trzeciej
16.7.4. Odpowiedzialność (accountability) Odpowiedzialność to stopień, w jakim działania dokonywane w systemie mogą być jednoznacznie odniesione do konkretnego użytkownika. Odpowiedzialność można uzyskać np. przez stosowanie podpisów cyfrowych.
16.7.5. Uwierzytelnianie (authenticity) Uwierzytelnianie (autentykacja) to stopień, w jakim można udowodnić tożsamość użytkownika. Istnieją trzy typowe metody weryfikacji tożsamości użytkownika:
przez coś, co użytkownik zna (np. hasło); przez coś, co użytkownik ma (np. token); przez coś, czym użytkownik jest (np. dane biometryczne). Testowanie uwierzytelniania polega na weryfikacji, czy przyjęty schemat weryfikacji tożsamości działa poprawnie (w szczególności można sprawdzać jakość samych technik uwierzytelniających, np. to, czy system dopuszcza definiowanie zbyt prostych haseł).
16.8. Testowanie pielęgnowalności (maintainability) Pielęgnowalność (utrzymywalność) dotyczy głównie czynności wykonywanych w fazie utrzymania oprogramowania. Jest to stopień, w jakim produkt może być modyfikowany po jego wydaniu. Wyjątkiem jest testowalność – podcharakterystyka pielęgnowalności dotycząca właściwości oprogramowania na etapie jego tworzenia. Standardem związanym z pielęgnowalnością oprogramowania jest IEEE 1219 [159]. Testowanie pielęgnowalności sprawdza nie tylko możliwość, lecz także łatwość i efektywność modyfikacji oprogramowania czy systemu. Zmiany w oprogramowaniu mogą być wykonywane zarówno przez deweloperów, jak i pracowników wsparcia technicznego oraz samych użytkowników. Pielęgnowalność dotyczy w szczególności instalacji uaktualnień (ang. update) oraz ulepszeń (ang. upgrade). podcharakterystyki:
Można
w
jej
ramach
wyróżnić
następujące
modularność; powtórne użycie; analizowalność; modyfikowalność; testowalność. pielęgnowalność (ang. maintainability) – łatwość, z jaką oprogramowanie może być modyfikowane w celu naprawy defektów, dostosowania do nowych wymagań, modyfikowane w celu ułatwienia przyszłego utrzymania lub dostosowania do zmian zachodzących w jego środowisku [3]
16.8.1. Modularność (modularity) Moduł
to
dający
się
logicznie
wydzielić
fragment
oprogramowania,
odpowiedzialny za realizację określonej funkcjonalności. Moduły komunikują się ze sobą za pomocą interfejsów. Modularność to stopień, w jakim oprogramowanie lub system jest zbudowany z komponentów w taki sposób, że zmiana w jednym z nich (lub jego wymiana na inny) ma możliwie mały wpływ na działanie innych komponentów. Modularność jest ważna z punktu widzenia projektu systemu, ale ma też duże znaczenie z punktu widzenia testowania. Jeśli system jest zaprojektowany z dobrze zorganizowanych modułów, zmiana jednego z nich nie powinna nieść ze sobą dużego ryzyka powstania defektów w innych komponentach lub na styku komponentu zmienianego z innymi modułami, z którymi się komunikuje. Modularność ma wiele zalet: pozwala radzić sobie z dużą złożonością systemu (metoda divide et impera – dziel i rządź) i efektywnie zarządzać procesem wytwórczym; pozwala na fizyczne oddzielenie od siebie poszczególnych części systemu, przez co zmiany zwykle dotyczą małych, lokalnych fragmentów systemu i nie wpływają na inne jego obszary; pozwala na jasny proces mapowania modułów na wymagania (w idealnym przypadku jedno wymaganie jest realizowane przez jeden moduł systemu); pozwala na lepsze zrozumienie systemu, jego struktury i logiki działania.
Rysunek 16.18. Zależności między modułami i odpowiadająca im macierz struktury projektu Moduły powinny cechować się wysoką kohezją oraz niskim powiązaniem (ang. coupling). Cechy te są szczególnie istotne w paradygmacie obiektowym. Istnieją narzędzia analizy statycznej pozwalające wyrażać te cechy liczbowo. Na rysunku 16.8 jest przedstawiona przykładowa struktura systemu złożonego z sześciu modułów (A, B, C, D, E, F) oraz odpowiadająca im tzw. macierz struktury projektu (ang. Design Structure Matrix, DSM). Jest to symetryczna, kwadratowa macierz, w której wiersze i kolumny oznaczają poszczególne moduły a X na przecięciu wiersza P i kolumny Q oznacza, że moduły P i Q są ze sobą w jakiś sposób powiązane (na przykład jeden z nich dostarcza dane do drugiego lub wymieniają się informacjami). Im więcej zależności między modułami, tym niższa pielęgnowalność. W systemie z gęstą siatką powiązań między poszczególnymi elementami trudniej o nieświadome wprowadzenie defektów.
jest
dokonywać
zmian
i
łatwiej
kohezja (ang. cohesion) – stopień powiązania elementów w ramach danego modułu, klasy bądź podsystemu; przyjmuje się, że elementy oprogramowania powinna cechować wysoka kohezja powiązanie (ang. coupling) – stopień powiązania między modułami, klasami bądź podsystemami; przyjmuje się, że powiązanie powinno być możliwie małe
Modularność jest cechą istotną dla rozwoju oprogramowania typu opensource, gdzie nad jednym projektem może pracować i rozwijać go wielu programistów. Baldwin i Clark w pracy [160] wykazali (na gruncie teoretycznym!), że modularność jest jedną z kluczowych cech wpływających na wysoki poziom inicjatywy ze strony deweloperów w rozwijaniu danego oprogramowania. Innymi słowy, im większa modularność, tym chętniej programiści biorą udział w dalszym rozwijaniu danej aplikacji open-source.
16.8.2. Powtórne użycie (reusability) Powtórne użycie określa stopień, w jakim dany fragment oprogramowania może być użyty w więcej niż jednym systemie lub przy tworzeniu innych elementów oprogramowania. Im mniej dany komponent „wie” o systemie, w którym działa i im mniej zależy od pozostałych komponentów, tym łatwiej jest go powtórnie użyć w innym miejscu. Nie zawsze taki fragment oprogramowania można przenieść wprost z jednego systemu do innego. Czasami wymaga to dodatkowego oprogramowania takiego komponentu, czy wprowadzenia mniejszych lub większych zmian w jego kodzie. Im większe możliwości powtórnego użycia, tym lepsza pielęgnowalność systemu. Testowanie tej podcharakterystyki polega głównie na stosowaniu przeglądów i inspekcji.
16.8.3. Analizowalność (analyzability) Analizowalność to poziom efektywności, z jakim możliwe jest ustalenie wpływu, jaki na cały system będzie mieć zmiana w jednym lub kilku miejscach tego systemu. Cecha ta określa również łatwość, z jaką dany produkt lub system można diagnozować pod kątem jego niedoskonałości, a także lokalizować przyczyny awarii. Analizowalność w fazie utrzymania wpływa na jakość pracy wsparcia technicznego (szybkość, z jaką są lokalizowane i usuwane awarie zgłaszane przez użytkowników), a w fazach produkcji oprogramowania – na efektywność procesu debugowania, czyli lokalizacji i usuwania defektów znalezionych w procesie testowania. analizowalność (ang. analyzability) – zdolność wytwarzanego produktu do bycia zdiagnozowanym pod kątem braków lub przyczyn awarii lub pod kątem
rozpoznania części do modyfikacji [3]; patrz także: pielęgnowalność Analizowalność można zwiększyć przez wprowadzenie do kodu źródłowego mechanizmów pozwalających na analizę przez program swoich własnych awarii, np. przez stworzenie mechanizmu obsługi wyjątków wraz z logowaniem zawartości
określonych
zmiennych
oraz
procesów,
które
zachodziły
w działającym programie do momentu wystąpienia awarii. Testowanie analizowalności sprawdza, w jakim stopniu jest zapewniona łatwość w lokalizacji i usuwaniu defektów.
16.8.4. Modyfikowalność (modifiability) Modyfikowalność to łatwość, z jaką system lub program może być modyfikowany bez wprowadzania defektów lub zmniejszania w inny sposób jego jakości produktowej. Na modyfikowalność duży wpływ mają modularność oraz analizowalność. Ta pierwsza wpływa na łatwość wprowadzania zmian, natomiast od drugiej zależy poziom ryzyka pojawienia się nowych defektów podczas wprowadzania zmian. modyfikowalność (ang. changeability, modifiability) – zdolność produktu oprogramowania do wprowadzania wyspecyfikowanych zmian [3]; patrz także: pielęgnowalność Przykładem modyfikowalności jest możliwość zdalnego pobrania przez program łatki (ang. patch), która jest w stanie zmienić kod wykonywalny programu. Sytuacje takie najczęściej zdarzają się wtedy, gdy w oprogramowaniu zostanie wykryta podatność na jakiś atak lub gdy zachodzi potrzeba dostosowania oprogramowania do współpracy z nowym sprzętem, systemem operacyjnym lub innym oprogramowaniem. Testowanie modyfikowalności może sprawdzać, czy proces ściągnięcia i zainstalowania łatki przebiega pomyślnie i czy po jego zakończeniu w oprogramowaniu wprowadzono oczekiwane zmiany.
16.8.5. Testowalność (testability) Testowalność to łatwość, z jaką można wyprowadzić dla oprogramowania lub systemu kryteria testowe, a także wykonać testy w celu weryfikacji spełnienia tych kryteriów. Podczas projektu nowego systemu nie tylko musimy zadać sobie
pytanie „czy potrafimy to zbudować?”, lecz także „czy potrafimy to przetestować?”. Dobra testowalność systemów staje się coraz ważniejsza [161], z uwagi na wzrastającą rolę inżynierii jakości i testowania. testowalność (ang. testability) – właściwość oprogramowania umożliwiająca testowanie go po zmianach4 [3] przegląd testowalności (ang. testability review) – szczegółowe sprawdzenie podstawy testów w celu określenia, czy jest ona na odpowiednim poziomie jakości, aby mogła posłużyć jako dokument wejściowy dla procesu testowego [28] Poziom testowalności ma znaczenie dla wysiłku testerów – im mniej testowalny jest produkt, tym więcej pracy trzeba włożyć w projektowanie, implementację i wykonanie testów [162]. W skrajnych przypadkach testowanie pewnych aspektów programu może w ogóle być niemożliwe. Na testowalność ma wpływ wiele czynników, takich jak: kontrolowalność, czyli możliwość weryfikacji, w jakim stanie aktualnie znajduje się program, a także łatwość wpływania na sposób działania programu; ma to znaczenie przy białoskrzynkowych technikach projektowania testów, gdzie chcemy wymusić np. przejście sterowania ściśle określoną ścieżką; obserwowalność, czyli możliwość obserwowania zarówno końcowych wyników działania programu, jak i kroków pośrednich; często w celu osiągnięcia odpowiednio wysokiego poziomu obserwowalności stosuje się narzędzia do debugowania, które potrafią na bieżąco podczas wykonywania programu pokazywać stan zmiennych, obiektów czy struktur danych; izolowalność, czyli możliwość testowania modułu w izolacji od innych komponentów; im mniejsza izolowalność, tym większa potrzeba tworzenia namiastek lub sterowników (jeśli moduły, z którymi testowany komponent się komunikuje, nie są jeszcze zaimplementowane) lub bardziej skomplikowanych i dłużej trwających testów (jeśli wszystkie moduły są zaimplementowane, ale test wymaga np. komunikacji między wieloma modułami);
złożoność, czyli stopień komplikacji wewnętrznej struktury modułu; im bardziej skomplikowany kod i jego struktura, tym trudniej zaprojektować testy i uzyskać odpowiedni stopień pokrycia; istnienie cyklicznych zależności między modułami sprawia z kolei, że nie wiadomo, jaka powinna być kolejność testowania tych modułów i może to wymagać implementacji namiastek/sterowników [163]; modularność rozumiana jako podział odpowiedzialności między modułami; dzięki dobrej modularności pojedynczy test dotyczy pojedynczej funkcjonalności; stosowanie niektórych wzorców projektowych w paradygmacie obiektowym może utrudniać testowanie, np. wykorzystywanie wzorca Singleton powoduje, że testy na instancji tej klasy będą od siebie zależne, tzn. wynik jednego może zależeć od wyniku drugiego; zrozumiałość, czyli stopień, udokumentowany i objaśniony; automatyzacja, automatycznych.
czyli
w
możliwość
jakim
testowany
przeprowadzenia
kod
jest
testów
Niektórzy autorzy uważają, że na poziomie testów modułowych testowalność może być zwiększona przez wykorzystanie takich technik jak Test Driven Development, jednak testowalność jest w tym przypadku produktem ubocznym, a nie rezultatem zaangażowanych działań i wysiłku. W podejściu TDD nie istnieje żaden proces, który pozwalałby na systematyczne zwiększenie poziomu testowalności. TDD jest podejściem metodą prób i błędów do testowalności i silnie zależy od refaktoringu. Metoda może dobrze sprawdzać się dla małych systemów, ale w dużych projektach czas i wysiłek poświęcony na refaktoring zarówno implementacji, jak i projektu staje się problematyczny [164]. Testowalność jest nie tylko cechą wykonywalnego kodu, lecz także wszystkich artefaktów mogących podlegać testowaniu. W szczególności można określać testowalność wymagań, co jest efektywną metodą obniżania ryzyka rezydualnego w projektowanym systemie. Wykrycie nietestowalnych wymagań pozwala bowiem przeformułować je tak, aby każdy aspekt systemu mógł podlegać kontroli jakości. Wymagania w szczególności przekładają się na rozwiązania projektowe, a wiemy, że wykrycie błędu projektowego po przekazaniu programu do użytkowania skutkuje o wiele kosztowniejszym usunięciem go niż np. w fazie projektu wysokiego poziomu. Testowalność specyfikacji wymagań jest
„prawdopodobnie
najważniejszym
celem
wymagań,
a
jednocześnie
najtrudniejszym do osiągnięcia” [165]. Testowalność może również dotyczyć dziedziny problemu, którego dotyczy tworzona aplikacja. Określa ona łatwość, z jaką można zaprojektować testy, biorąc w szczególności pod uwagę dwa czynniki: problem wyroczni – specyfika dziedziny problemu może utrudniać testowanie, jeśli nie istnieje wyrocznia testowa, bo np. określenie poprawnego wyjścia jest trudne lub czasochłonne dla wykonania ręcznego lub też projektowane oprogramowanie będzie musiało produkować wyjście o tak dużym rozmiarze, że weryfikowanie go staje się niepraktyczne; interakcja z zewnętrznymi systemami – częścią problemu jest wymagana interakcja z zewnętrznymi systemami; interakcja oraz systemy te same w sobie mogą powodować trudności w testowaniu [164]. Souza i Arakaki [166] proponują pięć następujących abstrakcyjnych wzorców, których stosowanie przyczynia się do zwiększenia poziomu testowalności: Built-In Self-Testing (BIST) – koncepcja zaczerpnięta z systemów hardware’owych, polegająca na tym, że komponent raportuje swój własny stan. Deweloper musi zaimplementować standardowy interfejs we wszystkich klasach wykorzystujących BIST. Dependency Injection (DI) – gdy komponent biznesowy zależy od systemów zewnętrznych (tzw. third-party component, bazy danych, webserwisy itp.) często ciężko usunąć lub zmienić te zależności bez zmiany kodu źródłowego. Komponent powinien mieć te zależności wbudowane, zamiast nimi samemu zarządzać. Konsekwencją jest to, że tester będzie musiał tworzyć zaślepki i sterowniki, aby zastąpić nieistniejące komponenty, od których zależy działanie testowanego modułu. Dynamic Component Management Extension (DCME) – gdy zespół testerów ma do dyspozycji wiele namiastek i sterowników symulujących zewnętrzne zachowanie systemu, należy umożliwić ich dynamiczną zmianę, tzn. dynamiczny wybór konkretnej implementacji.
Testability Logger (TL) – zdarzenia oraz dane związane z testowaniem muszą być logowane dla celów procesu testowego, co może prowadzić do redundantnego kodu. Logger dostarcza scentralizowaną kontrolę funkcjonalności logowania i sam zajmuje się sposobem klasyfikacji i logowania zdarzeń. Taki logger zwiększa obserwowalność systemu, natomiast wpływa negatywnie na wydajność systemu. Testability Interceptor (TI) – pozwala na zwiększenie obserwowalności i kontrolowalności przez pozwolenie komponentom na monitorowanie i dynamiczną zmianę ich zachowań. Tester może obserwować i modyfikować funkcjonalność bez ingerencji w wewnętrzną strukturę logiczną komponentu. Testowalność można również zwiększać przez znajdowanie w kodzie lub projekcie architektury tzw. antywzorców, czyli złych praktyk programistycznych obniżających testowalność produktu. antywzorzec (ang. anti-pattern) – powtarzalna akcja, proces, struktura lub rozwiązanie wielokrotnego użytku, które początkowo wydawało się korzystne i jest często używane, ale jest nieskuteczne lub nieproduktywne w zastosowaniu
16.9. Testowanie przenaszalności (portability) Często zdarza się, że zmienia się środowisko, w którym działa oprogramowanie. Może być to spowodowane np. modernizacją systemu operacyjnego do nowej wersji lub wręcz przeniesieniem oprogramowania na inną platformę. Przenaszalność to stopień, w jakim system lub komponent może być przeniesiony z jednego środowiska sprzętowego, oprogramowania lub środowiska operacyjnego do innego. Przykładem dobrej przenaszalności są programy pisane w Javie – jeśli docelowe środowisko, w którym taki program ma działać, ma maszynę wirtualną Javy, to program będzie pracował poprawnie niezależnie od konfiguracji systemu. W ramach przenaszalności można wyróżnić trzy podcharakterystyki: adaptowalność, instalowalność oraz zastępowalność. przenaszalność (ang. portability) – łatwość, z jaką oprogramowanie może być przeniesione z jednego środowiska sprzętowego lub programowego do innego środowiska [3]
16.9.1. Adaptowalność (adaptability) Adaptowalność to stopień, w jakim program lub system potrafi dostosować się do innego lub zmienionego środowiska. Powinna być zapewniona nie tylko pod kątem zmian hardware’u czy systemu operacyjnego, lecz także na poziomie poszczególnych elementów oprogramowania. Przykładem może być wygląd ekranu, który powinien dostosować się do innej niż domyślna rozdzielczości ekranu. W przypadku stron internetowych jest ważne, aby zapewnić adaptowalność do wszystkich (a przynajmniej najczęściej używanych) przeglądarek internetowych. Techniką często stosowaną przy projektowaniu stron www jest tzw. responsive web design. Polega ona na takim projektowaniu strony, aby jej wygląd i układ automatycznie dostosowywał się do zmiany rozmiaru okna, w którym ma być wyświetlona. Jest to szczególnie istotne w przypadku aplikacji mobilnych, wyświetlanych na urządzeniach przenośnych, które mogą być ustawione przez użytkownika zarówno wertykalnie, jak i horyzontalnie. adaptowalność, zdolność do adaptacji (ang. adaptability) – zdolność oprogramowania do dostosowania się do różnych środowisk, bez konieczności stosowania działań lub środków innych niż te, które dostarczono do tego celu [3]; patrz także: przenaszalność Testowanie adaptowalności może polegać na zwykłym czarnoskrzynkowym testowaniu opartym na scenariuszach użycia, może też przyjąć formę inspekcji. Na przykład, dla stron www można zweryfikować poprawność responsive web design przez sprawdzenie, czy kod html zawiera w sekcji „head” instrukcję
Listing 16.1. Fragment kodu html obowiązkowego w responsive web design Najczęściej, w przypadku wielu możliwych wersji różnych komponentów środowiska docelowego, stosuje się technikę pokrycia kombinatorycznego (np.
pairwise) tak, aby każda para wersji dwóch różnych charakterystyk występowała w jednym teście.
16.9.2. Instalowalność (installability) Instalowalność to stopień, w jakim oprogramowanie lub system może być efektywnie zainstalowane i odinstalowane w określonym środowisku. Testowanie instalowalności polegać może na: testowaniu kreatora instalacji; przeprowadzeniu standardowego procesu instalacji zgodnie z instrukcją użytkownika; przerwaniu instalacji w pewnym momencie i sprawdzeniu, czy system wrócił do stanu przed rozpoczęciem instalacji, np. czy w rejestrze Windows nie pozostały jakieś wpisy instalowanej aplikacji lub czy na dysku nie pozostały katalogi bądź pliki tymczasowe stworzone przez program instalacyjny; odinstalowaniu poprawnie zainstalowanego programu i zweryfikowaniu, czy system wrócił do stanu sprzed instalacji; próbie powtórnej instalacji i sprawdzeniu, czy jest to możliwe, a jeśli tak, to czy nie zostają nadpisane jakieś dane z poprzedniej instalacji. kreator instalacji (ang. installation wizard) – oprogramowanie dostarczone na odpowiednich nośnikach, które prowadzi instalatora przez proces instalacji; zazwyczaj wykonuje proces instalacji, informuje o jego wynikach i prosi o wybór opcji instalowalność (ang. installability) – zdolność oprogramowania do bycia zainstalowanym w wyspecyfikowanym środowisku [3]; patrz także: przenaszalność
16.9.3. Zastępowalność (replaceability) Zastępowalność to stopień, w jakim dany produkt może zastąpić inny produkt w tym samym celu i w tym samym środowisku. Zastępowalność nie dotyczy wyłącznie innych aplikacji, ale także nowych wersji tej samej aplikacji. W takim
przypadku sprawdza się, czy ulepszenie (ang. upgrade) przebiega poprawnie i czy funkcjonalność programu nie uległa niepożądanym zmianom. zastępowalność (ang. replaceability) – zdolność oprogramowania do wykorzystania w miejsce innego oprogramowania o takim samym przeznaczeniu i w takim samym środowisku [3]; patrz także: przenaszalność
1 Skrajnym przykładem tego efektu jest tzw. zasada nieoznaczoności Heisenberga, która mówi, że istnieją pary wielkości, których nie można zmierzyć jednocześnie z dowolną dokładnością (np. położenie i pęd cząstki) – zasada ta nie wynika z niedoskonałości metod czy urządzeń pomiarowych, lecz z samej natury rzeczywistości. 2 Taka sytuacja może wystąpić wtedy, gdy w narzędziu do automatycznej generacji obciążenia maksymalna wydajność samego generatora jest niższa niż wydajność testowanego programu w obsłudze tego obciążenia. 3 Losowy dobór uczestników eksperymentu jest wbrew pozorom skomplikowaną i wysoce nietrywialną procedurą. Uzyskanie reprezentatywnej próby może być bardzo trudne zwłaszcza w przypadku, gdy uczestnicy rekrutują się spośród aktualnych użytkowników oprogramowania tworzących homogeniczną populację. 4 Według definicji ISO 9126 testowalność dotyczy testów po zmianach w oprogramowaniu, jednak definicja ta może obejmować ogół cech oprogramowania w ogóle umożliwiających bądź ułatwiających każde jego testowanie, nie tylko po wprowadzeniu zmian do kodu.
17. Testowanie jakości danych
17.1. Model jakości danych W podrozdziale 1.3 opisaliśmy przykład katastrofy projektu Mars Climate Orbiter. Awaria spowodowana była tym, że dwa zespoły pracujące nad dwoma osobnymi modułami oprogramowania używały odmiennych jednostek – jedna wykorzystywała niutony, druga – funty (tzw. funty siły, lbf). Przyczyną tej awarii była zła jakość danych, która jest bardzo częstym powodem występowania błędów w oprogramowaniu. Norma ISO 25012 [146] jest poświęcona w całości modelowi jakości danych, aby charakterystyki niefunkcjonalnej.
podkreślić
znaczenie
testowania
tej
jakość danych (ang. data quality) – atrybut danych opisujący stopień ich poprawności względem predefiniowanych kryteriów, np. wymagań biznesowych, wymagań na integralność danych, spójność danych itp. Model ISO wyróżnia piętnaście podcharakterystyk jakości danych, dzieląc je na trzy grupy (patrz rys. 14.4): inherentne, tzn. jakość tych danych zależy od nich samych, wynika np. z reguł biznesowych danej aplikacji lub z relacji między wartościami różnych zmiennych; zależne od systemu, tzn. ich jakość zależy od aspektów technologicznych, na przykład precyzja zależy od architektury komputera, a odtwarzalność czy przenaszalność od odpowiedniego oprogramowania; należące do obu powyższych grup jednocześnie.
Dane, o których mowa, to zarówno dane wewnętrzne programu (np. wartości poszczególnych zmiennych), jak i wszystkie dane „zewnętrzne”, przetwarzane przez program. W szczególności kontroli jakości poddane być mogą wejścia do programu podawane przez użytkownika bądź wczytywane ze źródeł zewnętrznych, np. z pliku lub z sieci, a także wyjścia w formie raportów, plików wyjściowych czy wszelkiego rodzaju informacji, jakie pojawiają się na ekranie. Obecnie wiele aplikacji oraz (zwłaszcza) stron internetowych jest sterowanych informacją i zawartością (ang. information & content driven), co sprawia, że jakość informacji staje się kluczowym atrybutem jakościowym tego typu systemów. Niektórzy autorzy (np. [167]) proponują swoje wersje modelu jakości danych, będące rozszerzeniem modelu ISO/IEC dopasowanym właśnie do kontekstu systemów sterowanych informacją. Omówimy teraz poszczególne charakterystyki jakości danych wyróżnione w normie ISO 25012. Tester powinien mieć świadomość, że przyczyną złej jakości danych często są defekty w oprogramowaniu. Testowanie jakości danych może pośrednio wskazać obecność tych usterek. Oczywiście defektem jest również niska jakość danych sama w sobie.
17.2. Charakterystyki inherentne 17.2.1. Dokładność (accuracy) Dokładność danych to stopień, w jakim dane reprezentują poprawne wartości w danym kontekście użycia. Można wyróżnić dwa jej typy: syntaktyczną oraz semantyczną. Syntaktyczna poprawność oznacza, że postać danych ma właściwą strukturę. Semantyczna poprawność oznacza, że znaczenie wartości danej odpowiada prawdzie. dokładność (ang. accuracy) – zdolność oprogramowania do zapewnienia właściwych lub uzgodnionych rezultatów lub efektów z wymaganym poziomem precyzji [3] W tabeli 17.1 jest przedstawiona baza danych adresów urzędów wojewódzkich w Polsce w postaci tabeli bazy danych. W wierszu o id=4 kod pocztowy jest niepoprawny syntaktycznie, ponieważ po kresce występują tylko dwie cyfry,
a powinny być trzy. W wierszu o id=2 nazwa ulicy napisana jest małą literą, a powinna być napisana wielką, co również jest błędem syntaktycznym. W wierszu o id=6 adres e-mailowy, choć poprawny syntaktycznie, jest błędny semantycznie, ponieważ domena powinna nazywać się malopolska.uw.gov.pl, a nie malapolska.uw.gov.pl1. Tabela 17.1. Baza danych adresów urzędów wojewódzkich
id województwo ulica
kod miasto
e- mail
1
dolnoś ląs kie
pl. Pows tańców Wars zawy 1
50Wrocław 951
[email protected]
2
kujaws kopomors kie
jag iellońs ka 3
85Bydg os zcz 950
[email protected]
3
lubels kie
S pokojna 4
20Lublin 914
[email protected]
4
lubus kie
Jag iellończyka 668 40
5
łódzkie
Piotrkows ka 103
90Łódź 425
s [email protected] ov.pl
6
małopols kie
Mog ils ka 26
31Kraków 156
urząd@malapols k
7
mazowieckie
Dług a 5
00Wars zawa 263
ws o@mazowieck
8
opols kie
Pias tows ka 14
45Opole 082
ws [email protected]
9
podkarpackie
Grunwaldzka 15
35Rzes zów 959
o@rzes zow.uw.g
Gorzów ws [email protected] orzo Wielkopols ki
10 podlas kie
Mickiewicza 3 15- Białys tok 213
ws o@białys tok.uw
11 pomors kie
Okopowa 21/27
80Gdańs k 810
ws o@g dans k.uw.
12 ś ląs kie
Jag iellońs ka 25
40032
ws oim@katowice
13 ś więtokrzys kie
al. IX Wieków Kielc 3
25Kielce 516
ws [email protected]
warmińs komazurs kie
al. Mar. J. Piłs uds kieg o 7/9
10Ols ztyn 575
s ekrs [email protected] zty
14
17.2.2. Zupełność (completeness) Zupełność danych to stopień, w jakim dane opisujące określony obiekt mają wartości dla wszystkich atrybutów w danym kontekście użycia. W tabeli 17.1 występują dwa błędy związane z zupełnością danych. Po pierwsze, baza zawiera dane adresowe tylko czternastu urzędów wojewódzkich, natomiast województw jest szesnaście – brakuje adresów urzędów dla województw wielkopolskiego i zachodniopomorskiego. Po drugie, w adresie urzędu dla województwa śląskiego (id 12) brakuje nazwy miasta. Jeśli spis, taki jak ten z tabeli 17.1, jest generowany automatycznie przez program na podstawie jakiegoś zewnętrznego źródła, np. bazy danych, fakt brakowania dwóch ostatnich elementów może być oznaką błędu w zapytaniu, które polega na wypisaniu tylko pierwszych czternastu rekordów. Brak miasta w wierszu nr 12 jest prawdopodobnie spowodowany fizycznym brakiem tej wartości w źródłowej bazie danych.
17.2.3. Spójność (consistency) Spójność danych to stopień, w jakim wartości poszczególnych danych są ze sobą niesprzeczne i spójne z innymi danymi w określonym kontekście użycia. Jeśli na
przykład baza danych osobowych kierowców zawiera pole rok urodzenia oraz rok wydania prawa jazdy, to błąd spójności wystąpiłby, gdyby np. data wydania prawa jazdy dla pewnego kierowcy była wcześniejsza niż data jego urodzenia. Inny przykład braku spójności możemy zaobserwować w danych z tabeli 17.1. Zauważmy, że adresy e-mailowe urzędów wojewódzkich pochodzą z bardzo różnych domen, a nawet w ramach jednej domeny (uw.gov.pl) mają różną strukturę. Jest to przykład złej jakości spójności danych. Wszystkie urzędy powinny mieć taką samą formę adresu, na przykład [departament]@uw. [miasto].gov.pl, gdzie [departament] jest kodem departamentu (np. wso = Wydział Spraw Osobowych), a [miasto] – nazwą miasta. spójność (ang. consistency) – stopień jednolitości, standaryzacji oraz brak sprzeczności między dokumentami oraz częściami modułu lub systemu [7] Spójność można także rozumieć jako wewnętrzną integralność (poprawność) danych. Na przykład, aby kontrolować, czy operacje wykonywane na danych nie zmieniły ich zawartości (np. przy przesyłaniu pakietów przez sieć) można stosować tzw. sumy kontrolne (ang. checksum), będące kilku- lub kilkunastoznakowym skrótem dokumentu. Jeśli w zadanym tekście choć jeden bit zostanie zmieniony, to jego suma kontrolna również ulegnie zmianie, przez co bardzo łatwo wykryć uszkodzenie treści pliku lub innego obiektu zawierającego dane.
17.2.4. Wiarygodność (credibility) Wiarygodność danych to stopień, w jakim dane są uważane za odpowiadające prawdzie. Na przykład, dla danych z tabeli 17.1 atrybutowi wiarygodności mogą podlegać adresy urzędów. Użytkownicy tej bazy, np. internauci wyszukujący informacji o urzędzie wojewódzkim w sieci, natrafiając na stronę Ministerstwa Spraw Wewnętrznych z tymi danymi mogą być bardziej przekonani co do prawdziwości adresów urzędów, niż znajdując je na jakiejś innej witrynie, niebędącej w domenie gov.pl. Wiarygodność związana jest w szczególności z pojęciem autentyczności, to znaczy zaufaniem do źródła informacji. Przykład z MSW dotyczy właśnie autentyczności – ludzie ufają instytucjom rządowym (przynajmniej, jeśli chodzi o prawdziwość danych na ich stronach www...).
17.2.5. Aktualność (currentness) Aktualność danych to stopień, w jakim dane mają poprawne atrybuty w określonym czasie. Na przykład przedstawiany na ekranie elektronicznym rozkład jazdy w systemie ELROJ (patrz Dodatek A) musi być uaktualniany tak, aby o danej godzinie były wyświetlane wszystkie kursy autobusów mających przyjechać w najbliższym czasie. Jeśli na ekranie są wyświetlane kursy z godzin 13:00–13:40 oraz 19:00–19:50, choć mogą być zupełnie poprawne i prawdziwe, nie będą zbyt użyteczne dla pasażerów czekających na przystanku o godzinie 13:45.
17.3. Charakterystyki inherentne i zależne od systemu 17.3.1. Dostępność (accessibility) Dostępność do danych to stopień, w jakim dane mogą być uzyskane przez użytkownika w określonym kontekście użycia. W szczególności dotyczy to osób mających różne formy niepełnosprawności. Przykładem złej dostępności do danych jest sytuacja, w której tekst jest prezentowany na czytniku w formie obrazka, przez co nie może zostać przeczytany przez urządzenie przekształcające tekst na głos, co jest wymagane dla aplikacji użytkowanych przez osoby niewidome. Realizacja dostępu może polegać również na możliwości prezentowania danych odpowiednio czytelnym krojem czcionki, różnymi rozmiarami (np. większe napisy dla osób niedowidzących) lub kolorami.
17.3.2. Zgodność (compliance) Zgodność to stopień, w jakim atrybuty danych spełniają określone standardy, regulacje lub przyjęte konwencje. Na przykład na oprogramowanie dla klientów banków może być nałożony wymóg, aby hasło do konta było przesyłane w formie zaszyfrowanej, zgodnie z określonym protokołem kryptograficznym. Innym przykładem może być regulacja występująca w Przepisach Lekkoatletycznych organizacji IAAF (Międzynarodowego Stowarzyszenia Federacji Lekkoatletycznych) określająca precyzję, z jaką ma być mierzony czas biegu. Na rysunku 17.1 przedstawiono fragment tych przepisów, będący przykładem specyfikacji wymagań dla programu mierzącego czas.
Rysunek 17.1. Przykład zgodności danych z zewnętrznymi regulacjami
17.3.3. Poufność (confidentiality) Poufność to stopień, w jakim dane są chronione przed niepowołanym dostępem. Charakterystyka ta wchodzi w skład obszaru bezpieczeństwa informacji (razem z dostępnością oraz integralnością) zdefiniowanego w normie ISO/IEC 13335 [168]. Jeśli firma używa programu komputerowego do obsługi płac, każdy pracownik powinien mieć w tym systemie możliwość wglądu do swoich danych (historia wynagrodzeń i tzw. paski z wynagrodzeniem), ale tylko upoważnione osoby mogą mieć dostęp do danych o pensjach wszystkich pracowników. Innym przykładem zapewniania poufności jest wykropkowanie wpisywanego hasła tak, aby żadna postronna osoba nie mogła odczytać z ekranu hasła, które jest właśnie wpisywane (patrz rys. 17.2).
Rysunek 17.2. Przykład poufności danych (hasło do poczty)
17.3.4. Wydajność (efficiency) Wydajność to stopień, w jakim dane mogą być przetwarzane z zachowaniem określonego poziomu efektywności przy określonym poziomie zasobów. Oto przykłady wysokiej wydajności danych, które pozwalają na lepsze gospodarowanie zasobami (miejsce na dysku) lub na efektywniejsze przetwarzanie danych: przyrostowe zapisywanie kolejnych wersji oprogramowania w systemach kontroli wersji (nowa wersja nie jest zapisywana w całości, ale tylko jako różnica między nią a wersją poprzednią);
kompresja archiwizowanych danych, które prawdopodobnie nie będą często wykorzystywane; stosowanie normalizacji w strukturze bazy danych, co zwiększa szybkość przetwarzania zapytań; stosowanie słowników dla często używanych wartości zmiennych (np. nazwy województw).
17.3.5. Precyzja (precision) Precyzja jest głównie związana z danymi liczbowymi i oznacza dokładność zapisu tych liczb, czyli to, ile miejsc po przecinku ma mieć reprezentacja danej wartości. Na przykład dane walutowe dla złotego polskiego czy euro powinny być zapisywane z precyzją do dwóch miejsc po przecinku (dokładność 1 grosza), choć czasami wymóg precyzji dla danych walutowych może być jeszcze większy (sic!). Można mianowicie wyobrazić sobie program naliczający koszt jakiegoś materiału w zależności od jego masy. Masa jest zmienną ciągłą, dlatego można wyobrazić sobie, że wartość tego towaru będzie liczbą o potencjalnie nieskończonym rozwinięciu dziesiętnym (np. jeśli 1 kg materiału kosztuje 100 PLN, to 1/3 kilograma to 33,333(3) PLN). W takiej sytuacji istotny może być sposób zaokrąglania tych wartości do rzeczywistych kwot walutowych (czy 33,3333 PLN to 33,33, czy też 33,34 PLN?), co może skutkować nałożeniem na system wymogu zapamiętywania tych wartości z większą precyzją niż 2 miejsca po przecinku. Dokładność jest podcharakterystyką odwrotnie proporcjonalną
do
efektywności. Im większej dokładności wymagamy, tym więcej potrzeba na to zasobów i być może czasu do przetwarzania tych danych.
17.3.6. Identyfikowalność (traceability) Identyfikowalność to stopień, w jakim jest możliwe śledzenie dostępu oraz zmian w danych. Odbywa się to zwykle za pomocą logów (ang. audit trial), w których jest zapisywane, kto i kiedy żądał dostępu do jakich danych, kto i kiedy wytworzył lub zmienił dany dokument itd.
Rysunek 17.3. Przykład identyfikowalności danych Na rysunku 17.3 przedstawiono zastosowanie identyfikowalności danych w praktyce. Jest to zrzut ekranu ze strony Uniwersytetu Jagiellońskiego zawierającej zarządzenia Rektora UJ. Przy każdym zarządzeniu podana jest informacja, kto i kiedy wytworzył dany dokument oraz kto go opublikował na stronie internetowej.
17.3.7. Zrozumiałość (understandability) Zrozumiałość danych to stopień, w jakim atrybuty danych pozwalają na właściwe odczytanie i interpretację tych danych przez użytkownika. Zrozumiałość może dotyczyć języka użytego do zapisu danych, odpowiedniej symboliki (np. piktogramy lub ikony programów) bądź jednostek (np. w Wielkiej Brytanii niektóre jednostki układu SI nie są w powszechnym użyciu). Zrozumiałość danych można zwiększyć, wykorzystując tzw. metadane, czyli „dane opisujące dane”. Prostym przykładem jest swoista metryczka pliku wytworzonego przez Rektora UJ, pokazana na rysunku 17.3, mówiąca, że rodzaj tego dokumentu to zarządzenie (a nie np. decyzja).
17.4. Charakterystyki zależne od systemu 17.4.1. Dostępność (availability) Dostępność określa stopień, w jakim dane mogą zostać uzyskane/odzyskane przez uprawnionych użytkowników. Dostępność jest szczególnie istotna w systemach umożliwiających równoległy dostęp do danych wielu użytkownikom – o ile dostęp w trybie odczytu w większości przypadków może być nadany wszystkim uprawnionym użytkownikom jednocześnie, o tyle dostęp do zapisu określonej danej powinna mieć jedna osoba w jednym czasie.
17.4.2. Przenaszalność (portability) Przenaszalność to właściwość pozwalająca na instalowanie, zamianę lub przenoszenie danych z jednego systemu na inny z zachowaniem ich poziomu jakości. Przykładem może być przeniesienie pliku arkusza kalkulacyjnego z programu MS Excel do programu Open Office Calc. Zły poziom przenaszalności może objawić się tym, że dane podczas tej migracji zostaną utracone, błędnie sformatowane, zmieni się precyzja danych liczbowych itp. Cecha ta jest zależna od systemu, gdyż dane same w sobie nie mogą gwarantować przenaszalności – proces ten zawsze wykonywany jest przez jakiś zewnętrzny wobec danych proces.
17.4.3. Odtwarzalność (recoverability) Odtwarzalność to stopień, w jakim dane są chronione przed utratą oraz możliwość ich odzyskania w przypadku awarii. Przykładowym mechanizmem zapewniającym odtwarzalność jest stosowanie redundancji (np. dyski RAID) czy też polecenie ROLLBACK w języku SQL, którego przykład, pochodzący z podręcznika języka PL/SQL [169] jest podany na listingu 17.1. W bloku BEGIN – END informacja o pracowniku o employee_id = 120 jest uaktualniana w trzech różnych tabelach: emp_name, emp_sal i emp_job. Jeśli jednak instrukcja INSERT usiłuje zapisać numer id już istniejący w bazie, to zostanie rzucony wyjątek DUP_VAL_ON_INDEX. W takim przypadku w bloku EXCEPTION instrukcja ROLLBACK spowoduje cofnięcie zmian dokonanych na wszystkich tabelach, pozwalając odtworzyć stan bazy sprzed awarii.
CREATE TABLE emp_name AS SELECT employee_id, last_name FROM employees; CREATE UNIQUE INDEX empname_ix ON emp_name (employee_id); CREATE TABLE emp_sal AS SELECT employee_id, salary FROM employees; CREATE UNIQUE INDEX empsal_ix ON emp_sal (employee_id); CREATE TABLE emp_job AS SELECT employee_id, job_id FROM employees; CREATE UNIQUE INDEX empjobid_ix ON emp_job (employee_id); DECLARE emp_id emp_lastname emp_salary emp_jobid
NUMBER(6); VARCHAR2(25); NUMBER(8,2); VARCHAR2(10);
BEGIN SELECT employee_id, last_name, salary, job_id INTO emp_id, emp_lastname, emp_salary, emp_jobid FROM employees WHERE employee_id = 120; INSERT INTO emp_name VALUES (emp_id, emp_lastname); INSERT INTO emp_sal VALUES (emp_id, emp_salary); INSERT INTO emp_job VALUES (emp_id, emp_jobid); EXCEPTION WHEN DUP_VAL_ON_INDEX THEN ROLLBACK; DBMS_OUTPUT.PUT_LINE('Inserts have been rolled back'); END; / Listing 17.1. Skrypt PL/SQL z poleceniem ROLLBACK
1
Jest to rzeczywisty błąd, występujący na stronie MSW www.msw.gov.pl/pl/sprawy-obywatelskie/ewidencja-ludnosci-dowo/wydzialewidencji-ludn/4519,dok.html (dostęp 03.05.2014). Na stronie tej występuje więcej błędów, np. polskie znaki diakrytyczne w adresach mailowych czy brak odstępu między nazwą miasta a adresem mailowym (w przypadku Gdańska).
Część IV Zarządzanie testowaniem
In preparing for battle, I have always found that plans are useless but planning is indispensable Dwight D. Eisenhower
W tej części książki zajmiemy się zagadnieniami dotyczącymi jednej konkretnej roli w testowaniu: kierownika testów. Zarządzanie nie jest czynnością przesadnie twórczą, niektórzy uważają nawet, że jest zajęciem dosyć nudnym. Niezależnie od takich opinii, zarządzanie testowaniem – zwłaszcza w dużych, skomplikowanych projektach – jest koniecznością. Zarządzanie to radzenie sobie ze złożonością problemu. Dobry kierownik testów powinien cechować się wieloma umiejętnościami: powinien być dobrym strategiem i taktykiem, powinien umieć rozmawiać z ludźmi, wywierać wpływ, negocjować, planować, przewidywać, kontrolować, monitorować i podejmować trafne decyzje. Kierownik testów jest osobą pośredniczącą między zespołem testerów a kierownikiem projektu czy kierownictwem wyższego szczebla. W rozdziale 18 opiszemy, jak wygląda zarządzanie testowaniem w kontekście różnych uwarunkowań i ograniczeń projektowych. Omówimy również kwestie dotyczące różnic między zarządzaniem a przywództwem. Rozdział 19 jest poświęcony zarządzaniu opartemu na ryzyku, czyli najczęściej wykorzystywanemu podejściu analitycznemu do zarządzania. W rozdziale tym omówimy dokładnie fazy procesu zarządzania ryzykiem oraz techniki oparte na ryzyku, takie jak PRAM, SST, PRisMa, analiza zagrożeń, Cost of Exposure, FMEA, Quality Function Deployment, metoda drzewa usterek, metodologia TMap oraz TestGoal. W rozdziale 20 opiszemy pozostałe strategie testowania: oparte na wymaganiach, na modelu, metodyczne, oparte na standardzie, reaktywne, „good enough” i konsultacyjne. Rozdział 21 jest poświęcony dokumentacji związanej z testowaniem. Opiszemy najczęściej używane typy dokumentów wraz z konkretnymi przykładami ich użycia. Rozdział 22 dotyczy szacowania kosztu oraz pracochłonności testów. Przedstawimy typowe techniki szacowania oraz omówimy kwestię negocjacji zakresu testów. W rozdziale 23 omówimy czynności związane z nadzorem i kontrolą postępu testów. Wprowadzimy przykłady metryk, które pomagają kierownikowi testów w ocenie postępów prac zespołu testowego. Osobno opiszemy kwestię nadzoru i kontroli w przypadku testowania eksploracyjnego, które na pierwszy rzut oka trudno poddaje się procesowi monitorowania.
Rozdział 24 dotyczy biznesowej wartości testowania, czyli oceny zysków, jakie możemy uzyskać, stosując proces testowy w organizacji. Pokażemy, w jaki sposób, budując
odpowiednie
business-case’y,
można
przekonywać
kierownictwo
do
wspierania testowania i zwiększania budżetu na prace testowe. W rozdziale 25 omówimy zagadnienia związane z testowaniem przeprowadzanym poza naszym zespołem testowym: rozproszone, zakontraktowane i zewnętrzne. Przedstawimy zalety i wady tych podejść oraz sposoby zarządzania nimi. Krótki rozdział 26 dotyczy kwestii wdrażania standardów przemysłowych i norm do procesu testowego. Na zakończenie, w rozdziale 27 opiszemy, jak zarządzać incydentami, jak je śledzić i mierzyć, oraz co z tych informacji może uzyskać kierownik testów. Omówimy również kwestię profesjonalnego komunikowania incydentów.
18. Zarządzanie testowaniem w kontekście
Zarządzanie polega na nadzorowaniu wykonania procesu, który w zamyśle ma wnieść jakąś wartość dodaną do projektu czy dla organizacji. Testowanie jest takim procesem, ponieważ pozwala ocenić jakość wytwarzanego oprogramowania oraz wskazać jego słabe strony (defekty), które można naprawić, tym samym podnosząc jakość końcowego produktu oraz minimalizując ryzyko. Menedżer testów (zwany także kierownikiem testów) zajmuje się właśnie tym procesem. Testowanie nigdy nie odbywa się w próżni – jest zwykle częścią większego procesu, mianowicie procesu tworzenia oprogramowania. Z jednej strony proces testowy sam w sobie jest na tyle skomplikowany, że wymaga zarządzania. Z drugiej, ponieważ jest on składową innego procesu, występuje wiele czynników, warunków i ograniczeń, do których należy go dostosować. Kierownik testów jest więc odpowiedzialny nie tylko za wybór i implementację odpowiedniej strategii testowania, lecz także musi rozpoznać i zdefiniować wszystkich interesariuszy procesu testowego i zarządzać ich wymaganiami wobec testowania czy ogólnie jakości wytwarzanego oprogramowania. Jeśli w organizacji zdefiniowana jest strategia testowa lub inne wysokopoziomowe dokumenty określające standardy panujące w organizacji, to kierownik testów musi zapewnić, aby proces testowy był zgodny z tymi dokumentami. podejście do testu (ang. test approach) – implementacja strategii testów dla konkretnego projektu; zwykle zawiera decyzje podjęte na podstawie celów i analizy ryzyka projektu (testowego), punkty startowe procesu testowego, techniki projektowania testów, kryteria wyjścia i typy testów do wykonania
Wreszcie, proces testowy sam w sobie może być traktowany jako projekt, zatem podlega klasycznym ograniczeniom przedstawionym na rysunku 18.1: czasowi, budżetowi oraz zakresowi.
18.1. Kontekst ograniczeń projektowych Projekt testowy polega na wykonaniu wielu czynności, które w sumie dają określoną jakość, reprezentowaną polem trójkąta z rysunku 18.1. Długości boków trójkąta reprezentują ograniczenia projektowe. Trójkąt można zmieniać, manipulując długościami boków, jednak tak, aby jego pole nie przekroczyło określonej wartości. Rysunek ten można równoważnie przedstawić w postaci równania: ZAKRES ⋅ BUDŻET ⋅ CZAS = const. Kierownik testów nigdy nie ma nieograniczonych zasobów. Testowanie musi zakończyć się w określonym czasie i zamknąć w określonym budżecie. Nie możemy dowolnie manipulować wszystkimi trzema parametrami naraz – mając dwa z nich, trzeci jest dany, wynika z zadanych pozostałych dwóch ograniczeń. Na przykład, przy ustalonym budżecie i czasie jesteśmy w stanie wykonać określony zakres projektu (nie więcej). Mając zadany zakres i budżet, musimy poświęcić określoną ilość czasu (nie mniej) itd.
Rysunek 18.1. Klasyczny trójkąt ograniczeń projektowych Interesariusze, np. menedżerowie wyższego szczebla, często nie rozumieją istoty tych ograniczeń i żądają wykonania określonego zakresu testowania przy zadanym czasie i budżecie, co zwykle jest niewykonalne. W takiej sytuacji kierownik testów musi negocjować zmianę przynajmniej jednego z parametrów: zwiększenie budżetu, czasu lub zmniejszenie jakości. W negocjacjach tych są bardzo przydatne dane historyczne z poprzednich projektów, ponieważ wiadomo wtedy, jaka jest produktywność zespołu testowego, ile przeciętnie kosztuje stworzenie jednego testu i ile czasu to zajmuje.
18.2. Kontekst interesariuszy procesu testowego W procesie testowym interesariuszem jest każdy zainteresowany wewnętrzną czy też końcową jakością produktu (tzw. jakością użytkową). Kierownik testów musi zidentyfikować wszystkich interesariuszy procesu testowego oraz poznać ich oczekiwania i wymagania wobec tego procesu, a następnie sprawnie nimi zarządzać. Przykładowymi interesariuszami mogą być: właściciel projektu – jest to organizacja lub osoba, która finansuje projekt; często właścicielem jest po prostu klient, a w przypadku projektów wewnętrznych – własna organizacja; klient bezpośredni – czyli osoba lub organizacja zamawiająca program lub system podlegający testom; klient pośredni – w przypadku produktów z półki (COTS) klientem nie jest konkretna osoba, lecz zbiór osób będących potencjalnymi nabywcami i użytkownikami testowanego oprogramowania; testerzy – ponieważ to oni projektują i wykonują testy; outsorcingowe zespoły testerów – w przypadku, gdy organizacja zleca wykonanie całości lub części testów podmiotom zewnętrznym; kierownik projektu – odpowiada za całość projektu i jego powodzenie, a więc w szczególności za podproces testowy; analitycy biznesowi – czyli osoby tłumaczące wymagania klienta z języka biznesowego na język techniczny; wymagania powinny być testowalne i powinny temu testowaniu podlegać; projektanci – ich zaangażowanie w proces testowy polega na tym, że powinni dbać o testowalność kodu pisanego na podstawie ich projektów; deweloperzy – którzy sami często stosują testy jednostkowe, a ponadto wspólnie z zespołem testerów biorą udział w procesie zarządzania i usuwania defektów; wsparcie techniczne – czyli osoby wspierające użytkowników; muszą oni być świadomi, jakie błędy pozostały w wydanym oprogramowaniu i jak można je obejść lub załagodzić w przypadku ich wystąpienia. Lista ta nie wyczerpuje wszystkich możliwych interesariuszy. Niezależnie od tego kierownik testów musi zidentyfikować wszystkie osoby zainteresowane
procesem testowym, dobrze zrozumieć rolę każdego z nich oraz poznać jego wymagania w stosunku do procesu testowego i samego produktu. Struktura zależności między poszczególnymi rolami w projekcie może być bardzo skomplikowana (patrz rys. 18.2). Ponadto, kierownik testów musi określić: z którymi osobami, w jaki sposób i jak często się komunikować; co raportować, którym interesariuszom oraz na jakim poziomie abstrakcji; do kogo zwracać się w przypadku wystąpienia różnego rodzaju problemów (niektóre z nich mogą wymagać interwencji kierownika projektu, a inne można rozwiązać bezpośrednio z klientem). Bardzo ważną rolą kierownika testów jest wyważenie wszystkich oczekiwań interesariuszy wobec projektu testowego, często bowiem zdarza się, że wymagania te są sprzeczne i niemożliwe do zrealizowania w stu procentach. Często kierownik testów musi wykazać się umiejętnościami miękkimi w negocjacjach lub przekonywaniu interesariuszy, że niektóre z ich oczekiwań czy wymagań są niedobre lub wręcz niebezpieczne dla projektu. Na przykład wielu menedżerów ma pokusę, aby oceniać efektywność zespołu testowego przez użycie metryki liczby testów lub wykrytych defektów, co prowadzi do tworzenia wielu słabych, nieistotnych i redundantnych testów bądź do znajdowania nieistotnych błędów. Kierownik testów musi potrafić edukować interesariuszy i uświadamiać im istniejące ograniczenia oraz zagrożenia. Często wymaga to przedstawienia analizy ryzyka oraz zaproponowania jej jako podstawy dalszych działań.
Rysunek 18.2. Skomplikowana struktura zależności między rolami w projekcie
18.3. Kontekst produkcji oprogramowania Testowanie dotyczy programu lub systemu produkowanego poza procesem testowym, dlatego kierownik testów musi zaplanować i przeprowadzać wszystkie czynności testowe tak, aby były zgodne z nadrzędnym procesem wytwórczym. Należy pamiętać, że istnieje obustronna interakcja między tymi dwoma procesami: proces testowy wpływa na proces produkcji – np. przez wymuszanie wprowadzenia zmian po wykryciu defektu, wymuszenie określonego standardu implementacji zapewniającego testowalność kodu; proces produkcji wpływa na proces testowy – np. jeśli organizacja używa metodyki zwinnej wytwarzania oprogramowania, to kierownik testów powinien ustalić z kierownikiem deweloperów, w jaki sposób
testerzy powinni uczestniczyć w tym procesie (np. mając dostęp do testów jednostkowych mogą sugerować, jak je poprawić, aby uzyskać lepsze pokrycie lub bardziej zminimalizować istniejące ryzyko oraz mogą wykorzystać tę wiedzę o testach jednostkowych przy produkcji swoich testów, nie powielając tego, co już zostało przetestowane; ponadto, metodyka zwinna często jest związana z ciągłą integracją, co może powodować problemy z wykonywaniem testów regresji, jeśli jest ich bardzo dużo; szybko i często zmieniające się wymagania w procesie zwinnym stawiają pod znakiem zapytania sens automatyzacji testów itd.). Kierownik projektu powinien uczestniczyć (lub delegować testerów ze swojego zespołu) właściwie we wszystkich czynnościach procesu produkcji: w fazie analizy wymagań – aby pomóc w zapewnieniu testowalności wymagań oraz móc możliwie jak najwcześniej opracować plan testów akceptacyjnych i systemowych; w fazie projektowania architektury – aby wcześnie wykrywać błędy projektowe, zapewnić testowalność kodu tworzonego na podstawie projektu oraz inne cechy jakościowe testowanego programu, takie jak utrzymywalność czy przenaszalność; w fazie kodowania – aby skorelować wydań, wspomagać deweloperów przy uczestniczyć wspólnie z zespołem zarządzania defektami (defekty są
czynności testowe z datami testach jednostkowych oraz programistów w procesie zwykle poprawiane przez
deweloperów, dlatego musi istnieć dobra i profesjonalna komunikacja między nimi a testerami); w zarządzaniu projektem – ponieważ testowanie jest częścią projektu, kierownik testów raportuje do kierownika projektu, informując go o postępach, ryzykach i szacowaniach dotyczących samego procesu, a także jakości tworzonego oprogramowania; kierownik projektu wspólnie z kierownikiem testów powinni opracować także harmonogram czynności testowych oraz budżet zespołu testerów; w zarządzaniu konfiguracją, wydaniami i zmianą – kierownik testów lub delegowany przez niego przedstawiciel powinien uczestniczyć w pracach rady kontroli zmian (ang. Change Control Board, CCB),
w szczególności podczas szacowania w analizie wpływu, jak określona zmiana w wymaganiach lub konfiguracji wpłynie na poziom ryzyka oraz zakres testów regresji; w procesie dokumentowania – aby ustalić harmonogram i metody testowania dokumentacji oraz wspólnie z dokumentalistami zarządzać defektami znalezionymi w dokumentacji; w procesie wsparcia technicznego – aby dostarczyć zespołowi wsparcia technicznego odpowiednie produkty testowe (raporty itp.) oraz informację na temat tego, jakie błędy pozostały w wydanym systemie oraz jak można sobie z nimi radzić, jeśli przytrafią się klientowi; kierownik testów razem z kierownikiem wsparcia powinien przeanalizować błędy polowe, aby właściwie zaimplementować poprawę procesu testowego; kierownik testów może również (zwykle za pomocą działu zapewniania jakości) wykorzystać modele jakości oprogramowania i przewidywać liczbę błędów polowych oraz ryzyko, jakie niosą ze sobą, co jest podstawą do szacowania ilości zasobów w dziale wsparcia technicznego.
18.4. Kontekst cyklu życia oprogramowania Testowanie w cyklu życia omówiliśmy dokładnie w rozdziale 4. Z punktu widzenia kierownika testów istotne jest to, aby wiedział on, jakie czynności testowe oraz kiedy wykonać, mając narzucony określony cykl życia oprogramowania. Proces testowy powinien być dostosowany do przyjętego cyklu życia. Wybór metodyki wytwarzania oprogramowania może istotnie wpłynąć na poziom istotności różnych czynności testowych. Na przykład w metodykach zwinnych zazwyczaj nacisk na dokumentację jest o wiele mniejszy niż w modelach sekwencyjnych. Informacje o tym, jak uzgodnić testowanie z cyklem życia mogą być zawarte w strategii testów. Wtedy kierownik testów musi dostosować swój projekt do tych wymogów, chyba że z jakichś względów będą konieczne odstępstwa od strategii (w takim przypadku w planie testów należy wyraźnie to zaznaczyć wraz z uzasadnieniem odstępstwa). W szczególności strategia może określać poziomy testów. Jeśli ich nie określa, kierownik testów musi sam je zdefiniować w planie testów. Przyjęty cykl życia może wymuszać inne niż cztery standardowe poziomy
testów omówione w podrozdziale 4.3, np. testy integracji software–hardware czy testy integracji systemowej.
18.5. Kontekst testów Przyjęcie określonego typu testów rzutuje na kwestie zarządzania testami. Na przykład testy niefunkcjonalne są często drogie, a ponadto występuje bardzo dużo ich typów. Kierownik testów musi zdecydować, które będą odpowiednie dla danego projektu i które wykonać najpierw (priorytetyzacja). Ta decyzja musi oczywiście być zgodna ze strategią testów oraz zgodą odpowiednich interesariuszy. Testy niefunkcjonalne zwykle wykonuje się przy użyciu narzędzi, więc trzeba zaplanować zakup narzędzia, dokonać wyboru, skalkulować opłacalność wykorzystania narzędzia itd. (patrz rozdz. 30). Kierownik testów powinien pamiętać, że testy niefunkcjonalne nie muszą być wykonywane dopiero po testach funkcjonalnych. Wręcz przeciwnie – wiele testów niefunkcjonalnych można zacząć wcześniej i dzięki temu wcześniej wykryć poważne błędy. Priorytetyzacja powinna wynikać z analizy ryzyka (np. poziom użyteczności dla prototypu GUI albo wydajność systemu, który jeszcze nie ma GUI, ale z którym można już się komunikować). W przypadku testowania opartego na doświadczeniu kierownik testów musi mieć świadomość, że ten typ testów nie daje się łatwo dokumentować ani replikować. Decydując się na przeprowadzenie testowania opartego na doświadczeniu, należy pozostawić wolną rękę testerowi, ale należy dokładnie zaplanować czas trwania sesji. Istnieją natomiast metody zarządzania tego typu testami w zakresie nadzoru i kontroli, omówione w podrozdziale 23.3. Jeszcze jednym
problemem, przed jakim
staje kierownik
testów, jest
zarządzanie procesem testowym zleconym na zewnątrz (outsourcing). Kwestie te omówimy w rozdziale 25.
18.6. Kontekst czynnika ludzkiego Mówi się, że największym kapitałem organizacji są zatrudnieni w niej ludzie. Paradoksalnie, również ludzie stanowią największe źródło kłopotów i problemów na drodze do sukcesu. Kierownik testów musi zarządzać nie tylko procesem testowym, lecz także zespołem złożonym z różnych ludzi. Ich doświadczenie,
postawa, charakter oraz umiejętności wpływają znacząco na przyjmowanie przez kierownika testów odpowiednich metod zarządzania, kierowania i wspierania zespołu oraz wyboru metod pracy grupowej. Kwestie związane z zarządzaniem zasobami ludzkimi omówimy w rozdziale 28.
19. Testowanie oparte na ryzyku
W idealnym projekcie wszystko jest doskonale zaplanowane, a prace projektowe podążają dokładnie za planem. Wszystko się udaje, wszystko jest zrobione na czas i kosztuje dokładnie tyle, ile miało kosztować. Testowanie wykrywa wszystkie defekty tkwiące w oprogramowaniu, dzięki czemu klient otrzymuje produkt całkowicie wolny od błędów. Brzmi wspaniale? Niestety, nie ma projektów idealnych – w każdym występuje mnóstwo nieprzewidzianych zdarzeń, które negatywnie wpływają zarówno na postęp projektu, jak i na jakość produktu. Te wszystkie negatywne zdarzenia, które mogą, ale nie muszą wystąpić to ryzyka. Naszym celem zwykle jest dążenie do ideału, który opisaliśmy w poprzednim akapicie, dlatego naturalnym podejściem w zarządzaniu testowaniem jest testowanie oparte na ryzyku. Technika ta polega na identyfikacji możliwych ryzyk, szacowaniu ich wpływu na projekt, opracowaniu planów redukujących ryzyka oraz tzw. planów B, czyli gotowych do przeprowadzenia działań na wypadek rzeczywistego wystąpienia tych ryzyk. Wszystkie te czynności opiszemy w kolejnych rozdziałach. testowanie oparte na ryzyku (ang. risk based testing) – testowanie nastawione na wykrycie i dostarczenie informacji o ryzykach produktowych [59] Testowanie oparte na ryzyku jest jedną z najpowszechniejszych i ogólnie uznanych analitycznych technik zarządzania testowaniem. Dobrze przeprowadzone minimalizuje prawdopodobieństwo porażki projektu i pozwala na uzyskanie wysokiej jakości produktu końcowego. testowanie analityczne (ang. analytical testing) – testowanie oparte na systematycznej analizie np. ryzyk produktowych lub wymagań
19.1. Czym jest ryzyko? Ryzyko jest możliwością wystąpienia niepożądanego zdarzenia. Jego pojawienie się ma negatywny wpływ na produkt bądź proces. Nieodłączną cechą ryzyka jest jego probabilistyczny charakter, tzn. ryzyko może, ale nie musi wystąpić. W tej niepewności tkwi natura ryzyka i cały problem z nim związany. Gdybyśmy bowiem wiedzieli z góry, kiedy jakieś ryzyko wystąpi, moglibyśmy bardzo dokładnie przygotować się na jego nadejście. Jednocześnie, gdybyśmy wiedzieli z całą pewnością, że dane ryzyko w ogóle nie wystąpi, nie musielibyśmy się nim przejmować i moglibyśmy je zignorować. Niestety, ponieważ nie potrafimy przewidywać przyszłości, możemy jedynie szacować szanse wystąpienia danego ryzyka i tworzyć plany na wypadek jego wystąpienia. Nigdy jednak nie będziemy wiedzieli, czy plany te trzeba będzie wdrożyć w życie. Wystąpienie ryzyka może wpływać pośrednio lub bezpośrednio na postrzeganie przez klienta, użytkownika czy interesariusza jakości produktu lub projektu. Ponieważ, jak już wspomnieliśmy, bardzo trudno dokładnie oszacować prawdopodobieństwo wystąpienia ryzyka, a jeszcze trudniej wpływ jego negatywnych konsekwencji, ryzyko zwykle określane jest subiektywnie. Bardzo trudno ująć je w dokładne miary ilościowe, choć nie jest to niemożliwe – techniki szacowania ryzyka zaprezentujemy w podrozdziale 19.6. W
wielu
projektach
(zwłaszcza
wykorzystujących
zwinne
metodyki
wytwarzania) używa się jakościowego podejścia do ryzyka, tzn. nie określa się go liczbowo, ale w skali porządkowej (np.: znikome, małe, średnie, duże, bardzo duże). Choć skala porządkowa jest mniej dokładna od skali „liczbowej”, wykorzystanie tej pierwszej, zwłaszcza w tak trudnym zadaniu jak szacowanie ryzyka, nie tylko nie jest błędem, lecz także przez wielu jest wręcz zalecane. Skoro bowiem nie da się dokładnie (liczbowo) określić prawdopodobieństwa czy wpływu ryzyka, to lepiej operować na bardziej zgrubnych miarach, które przynajmniej pozwolą nam uszeregować ryzyka od tych najważniejszych po najmniej istotne. Skala porządkowa, stosowana w analizie jakościowej, umożliwia określanie kolejności oraz porównywanie ze sobą elementów tej skali. Więcej informacji o skalach pomiarowych Czytelnik znajdzie w podrozdziale 39.2. Istnieje wiele definicji ryzyka. Poniżej definicja stosowana przez ISTQB [6]: ryzyko (ang. risk) – możliwość wystąpienia czynnika powodującego negatywne konsekwencje, wyrażana przez prawdopodobieństwo wystąpienia oraz
potencjalny wpływ na produkt lub projekt Inny przykład to definicja pochodząca z raportu Heemstry i in. [170]: ryzyko – prawdopodobieństwo wystąpienia odchylenia od zamierzonego rezultatu Niezależnie od definicji ryzyka, zawsze musi zawierać ona w sobie element niepewności („możliwość” wystąpienia w definicji ISTQB czy „prawdopodobieństwo” wystąpienia w definicji Heemstry). wpływ ryzyka (ang. risk impact) – szkoda, jaka powstanie, jeżeli ryzyko zmaterializuje się jako rzeczywisty skutek lub zdarzenie prawdopodobieństwo ryzyka (ang. risk likelihood) – oszacowane prawdopodobieństwo, że ryzyko wystąpi jako wynik rzeczywisty lub zdarzenie Ryzyko ryzyku nierówne – jedne cechują się większym prawdopodobieństwem wystąpienia, inne mniejszym. Wystąpienie jednych może mieć duży wpływ na system, a innych – znikomy. Prawdopodobieństwo oraz wpływ są od siebie niezależne, tzn. nie istnieje zależność mówiąca, że np. im większe prawdopodobieństwo wystąpienia, tym większy wpływ ryzyka na system. Ryzyko może być bardzo prawdopodobne, ale o znikomym wpływie. Tak samo mogą istnieć ryzyka o bardzo niskim prawdopodobieństwie wystąpienia, ale o wpływie katastrofalnym.
Rysunek 19.1. Prawdopodobieństwo ryzyka a jego wpływ Sytuację tę zilustrowano na rysunku 19.1. Mamy tu przedstawionych sześć ryzyk. Ryzyko A cechuje się zarówno niskim prawdopodobieństwem wystąpienia, jak i znikomym wpływem. Tego typu ryzykami można zająć się w ostatniej kolejności lub nawet je zignorować. Ryzyko B, choć mało prawdopodobne, niesie ze sobą bardzo negatywny wpływ. Takim ryzykom należy się przyglądać. Tym bardziej należy zająć się ryzykiem F, które nie dość, że ma katastrofalny wpływ, to jest bardzo duża szansa, że się urzeczywistni. Ryzyko E jest przkładem ryzyka „uciążliwego” – nie powoduje wielkich strat czy kosztów, ale występuje bardzo często. Ryzyko C charakteryzuje się średnim prawdopodobieństwem wystąpienia i takim samym poziomem wpływu. Jest jednak mniej ważne od ryzyka D, które –
w stosunku do C – ma zarówno nieco większe prawdopodobieństwo, jak i nieco większy wpływ. Na podstawie tej analizy moglibyśmy stworzyć priorytetyzację: na pewno najwyższy priorytet ma ryzyko F, następnie grupa ryzyk B, C i D, potem E i na końcu A. Poziom ryzyka określa się zazwyczaj jako iloczyn prawdopodobieństwa wystąpienia ryzyka oraz wpływu, który nastąpi w przypadku zaistnienia ryzyka. Prawdopodobieństwo jest wielkością bezwymiarową, zatem poziom ryzyka jest wyrażony w jednostkach wpływu. W podejściu ilościowym wpływ wyraża się zwykle w kwotach pieniężnych. Wtedy poziom ryzyka opisuje potencjalną stratę finansową, jaka może nastąpić w przypadku urzeczywistnienia się ryzyk. W przypadku podejścia jakościowego poziom ryzyka jest określany na skali porządkowej. Ilościowe ujęcie ryzyk z rysunku 19.1 jest przedstawione w tabeli 19.1. Każde z ryzyk ma przypisane prawdopodobieństwo oraz wielkość wpływu, a poziom ryzyka to iloczyn tych wartości. Możemy zatem dokonać priorytetyzacji według poziomu poszczególnych ryzyk. Kolejność najważniejszych do najmniej istotnych) będzie następująca: F, D, B, C, E, A.
(od
poziom ryzyka (ang. risk level) – określenie istotności ryzyka zdefiniowane przez jego właściwości: wpływ i prawdopodobieństwo; poziom ryzyka może być użyty do określenia poziomu rodzaju bądź dokładności testów, które należy przeprowadzić Tabela 19.1. Ilościowe szacowanie ryzyka
W pływ Ryzyko Prawdopodobieństwo (w tys. USD)
Poziom ryzyka (w tys. Priorytet USD)
A
0.1
1 200
120
6
B
0.2
5 300
1 060
3
C
0.4
2 500
1 000
4
D
0.6
4 800
2 880
2
E
0.8
1 000
800
5
F
0.79
5 100
4 029
1
Właścicielem ryzyka produktowego jest zawsze biznes, nie zespół testowy. To interesariusze biznesowi są najbardziej zainteresowani tym, aby poziom ryzyka w produkcie był możliwie zminimalizowany. Osoby te są odpowiedzialne za podjęcie decyzji o tym, kiedy produkt jest wypuszczany na rynek lub do klienta, a zatem decydują o tym, że aktualny poziom ryzyka jest dopuszczalny. Informacje o tym poziomie dostają oni od zespołu zapewniania jakości lub zespołu testerów. Testerzy często popełniają błąd, wchodząc w rolę interesariusza biznesowego i decydując o poziomie akceptacji ryzyka produktowego. Należy pamiętać, że poziom ten zawsze powinien być określony przez właściciela projektu lub klienta, czyli interesariusza biznesowego. W tej kwestii zadaniem testera jest jedynie dostarczanie obiektywnych informacji o poziomie ryzyka. Tester nie powinien być w tym względzie osobą decyzyjną, natomiast czynności, które tester wykonuje w procesie testowym dopuszczalnego ryzyka.
powinny
uwzględniać
wspomniany
wyżej poziom
19.2. Zalety testowania opartego na ryzyku W podejściu opartym na ryzyku wszystkie czynności związane z testowaniem: planowanie, projektowanie, wykonanie oraz monitorowanie są odnoszone do poziomu ryzyka. Celem testowania staje się minimalizacja ryzyka rezydualnego (pozostałego w produkcie). Ten cel pośrednio wpływa na zwiększenie jakości końcowego produktu, ponieważ mniejsza liczba pozostałych ryzyk oznacza mniejsze prawdopodobieństwo awarii systemu. Testowanie oparte na ryzyku koncentruje się na pytaniu „co może pójść źle, jeśli nastąpi awaria?”. Takie podejście wymusza konieczność ustalenia możliwych ryzyk, to znaczy możliwych rodzajów awarii oraz przeanalizowanie wpływu ich pojawienia się – czynności te omówimy dokładnie w podrozdziałach 19.5 oraz 19.6. Następnie testowanie weryfikuje, czy poszczególne ryzyka rzeczywiście istnieją w systemie, czy też nie. Test zdany oznacza, że ryzyko związane z tym testem nie istnieje lub szansa jego pojawienia się jest mniejsza. Im więcej zdanych testów pokrywa coraz większy obszar ryzyk, tym bardziej wzrasta nasze przekonanie o tym, że te ryzyka nie stanowią zagrożenia.
Jedną z głównych zalet testowania opartego na ryzyku jest możliwość priorytetyzacji ryzyk, co pozwala zająć się w pierwszej kolejności tymi najpoważniejszymi. Takie podejście optymalizuje naszą pracę pod kątem maksymalizacji zapewnianej jakości. Bardzo często w projektach zostaje niewiele czasu na testowanie lub proces testowy jest zakańczany wcześniej ze względu na brak środków (tzw. squeeze on testing). W takich przypadkach, stosując priorytetyzację, efektywnie wykorzystujemy dany nam czas na testowanie. W pierwszej kolejności testujemy bowiem obszary o największym ryzyku. Co prawda nie przetestowaliśmy wszystkiego, co chcieliśmy, ale przynajmniej przetestowaliśmy najważniejsze, kluczowe aspekty produktu pod kątem wystąpienia najpoważniejszych zagrożeń. testowanie oparte na ryzyku (ang. risk based testing) – podejście do testowania mające na celu minimalizację ryzyka rezydualnego (pozostałego w produkcie) oraz informowanie interesariuszy o statusie wszystkich ryzyk, od momentu rozpoczęcia projektu do jego zakończenia; podejście to wymaga identyfikacji ryzyk oraz określenia poziomu każdego z nich w celu priorytetyzacji i właściwego ukierunkowania procesu testowego Testowanie oparte na ryzyku jest bardzo podobne do koncepcji testowania wystarczającego (ang. good enough testing) opisanej w punkcie 20.5.2. Metodologia ta opisuje sposób testowania w sytuacji, gdy osiągnięcie „idealnego” rozwiązania jest niemożliwe. Skoro cel uzyskania zero defektów jest nieosiągalny, jest nieunikniony pewien kompromis między poziomem jakości, wysiłkiem włożonym w testowanie i ograniczeniami projektowymi. Testowanie oparte na ryzyku wpasowuje się w strategię „good enough” w co najmniej trzech aspektach, odpowiadając na następujące pytania: czy wykonane testy wykazały, że dostarczona funkcjonalność jest poprawna i przynajmniej w minimalnym stopniu spełnia wymagania użytkownika? czy w systemie pozostały krytyczne defekty? Takie defekty należy usunąć przed wydaniem i wykazać – przez odpowiedni zbiór zdanych testów – że w systemie nie pozostał żaden znany defekt o znaczeniu krytycznym
czy mamy wystarczającą informację do podjęcia decyzji o wydaniu oprogramowania? Te trzy pytania dotyczą w gruncie rzeczy wyważenia między wystarczającą (good enough!) jakością, z akceptowalnym poziomem ryzyka, a ilością zużytych na ten cel zasobów [171]. Wyważenie to jest zilustrowane na rysunku 19.2. Zwykle jakości asymptotycznie zbiega do pewnej ustalonej wartości
przyrost
i zwiększanie zużycia zasobów w coraz mniejszym stopniu wpływa na poprawę tej jakości. Na przykład, zwiększenie zużycia zasobów z wielkości A do B = A + δZ daje wzrost jakości równy ∆Q1. Jednak takie samo zwiększenie zużycia zasobów dla większych wartości, tzn. z C do D = C + δZ daje wzrost jakości jedynie o ∆Q2, które jest dużo mniejsze niż ∆Q1. Generalnie, wraz ze wzrostem nakładów, aby zwiększyć poziom jakości o stałą wartość trzeba zużywać coraz więcej zasobów. Oczywiście po określonym czasie przestanie się to opłacać. Jeśli chcielibyśmy osiągnąć maksymalny poziom jakości, to musielibyśmy zużyć bardzo dużą ilość zasobów. Jednak po co to robić, skoro o wiele mniejszymi nakładami (np. na poziomie D z rys. 19.2) możemy osiągnąć prawie maksymalny poziom jakości? Celem jest więc wyważenie obu tych wartości, tzn. przyjęcie odpowiednio dużego, ale akceptowalnego poziomu zużycia, które da niekoniecznie maksymalny, ale za to akceptowalny poziom jakości.
Rysunek 19.2. Zależność między wielkością zużycia zasobów a osiąganym poziomem jakości W podejściu opartym na ryzyku testowanie uwzględnia ryzyko na trzy następujące sposoby [171]: testowanie ukierunkowane (ang. targeted testing), tzn. określanie poziomu wysiłku testowego, wybór technik testowania oraz retestowanie w sposób odpowiedni dla poziomu ryzyka związanego z danym, zidentyfikowanym ryzykiem produktowym (czyli testowanie rzeczy bardziej krytycznych powinno być dokładniejsze); priorytetyzacja testów (ang. prioritized testing), tzn. priorytetyzacja większych, ważniejszych ryzyk i testowanie obszarów z nimi związanych wcześniej niż obszarów obarczonych ryzykiem mniejszym; raportowanie wyników testów oraz statusu procesu testowego i jakości oprogramowania w terminach ryzyka rezydualnego, czyli ryzyka, które pozostaje w systemie z powodu nie wykonania odpowiednich testów pokrywających te ryzyka.
Wszystkie te trzy rodzaje radzenia sobie z ryzykiem powinny występować podczas całego cyklu wytwarzania oprogramowania, nie tylko na początku i końcu projektu (jak to często bywa w praktyce). Poziom danego ryzyka może zmieniać się w czasie. Zespół testowy powinien okresowo przeglądać listę ryzyk oraz uaktualniać ich parametry, gdyż zarówno prawdopodobieństwo wystąpienia, jak i wpływ danego ryzyka może się zmieniać na skutek przeprowadzenia określonych testów, odsetka zdanych testów, reewaluacji ryzyk itp. W trakcie prac wytwórczych mogą również pojawiać się nowe, nieznane dotąd ryzyka – w takich sytuacjach należy je dodać do listy ryzyk, oszacować ich prawdopodobieństwo i wpływ, oraz uaktualnić priorytetyzację uwzględniając te nowe zagrożenia. Stosując podejście oparte na ryzyku, należy pamiętać o tym, że testowanie wyłącznie na podstawie ryzyk zdefiniowanych w fazie identyfikacji ryzyka może spowodować, że przeoczymy jakiś istotny obszar i nie przetestujemy go lub zrobimy to niewystarczająco dokładnie. Dlatego zawsze należy pozostawić pewną swobodę zespołowi testowemu, który może np. zastosować testowanie eksploracyjne czy inne techniki oparte na doświadczeniu omówione w rozdziale 10.
19.3. Rodzaje ryzyka Ryzyka można podzielić na dwa główne rodzaje: produktowe (zwane też ryzykiem jakościowym); projektowe. Ryzyka produktowe to te zagrożenia, których urzeczywistnienie się ma negatywny wpływ przede wszystkim na wytwarzany produkt. Przykładem ryzyka produktowego może być defekt w oprogramowaniu, który powoduje zawieszanie się systemu. Ryzyka projektowe mają z kolei bezpośredni wpływ na powodzenie samego projektu. Przykładem ryzyka projektowego może być np. brak wykwalifikowanej kadry, spowodowany np. zwolnieniem się kilku osób z firmy. Sam fakt braku ludzi nie ma bezpośredniego wpływu na jakość produktu, ale może spowodować opóźnienie projektu w czasie.
ryzyko produktowe, ryzyko jakościowe (ang. product risk, quality risk) – ryzyko bezpośrednio powiązane z przedmiotem testów, np. atrybutem jakościowym ryzyko projektowe (ang. project risk) – ryzyko związane z zarządzaniem i kontrolą projektu (testowego), np. braki zasobów, napięty harmonogram, zmieniające się wymagania itp. Praktycznie nie da się wymienić wszystkich ryzyk, jakie mogą wystąpić w projekcie. W tabeli 19.2 są podane przykłady ryzyk produktowych oraz projektowych. Tabela 19.2. Przykłady ryzyk produktowych i projektowych
RYZYKA PRODUKT OW E (JAKOŚCIOW E) wys oka złożonoś ć s truktury modułu (zwięks za prawdopodobieńs two defektu) s komplikowana architektura (pog ars za pielęg nowalnoś ć s ys temu) niewykorzys tywanie lub błędne wykorzys tywanie wzorców projektowych podczas kodowania (pog ars za lub uniemożliwia tes towalnoś ć) nowi, niedoś wiadczeni prog ramiś ci w zes pole (zwięks zają ryzyko wprowadzenia defektów) nowe technolog ie wykorzys tywane w projekcie
RYZYKA PROJEKT OW E
ś rodowis ko tes towe i g otowoś ć narzędzi (może opóźnić proces tes towy) dos tępnoś ć pers onelu tes ters kieg o i jeg o kwalifikacje (może wpłynąć neg atywnie na jakoś ć tworzonych tes tów oraz opóźnić tes towanie) nis ka jakoś ć artefaktów tes towych (może wprowadzać w błąd kadrę zarządzającą przy podejmowaniu decyzji dotyczących produktu) zbyt duże zmiany w zakres ie lub definicji produktu (może opóźnić
(zwięks zają np. ryzyko błędów integ racji) brak przes zkolenia w przeprowadzaniu ins pekcji formalnych (zwięks za prawdopodobieńs two niezauważenia defektów podczas analizy s tatycznej kodu) napięte harmonog ramy (powodują s tres , niedokładnoś ć działania i więks ze s zans e na popełnienie pomyłek) nowe reg ulacje prawne (ryzyko błędneg o działania prog ramu)
proces tes towy i zwięks zyć kos zty fazy tes towania) dos tępnoś ć wykwalifikowanych członków zes połu (może opóźnić projekt) konflikty w zes pole (mog ą obniżyć morale zes połu, a co za tym idzie jeg o efektywnoś ć) brak ws parcia kadry zarządzającej (utrudnia prowadzenie projektu, jeg o finans owanie, rozwiązywanie konfliktów; w eks tremalnym przypadku może doprowadzić do anulowania projektu)
19.4. Zarządzanie ryzykiem w cyklu życia Z punktu widzenia zarządzania, w testowaniu opartym na ryzyku można wyróżnić cztery fazy: identyfikacja ryzyka; analiza ryzyka; łagodzenie ryzyka; monitorowanie ryzyka. Dwie pierwsze tradycyjnie postrzega się jako czynności związane z planowaniem i szacowaniem ryzyka (ang. risk assessment), natomiast dwie ostatnie – jako czynności związane z kontrolą ryzyka. Schematycznie model zarządzania ryzykiem jest pokazany na rysunku 19.3.
Rysunek 19.3. Model zarządzania ryzykiem Choć na rysunku poszczególne aktywności są narysowane jako odrębne fazy, w praktyce czynności identyfikacji, analizy, łagodzenia i monitorowania istotnie na siebie zachodzą. Dzieje się tak z kilku powodów. Proces zarządzania ryzykiem rozciąga się na cały okres trwania projektu. Co jakiś czas pojawiają się nowe ryzyka, które należy przeanalizować i oszacować ich poziom oraz opracować metody ich łagodzenia. Inne ryzyka mogą już wystąpić i w stosunku do nich należy wdrożyć plany naprawcze. Przy przejściu do kolejnej fazy projektowej może zajść potrzeba identyfikacji nowych ryzyk, związanych z daną fazą lub obszarem, którego dotyczyć będzie ta faza. Przy tym wszystkim na bieżąco, w sposób ciągły musimy monitorować i raportować poziom ryzyka. zarządzanie ryzykiem (ang. risk management) – systematyczne wdrażanie procedur i praktyk dla zadań identyfikacji, analizowania i ustalania priorytetów oraz kontrolowania ryzyka W idealnym przypadku proces zarządzania ryzykiem produktowym i projektowym powinien być opisany w wysokopoziomowym dokumencie, takim jak polityka testów czy strategia testów. Dokument taki powinien również
określać, w jaki sposób zarządzanie ryzykiem należy zintegrować z cyklem wytwórczym oprogramowania w organizacji bądź w poszczególnych projektach. W organizacjach o dojrzałym procesie wytwarzania proces identyfikacji ryzyk nie kończy się na ich wypisaniu. Dokonuje się pogłębionej analizy tak, aby dotrzeć do źródła – pierwotnej przyczyny powstawania ryzyk, stosując tzw. analizę przyczyny źródłowej (ang. root cause analysis). Usunięcie tych pierwotnych przyczyn nie tylko minimalizuje prawdopodobieństwo i/lub wpływ poszczególnych ryzyk, lecz także jest istotnym elementem ulepszania procesu wytwórczego bądź testowego. analiza przyczyny źródłowej, analiza przyczyny podstawowej (ang. root cause analysis) – technika analizy zorientowana na identyfikację podstawowych przyczyn defektów; przez wprowadzenie ukierunkowanych miar na podstawowe przyczyny defektów oczekuje się, że prawdopodobieństwo ponownego wystąpienia defektu będzie zminimalizowane W kolejnych czterech podrozdziałach opiszemy dokładniej poszczególne fazy zarządzania ryzykiem. W ostatnim podrozdziale omówimy istniejące techniki analizy ryzyka.
19.5. Identyfikacja ryzyka Identyfikacja ryzyka to próba możliwie najdokładniejszego określenia, co może pójść źle oraz kiedy i gdzie to się może wydarzyć. Jest to kluczowa faza dla całego procesu zarządzania opartego na ryzyku, ponieważ zarówno analizę, łagodzenie, jak i monitorowanie ryzyka możemy wykonywać dla znanych nam ryzyk. Identyfikacja może dotyczyć różnych typów ryzyk, związanych z typem testów, których wykonanie będzie minimalizować te ryzyka. typ ryzyka, kategoria ryzyka (ang. risk type, risk category) – specyficzna kategoria ryzyka, związana z typem testów, które należy przeprowadzić w taki sposób, aby świadomie je ograniczać i łagodzić (np. ryzyko związane z interfejsem użytkownika może być łagodzone przez przeprowadzenie testów użyteczności)
Produktem końcowym fazy identyfikacji jest lista ryzyk. Lista ta powinna być możliwie kompletna. Oczywiście nigdy nie mamy gwarancji, że zidentyfikowaliśmy wszystkie możliwe ryzyka, niemniej jednak należy starać się znaleźć ich tak dużo, jak to tylko możliwe. Aby osiągnąć ten cel, kierownik testów lub inna osoba odpowiedzialna za zarządzanie ryzykiem musi zapewnić, aby w pracach nad identyfikacją ryzyka brali udział wszyscy interesariusze. Każda osoba ma bowiem inne spojrzenie na system i oczekuje od niego innych rzeczy. Uzyskanie możliwie różnorodnego spojrzenia na produkt umożliwi zdefiniowanie większej liczby ryzyk. identyfikacja ryzyka (ang. risk identification) – proces identyfikacji ryzyk wykorzystujący takie techniki jak burza mózgów, listy kontrolne, historia awarii
19.5.1. Analiza interesariuszy (stakeholder analysis) W dużych projektach, w których tworzony produkt będzie oddziaływać na wiele osób identyfikację interesariuszy można przeprowadzić w postaci formalnego procesu, zwanego analizą interesariuszy (ang. stakeholder analysis). Proces ten wchodzi w skład obszaru zarządzania interesariuszami. Interesariuszem jest każda osoba lub organizacja, która wpływa na projekt, na którą projekt może mieć wpływ lub która jest w jakikolwiek sposób zainteresowana powodzeniem danego przedsięwzięcia. Ze względu na stopień wpływu na projekt, interesariuszy można podzielić na: głównych interesariuszy (ang. primary stakeholders), czyli osoby lub organizacje bezpośrednio zaangażowane w projekt (np. programiści, testerzy, analitycy biznesowi, użytkownik końcowy oprogramowania); pobocznych interesariuszy (ang. secondary stakeholders), czyli osoby lub organizacje, na które wpływ projektu jest pośredni (np. kierownictwo działu finansowego firmy wdrażającej system finansowo-księgowy nadzoruje pracowników tego działu, którzy bezpośrednio będą z tego systemu korzystać; program ten oddziałuje bezpośrednio na pracowników, ale pośrednio na ich kierownika, który nie jest bezpośrednim użytkownikiem programu);
kluczowych interesariuszy (ang. key stakeholders), czyli osoby lub organizacje niezbędne dla powodzenia przedsięwzięcia, których wpływ na projekt jest kluczowy (np. sponsor projektu lub klient zamawiający oprogramowanie). Interesariuszy można również podzielić ze względu na stopień powiązania z projektem: interesariusze wewnętrzni (np. pracownicy organizacji wytwarzającej oprogramowanie, kierownictwo tych organizacji); interesariusze powiązani (np. dostawcy, sprzedawcy, pracownicy działu obsługi klienta, wdrożeniowcy, pracownicy działu finansowego lub/i prawnego, dystrybutorzy); interesariusze zewnętrzni (np. użytkownicy, agencje rządowe, urzędy kontroli, prasa/media, społeczeństwo). Z punktu widzenia procesu tworzenia oprogramowania interesariuszy można podzielić ze względu na rolę, jaką odgrywają w tym procesie. Oto przykładowe role: kierownik projektu; sponsor projektu (osoba lub organizacja finansująca projekt); użytkownicy bezpośredni (osoby, które będą bezpośrednio używać oprogramowania); użytkownicy pośredni (osoby, które będą kupować nasz produkt sprzedawany jako „oprogramowanie z półki”); klient (osoba lub organizacja zamawiająca produkt); analityk biznesowy (osoba, która tłumaczy wymagania klienta wyrażone w języku biznesowym na język techniczny, zrozumiały dla zespołu; architekt (osoba, która tworzy projekt architektury systemu); programista (osoba, która na podstawie dokumentu wymagań oraz projektu tworzy kod); tester (osoba, która testuje tworzony kod oraz wszystkie artefakty procesu wytwórczego); kierownictwo (np. kierownik testów, kierownik programistów);
kierownictwo wyższego szczebla (dyrektor IT, kierownik działu, prezes); ekspert (osoba mająca wiedzę dziedzinową lub biznesową pokrywającą obszar dziedzinowy projektu, np. ekspert z dziedziny prawa, bankowości, standardów przemysłowych lub osoba mająca wiedzę związaną z charakterystykami jakościowymi oprogramowania, np. ekspert użyteczności, ekspert ds. bezpieczeństwa); wsparcie techniczne (osoby, które będą współpracowały po stronie producenta lub dystrybutora oprogramowania z klientem używającym tego oprogramowania, rozwiązując jego problemy); dokumentalista (osoba tworząca dokumentację użytkową). Każdy z tych interesariuszy będzie miał inne spojrzenie na system, inne priorytety co do systemu, inne wymagania, inny poziom władzy oraz inny stopień zainteresowania projektem. Każdy z nich będzie również w stanie zidentyfikować innego rodzaju ryzyka produktowe oraz projektowe.
19.5.2. Technika „władza versus zainteresowanie” Istnieje wiele formalnych technik analizy interesariuszy (zob. np. [172]). Jedną z nich jest technika o nazwie „władza versus zainteresowanie” (ang. power versus interest). Pozwala ona zidentyfikować interesariuszy, których interesy oraz podstawy władzy muszą być wzięte pod uwagę podczas rozwiązywania problemu. W naszym przypadku problemem jest identyfikacja ryzyk.
Rysunek 19.4. Macierz „władza versus zainteresowanie” Technika polega na skonstruowaniu macierzy przedstawionej na rysunku 19.4. Składa się ona z czterech części, podzielonych ze względu na dwa kryteria: stopień władzy, jaką ma interesariusz oraz stopień jego zainteresowania projektem. Używając techniki burzy mózgów, identyfikuje się wszystkich interesariuszy i umieszcza ich nazwiska w odpowiednich polach macierzy. W ten sposób tworzy się cztery grupy interesariuszy. Każda z tak stworzonych grup wymaga odmiennego podejścia i traktowania. Z punktu widzenia analizy ryzyka najważniejszymi grupami są „podmioty” oraz „kluczowi gracze”. Osoby przypisane do tych kategorii będą najbardziej
wartościowymi interesariuszami pod kątem identyfikacji ryzyk. Kluczowi gracze to osoby o dużej władzy, a tę ma zwykle kierownictwo wyższego szczebla, dlatego będą oni zwykle potrafili określać ryzyka projektowe. „Podmioty” z kolei będą się skupiać na ryzykach produktowych, czyli tych, które z punktu widzenia testowania interesują nas najbardziej. „Tłum” to osoby, których projekt nie dotyczy bezpośrednio i które nie będą zbytnio w niego zaangażowane. Przykładem może być dział prawny zajmujący się kwestiami licencji na tworzone przez firmę oprogramowanie. Osoby z działu prawnego nie muszą brać bezpośrednio udziału w procesie identyfikacji ryzyk, natomiast powinny być informowane o wszystkich działaniach, które mogą mieć związek z kwestiami prawno-legislacyjnymi. „Kreatorzy kontekstu”, podobnie jak „tłum”, nie są zbyt zainteresowani projektem, ale mają za to dużą władzę. Mogą dostarczyć informacji na temat ryzyk projektowych, ponieważ są to osoby, które zwykle mają wgląd w całość prac organizacji i potrafią zidentyfikować ryzyka o charakterze strategicznym, tzn. długofalowe lub związane z działalnością całej organizacji i mające przez to przełożenie na nasz projekt. Należy pamiętać, że testerzy jedynie przeprowadzają proces identyfikacji ryzyk – samo ryzyko należy do biznesu. Jeśli podczas procesu identyfikacji ryzyk część z nich zostanie pominięta, to zawsze jest odpowiedzialny za to biznes – nigdy tester.
19.5.3. Techniki identyfikacji ryzyk Istnieje wiele technik identyfikacji ryzyk. Każda skupia się na trochę odmiennym podejściu do procesu identyfikacji zagrożeń. Nie wszystkie ryzyka są oczywiste i nie wszystkie mogą zostać zidentyfikowane w ten sam sposób. Wybór odpowiedniej techniki zależy od wielu czynników, m.in. od stopnia dojrzałości organizacji, kwalifikacji i doświadczenia osób uczestniczących w procesie identyfikacji ryzyk, dostępnego czasu oraz tego, w którym momencie cyklu życia proces identyfikacji jest przeprowadzany. Najczęściej spotykane techniki identyfikacji ryzyka to: Technika oparta na wymaganiach – przydatna, jeśli mamy dobrze określony zbiór wymagań. Każde wymaganie jest w pewien sposób związane z odpowiadającym mu ryzykiem, ponieważ niespełnienie lub niewystarczające spełnienie wymagania oznacza problem, a więc jest
ryzykiem. Większość wymagań można przeformułować jako ryzyka (np.
wymaganie
„system
powinien
odpowiadać
na
zapytanie
użytkownika w ciągu 5 sekund” jest związane z ryzykiem „czas działania – system może działać zbyt wolno”). Burza mózgów – czyli spotkanie, na którym w krótkim czasie ludzie w nim uczestniczący mogą wygenerować wiele różnych pomysłów. Podstawową zasadą burzy mózgów jest to, że nie można krytykować pomysłów wymyślanych przez innych uczestników, ponieważ każdy, nawet najgłupszy czy najbardziej absurdalny pomysł może być dla innego uczestnika spotkania bodźcem do wymyślenia bardziej sensownego pomysłu. Wymyślone przez uczestników ryzyka są zapisywane w postaci krótkiej nazwy oraz wskazania jego źródła. Na etapie burzy mózgów nie dokonuje się analizy ryzyka – jedynie identyfikację. Nacisk jest położony na wymyślenie jak największej liczby pomysłów, ich jakość jest drugorzędna. Więcej informacji na temat burzy mózgów oraz innych technik pracy grupowej jest podane w rozdziale 29. Wywiady eksperckie – metoda stosowana, gdy interesariusze mogący wziąć udział w burzy mózgów lub innej technice są niedostępni. Polega ona na przeprowadzaniu kilkudziesięciominutowych wywiadów (zwykle jest to ok. 30–40 minut) z kadrą zarządzającą, zewnętrznymi konsultantami, ekspertami dziedzinowymi, doświadczonymi kierownikami projektów bądź kierownikami testów. Ważne jest, aby osoba, z którą przeprowadza się wywiad, była dobrze poinformowana o aktualnym statusie projektu oraz jego specyficznej charakterystyce. Wadą tej techniki jest brak interakcji pobudzającej do kreatywności, jaka ma miejsce np. w przypadku burzy mózgów. Niezależna ocena ryzyka – czyli dokonanie oceny przez doświadczonych, ale niezależnych ekspertów. Niezależność nie oznacza, że ekspert musi pochodzić spoza organizacji, ale że nie ma żadnego interesu w powodzeniu projektu. Innymi słowy, ekspert nie powinien być interesariuszem projektu. Listy kontrolne – stosowane, aby uniknąć „odkrywania koła na nowo”. Listy kontrolne zawierają spis możliwych ryzyk. Metoda polega na przeglądaniu listy kontrolnej i decydowaniu, czy poszczególne ryzyka
występujące na niej mogą się urzeczywistnić w naszym projekcie, czy w ogóle go nie dotyczą. Innymi słowy, wybieramy podzbiór interesujących
nas
ryzyk.
Istnieje
wiele
ogólnodostępnych
list
kontrolnych, z których można skorzystać. Można również posługiwać się swoją własną listą kontrolną, budowaną na podstawie doświadczeń z poprzednich projektów. Przed zastosowaniem metody należy upewnić się, czy elementy znajdujące się na liście kontrolnej mają odpowiedni poziom abstrakcji. Listy kontrolne mogą dotyczyć w szczególności takich zagadnień jak: zgodność ze standardami, możliwe wewnętrzne i zewnętrzne zagrożenia dla sprzętu, oprogramowania, danych lub zasobów ludzkich, kryteria akceptacji itp. [171]. Doświadczenie z poprzednich projektów (ang. lessons learned). Jest to bardzo ważna i skuteczna technika, zwłaszcza w przypadku organizacji, w których przeprowadzono wiele projektów w podobny sposób. Doświadczenie nabyte podczas pracy przy tych projektach jest bezcenne. Technika ta wymaga skrupulatnego zbierania danych w czasie trwania całego projektu. Dobrym momentem na zebranie takich danych jest np. spotkanie retrospektywne czy spotkania na zakończenie poszczególnych faz bądź kamieni milowych projektu. Często stosowaną praktyką jest mieszanie różnych technik identyfikacji ryzyk. Na przykład stosowanie listy kontrolnej można połączyć z burzą mózgów, traktując ryzyka występujące na liście jako „wyzwalacze” kreatywnych pomysłów uczestników sesji. Takie podejście można traktować w ogóle jako osobną metodę, zwaną „warsztatami ryzyka” (ang. risk workshops). niezgodność (ang. non-conformity) – niespełnienie konkretnego wymagania [1]
19.5.4. Przykłady ryzyk produktowych Black [173] podaje obszerną listę ryzyk, w formie listy kontrolnej. Przytoczymy ją tu w całości, ponieważ jest ona bardzo przydatna w praktycznych sesjach identyfikacji ryzyk. Black grupuje ryzyka ze względu na poziom testowania: Poziom testów jednostkowych:
stany (czy zachowania stanów modułu są poprawne? czy przejścia ze stanu do stanu następują podczas odpowiednich zdarzeń/wyzwalaczy? czy są wyzwalane prawidłowe akcje? czy z kazdym wejściem są związane poprawne zdarzenia?); transakcje (czy moduł poprawnie przetwarza transakcje? czy nie ma żadnych efektów ubocznych?); pokrycie kodu (jakie instrukcje, rozgałęzienia, decyzje, warunki, pętle, ścieżki oraz inne elementy przepływu sterowania mogą powodować awarie?); pokrycie przepływu danych (jakie przepływy danych w module, do modułu lub z modułu mogą powodować natychmiastowe lub opóźnione awarie? jaki typ przepływu może być za to odpowiedzialny – przekazanie wartości przez parametr, obiekt, zmienną globalną, trwałą strukturę danych, np. plik lub tabela bazodanowa?); funkcjonalność (czy funkcjonalność dostarczana przez ten moduł jest poprawna? czy może powodować efekty uboczne?); interfejs użytkownika (jeśli komponent wchodzi z użytkownikiem, czy użytkownik może mieć
w interakcję problemy ze
zrozumieniem komunikatów, informacji, żądań systemu? czy kolorystyka, grafika i inne elementy projektowe są odpowiednie dla użytkownika?); zużycie mechaniczne (czy komponent sprzętowy może się zużyć lub zepsuć podczas intensywnego używania?); jakość sygnału (czy w przypadku komponentów sprzętowych sygnały przez nie przetwarzane są odpowiedniej jakości? czy są prawidłowe?). Poziom testów integracyjnych: interfejsy komponentów lub podsystemów (czy interfejsy między komponentami są poprawnie zdefiniowane? jakie problemy mogą wystąpić podczas bezpośredniej i pośredniej interakcji komponentów?); funkcjonalność (czy mogą wystąpić efekty uboczne w wyniku interakcji komponentów? czy interfejsy obsługują całkowicie żądaną funkcjonalność?); pojemność i przepustowość (czy pamięć operacyjna i dyskowa wystarczają do przechowywania niezbędnych danych? czy medium
przekazu danych takie jak np. sieć ma odpowiednią przepustowość?); obsługa wyjątków i odtwarzanie po awarii (czy zintegrowany komponent zachowuje się poprawnie zarówno podczas typowych, jak i ekstremalnych sytuacji? czy komponent radzi sobie poprawnie z obsługą błędów, jakie mogą się pojawić? czy komponent potrafi powrócić do normalnego działania w przypadku awarii?); jakość
danych
(czy
system
potrafi
przechowywać,
ładować,
modyfikować, archiwizować i przetwarzać dane w sposób niezawodny, nie uszkadzając ich ani nie tracąc?); wydajność (czy mogą wystąpić jakieś problemy z czasem odpowiedzi, optymalną alokacją i zużyciem zasobów itp.?); interfejs użytkownika (jak w przypadku testów jednostkowych). Poziom testów systemowych i akceptacyjnych: funkcjonalność (czy funkcjonalność „end-to-end” jest zapimlementowana poprawnie? czy kombinacje funkcji programu poprawnie ze sobą współpracują?); interfejs użytkownika i użyteczność (czy system jest spójny na poziomie całego oprogramowania? czy interfejs jest zrozumiały dla użytkownika?); stany i transakcje poprawnie?); jakość danych (jw., i akceptacyjnych);
(czy ale
system na
obsługuje
poziomie
odpowiednie
testów
stany
systemowych
operacje (złożone systemy często wymagają administracji; czy mogą pojawić się problemy związane z utrzymaniem systemu, z migracją danych, z archiwizacją, z przydzielaniem uprawnień użytkownikom, z tworzeniem kopii zapasowych itp.?); wydajność i pojemność (czy są problemy z czasem odpowiedzi? czy system dobrze działa pod dużym obciążeniem? czy występują kłopoty z przepustowością łącza?); niezawodność, dostępność, stabilność (czy system podlega awarii podczas normalnego użytkowania? czy podczas dużego i ekstremalnego obciążenia? czy system może być niedostępny? czy wszystkie funkcje są stabilne?);
obsługa wyjątków i odtwarzanie po awarii (jw., ale na poziomie testów systemowych i akceptacyjnych); obsługa daty i czasu (czy zdarzenia związane z czasem mogą być awaryjne? czy funkcje używające daty/czasu dobrze współpracują ze sobą? czy sytuacje takie jak rok przestępny lub zmiana czasu (letni na zimowy bądź na odwrót) mogą powodować jakieś problemy? co ze strefami czasowymi?); lokalizacja (czy są poprawnie wspierane strony kodowe dla różnych języków?); sieć (czy opóźnienia, przepustowość lub inne czynniki związane z siecią mogą powodować problemy?); kompatybilność (czy system może być niekompatybilny z innym oprogramowaniem, sprzętem, systemem operacyjnym lub środowiskiem?); standardy (czy system używa jakichś standardów i czy może je naruszyć?); bezpieczeństwo (czy użytkownicy w ramach przydzielonych uprawnień mogą podjąć akcje wymagające innych uprawnień? czy mogą uzyskać dostęp do zasobów, do których nie powinni mieć dostępu? czy dane są szyfrowane tam, gdzie jest to konieczne? czy system jest podatny na ataki bezpieczeństwa?); środowisko (w przypadku systemów sprzętowych, czy działanie środowiska może powodować awarie systemu? czy czynniki fizyczne, takie jak temperatura, wilgoć czy zanieczyszczenia, mogą spowodować trwałą lub tymczasową awarię?); zużycie energii (czy są problemy ze zbyt wysokim zużyciem energii w systemach sprzętowych? czy wahania napięcia mogą spowodować awarie?); dokumentacja (czy dokumentacja zawiera błędy, nieścisłości? czy jest kompletna i pomocna? czy jest spójna? czy jest napisana zrozumiałym językiem?); pielęgnowalność (czy można zauktualizować system? czy można bezproblemowo zainstalować łatki? czy można usunąć niepotrzebne lub doinstalować nowe funkcjonalności bez powodowania awarii?).
19.6. Analiza ryzyka Po dokonaniu identyfikacji możliwych ryzyk należy je przeanalizować, to znaczy oszacować ich poziom, czyli efekty potencjalnego ich wystąpienia. O ile identyfikacja polega na dostarczeniu jak największej ilości ryzyk, o tyle analiza pozwala na ich skategoryzowanie bądź uszeregowanie od najważniejszych do najmniej istotnych. Jak już wcześniej wspomnieliśmy, poziom ryzyka wyraża się najczęściej jako iloczyn prawdopodobieństwa jego wystąpienia oraz wpływu, jaki faktyczne wystąpienie ryzyka będzie miało na projekt bądź produkt: Poziom ryzyka = Prawdopodobieństwo wystąpienia × Wpływ analiza ryzyka (ang. risk analysis) – proces oceny zidentyfikowanych ryzyk mający
na
celu
oszacowanie
ich
wpływu
i
prawdopodobieństwa
urzeczywistnienia się Niektóre metody zarządzania ryzykiem (np. PRisMa) nie uwzględniają poziomu ryzyka, traktując od początku do końca prawdopodobieństwo oraz wpływ jako oddzielne czynniki. Dzięki temu nie tracimy informacji o wielkości każdego z tych składników (np. dla ryzyka o bardzo dużym wpływie, ale niewielkim prawdopodobieństwie, jego poziom będzie niski bądź średni, co wcale nie musi odzwierciedlać stopnia ważności tego ryzyka, właśnie ze względu na wysoki wpływ). W podrozdziale 19.9 omówimy dokładniej istniejące i wykorzystywane w praktyce techniki analizy ryzyka.
19.6.1. Klasyfikacja ryzyk W analizie ryzyka, poza szacowaniem poziomu ryzyk, powinno się również dokonywać ich klasyfikacji. Podstawą klasyfikacji może być systematyka przyjęta i używana w organizacji, ale można również wykorzystać dostępne normy i standardy, takie jak rodzina norm ISO/IEC/IEEE 25000 (tzw. SQuaRE) [174]. Klasyfikacja może być oparta na charakterystykach jakościowych oprogramowania (np. funkcjonalność, użyteczność, niezawodność, przenaszalność, efektywność). Klasyfikacja ryzyk jest bardzo pomocna podczas procesu łagodzenia ryzyka, ponieważ typ ryzyka zwykle jest silnie skorelowany z określonym typem testu. Jeśli stosujemy klasyfikację opartą na charakterystykach jakościowych, to np. ryzyko należące do typu „wydajność”
najprawdopodobniej będzie redukowane przez wykonanie odpowiedniego testu wydajnościowego. Statystyczna informacja o liczbie oraz poziomie ryzyk określonego typu pozwala również lepiej zorganizować i zaplanować proces testowy. Jeśli np. okaże się, że najwięcej ryzyk o najwyższym wpływie jest związanych z użytecznością, prawdopodobnie proces testowy będzie się skupiał na tej charakterystyce jakościowej. Być może trzeba będzie zaplanować zorganizowanie laboratorium użyteczności, stworzyć odpowiednie kwestionariusze dla użytkowników itd.
19.6.2. Czynniki wpływające na prawdopodobieństwo i wpływ ryzyka Poziom ryzyka zależy od prawdopodobieństwa jego wystąpienia oraz potencjalnego wpływu. Czynniki wpływające na wielkość prawdopodobieństwa mają odmienną naturę od czynników określających wpływ. Prawdopodobieństwo określa szansę na to, że potencjalny problem ujawni się w testowanym systemie. Sam fakt pojawienia się problemu zależy od takich rzeczy, jak: użyty język programowania, stopień skomplikowania kodu, stosowanie określonych praktyk programistycznych, doświadczenie programistów, dostępność zasobów itp. Innymi słowy, prawdopodobieństwo wynika z ryzyka technicznego. Wpływ z kolei pochodzi z ryzyka biznesowego, określa on bowiem dotkliwość (lub: ważność, ang. severity), z jaką wystąpienie ryzyka działa na produkt oraz jego interesariuszy. dotkliwość, ważność (ang. severity) – stopień wpływu defektu na rozwój lub działanie modułu lub systemu [7] Na rysunku 19.5 przedstawiono przykładowe czynniki wpływające zarówno na ryzyko techniczne, jak i biznesowe.
Rysunek 19.5. Czynniki wpływające na prawdopodobieństwo i wpływ ryzyka
19.6.3. Ilościowa i jakościowa ocena ryzyka produktowego Poziom ryzyka produktowego może być ustalony na dwa sposoby: ilościowo lub jakościowo. Należy jednak pamiętać, że cały czas mówimy o postrzeganym, nie rzeczywistym poziomie ryzyka. Wszystkie nasze oceny są oparte na naszych przekonaniach co do prawdopodobieństwa czy wpływu ryzyka. Nie znamy jego rzeczywistego wpływu1. Podejście ilościowe pozwala wyrazić ryzyko jako stratę wyrażoną w wartości pieniężnej (lub też np. jako opóźnienie wyrażone w jednostce czasu, np. w dniach, tygodniach, miesiącach). Używanie liczb ma tę zaletę, że jest dokładne, ale niestety poziomu ryzyka nie da się wyrazić w dokładnych wartościach liczbowych. Dlatego bardzo często wykorzystuje się podejście jakościowe. Operuje ono skalą porządkową, zwykle złożoną z kilku stopni. Najczęściej spotykane skale porządkowe to: skala trójstopniowa, np.: prawdopodobieństwo duże, średnie, małe;
skala Likerta (pięciostopniowa), np.: ryzyko znikome, małe, przeciętne, duże, olbrzymie; skala dziesięciostopniowa, np. od 0 do 9 lub od 1 do 10. Używanie skal porządkowych o dużej liczbie możliwych wartości skutkuje tym, że osoby korzystające z tej skali rzadziej wykorzystują wartości skrajne. Używając skali dziesięciostopniowej, należy pamiętać, że liczby na niej występujące można ze sobą porównywać, tzn. można powiedzieć, że 4 jest mniejsze od 5, ale nie można powiedzieć, że między 0 a 1 jest taka sama różnica jak między 4 a 5, ponieważ jest to skala porządkowa, a nie interwałowa. Najczęściej stosowaną skalą porządkową jest pięciostopniowa skala Likerta. Z jednej strony ma ona odpowiednio dużo elementów, z drugiej jest ich na tyle mało, że użytkownicy nie mają oporów przed stosowaniem wartości skrajnych. ocena ryzyka (ang. risk assessment) – proces oceny ryzyka projektowego lub produktowego w celu określenia jego poziomu, zwykle przez wyznaczenie wartości prawdopodobieństwa i wpływu, a następnie połączenie tych wskaźników w pojedynczy wskaźnik priorytetu ryzyka Jeśli stosujemy podejście jakościowe do szacowania ryzyka, to musimy zdefiniować, w jaki sposób przeprowadzać mnożenie prawdopodobieństwa i wpływu. Zwykle definiuje się w tym celu specjalną tabelę, zwaną macierzą ryzyka produktowego, określającą wprost wyniki mnożeń elementów takich jak „duży”, „przeciętny” czy „znikomy”. Przykłady takich macierzy wyznaczających poziom ryzyka na podstawie jakościowo określonego prawdopodobieństwa i wpływu są pokazane na rysunku 19.6. Oczywiście każda organizacja i każdy kierownik projektu może zdefiniować własną macierz opartą na własnych skalach.
Rysunek 19.6. Przykłady macierzy ryzyka produktowego Macierz w lewej górnej części rysunku pokazuje przypadek, w którym zarówno prawdopodobieństwo, jak i wpływ oraz poziom ryzyka są podane na tej samej skali. Zauważmy, że wartości są tu ułożone symetryczne względem przekątnej, co sugeruje, że zarówno prawdopodobieństwo, jak i wpływ są traktowane w podobny sposób. Macierz w lewej dolnej części (wykorzystująca klasyczną skalę Likerta) nie ma już tej symetrii: średnie prawdopodobieństwo i olbrzymi wpływ dają olbrzymi poziom ryzyka, natomiast olbrzymie prawdopodobieństwo i średni wpływ dają tylko duży poziom ryzyka. Olbrzymi wpływ ma tutaj większą wagę niż olbrzymie prawdopodobieństwo. Macierz w prawej górnej części to przykład na to, że skala wyniku mnożenia prawdopodobieństwa przez wpływ nie musi być identyczna ze skalą tych dwóch czynników. Jak widać, skala poziomu ryzyka w stosunku do skal prawdopodobieństwa i wpływu używa dodatkowych dwóch elementów: „bardzo mały” i „bardzo duży”. Macierz w prawym dolnym rogu z kolei pokazuje, że skale obu czynników nie muszą być takie same; co więcej, nie muszą mieć takiej samej liczby elementów. W tym przykładzie wpływ odnoszony jest na trzystopniowej skali, a prawdopodobieństwo – na dwustopniowej. Ostateczna postać macierzy ryzyka produktowego zależy od konkretnego produktu, potrzeb i decyzji osób odpowiedzialnych za zarządzanie ryzykiem.
19.6.4. Osiąganie konsensusu w procesie decyzyjnym W inżynierii oprogramowania dane, na podstawie których analizuje się ryzyko zwykle nie są ścisłe i statystycznie poprawne. Dlatego ocena ryzyka zwykle jest
subiektywna. Każdy z interesariuszy może postrzegać różne ryzyka oraz ich poziomy w odmienny sposób. Jest więc konieczne ustalenie sposobu na osiągnięcie konsensusu w przypadku, gdy rozpiętość ocen poszczególnych osób jest bardzo duża. Istnieje wiele metod osiągania konsensusu w takich sytuacjach. Oto kilka z nich: Uśrednianie wyników – metoda najprostsza, zwłaszcza w przypadku, gdy mamy do czynienia z danymi ilościowymi. Jeśli jednak rozpiętość poszczególnych ocen jest bardzo duża, to ta wysoka wariancja oznacza wysoką niepewność i nie zawsze zwykłe wyciąganie średniej może być najlepszym pomysłem. Czasami zamiast średniej stosuje się medianę, która jest odporna na wartości ekstremalne. Metoda „wide band Delphi” – pokonuje ona trudności metody uśredniania; w przypadku dużych rozbieżności oceny skrajne poddane są dyskusji, po czym proces oceny jest powtarzany do momentu uzyskania pełnego konsensusu lub akceptowalnego poziomu rozbieżności (w takim przypadku wynik się uśrednia lub oblicza medianę). Oczekuje się, że w wyniku dyskusji na temat ocen skrajnych eksperci biorący udział w panelu zmodyfikują swoje oceny tak, że w rezultacie ich wyniki będą ze sobą bardziej zbieżne w kolejnych rundach. Więcej informacji na temat tej i podobnych metod pracy grupowej podanych jest w rozdziale 29. Metoda odcinania wartości skrajnych – jeśli w procesie oceny bierze udział większa liczba osób (co najmniej 3, ale najlepiej 5 lub więcej), po dokonaniu indywidualnych ocen odrzuca się wartości skrajne, po czym oblicza się średnią; ta metoda w stosunku do metody uśredniania jest mniej podatna na wartości odstające, np. dla ocen 1, 5, 5, 5 wynikiem będzie 5 (średnia z dwóch środkowych wartości, po usunięciu jedynki i jednej piątki), natomiast w przypadku metody uśredniania wynikiem będzie (1+5+5+5)/4 = 4.
19.6.5. Priorytetyzacja ryzyk Gdy analiza ryzyk zostanie ukończona, możemy dokonać priorytetyzacji według ich poziomu. Pozwoli to uszeregować wszystkie ryzyka od najważniejszych do tych, którymi można zająć się na samym końcu. Poziom, prawdopodobieństwo,
wpływ oraz klasyfikacja umożliwiają również spojrzenie na ryzyka z różnych perspektyw. Oto kilka przykładów. Załóżmy, że w procesie identyfikacji i analizy otrzymaliśmy listę ryzyk przedstawioną w tabeli 19.3. Tabela 19.3. Lista ryzyk z określonym poziomem i kategorią
Ryzyko Prawdopodobieństwo
W pływ Poziom Kategoria
R1
0,2
400
80
funkcjonalnoś ć
R2
0,05
20 000
1000
bezpieczeńs two
R3
0,3
600
180
bezpieczeńs two
R4
0,1
1500
150
wydajnoś ć
R5
0,3
3000
900
niezawodnoś ć
R6
0,3
2000
600
funkcjonalnoś ć
R7
0,25
2500
625
bezpieczeńs two
R8
0,5
100
50
funkcjonalnoś ć
R9
0,6
400
240
niezawodnoś ć
R10
0,3
450
135
użytecznoś ć
R11
0,15
240
36
niezawodnoś ć
Jakie informacje możemy otrzymać, analizując szczegółowo tę tabelę? Po pierwsze, możemy dokonać priorytetyzacji ryzyk, tak jak to pokazano w tabeli 19.4 w pierwszych trzech kolumnach. Jak widać, największy priorytet mają ryzyka R2 i R5, natomiast najmniej istotnymi są ryzyka R1, R8 i R11. Po drugie, możemy określić całkowite ryzyko przez obliczenie średniej ważonej wpływu, gdzie wagami są prawdopodobieństwa (jest to po prostu suma poziomów ryzyka): Rtotal = 0.2 ⋅ 400 + 0.05 ⋅ 20000 + … + 0.3 ⋅ 450 + 0.15 ⋅ 240 = 3996.
Oczywiście zabieg ten możemy wykonać tylko wtedy, gdy stosujemy podejście ilościowe do szacowania ryzyka. Porównując całkowitą wartość ryzyka z poziomami poszczególnych ryzyk, możemy określić ilościowo ich wpływ na powodzenie projektu. Jest to znacznie dokładniejsza informacja niż sama priorytetyzacja. Ta ostatnia ustala jedynie kolejność, natomiast ilościowe określenie wpływu pozwala stosować miary ilorazowe, tzn. pozwala nam np. powiedzieć, że jedno ryzyko ma dwukrotnie większy wpływ na powodzenie projektu niż inne. Procentowy udział w całkowitym ryzyku jest przedstawiony w ostatniej kolumnie tabeli 19.4. Jak widać poziom ryzyk R2 i R5 stanowi aż 48% całkowitego ryzyka – są to więc ryzyka kluczowe, na które trzeba będzie zwrócić szczególną uwagę w procesie łagodzenia ryzyk. Tabela 19.4. Priorytetyzacja ryzyk
Ryzyko
Priorytet
Poziom
Udział w całkowitym ryzyku
R2
1
1000
25%
R5
2
900
23%
R7
3
625
16%
R6
4
600
15%
R9
5
240
6%
R3
6
180
4%
R4
7
150
4%
R10
8
135
3%
R1
9
80
2%
R8
10
50
1%
R11
11
36
1%
Po trzecie, możemy określić poziom ryzyk w podziale na kategorie, tak jak na rysunku 19.7.
Największy, bo aż 75-procentowy udział w ryzyku mają zagrożenia związane z bezpieczeństwem i niezawodnością
Oznacza to, że proces testowy powinien skupić się głównie na testach bezpieczeństwa i niezawodności.
Rysunek 19.7. Poziom ryzyka według jego klasyfikacji
19.7. Łagodzenie ryzyka Po określeniu ryzyk, ich poziomu, kategoryzacji oraz klasyfikacji można przystąpić do kolejnego etapu – łagodzenia ryzyka. Polega on na przygotowaniu planów mających na celu zapobieżenie w możliwie jak największym stopniu wystąpieniu ryzyk oraz opracowaniu planów „awaryjnych” na wypadek, gdyby ryzyka te jednak się urzeczywistniły. łagodzenie ryzyka, kontrola ryzyka (ang. risk mitigation, risk control) – proces, w którym podejmuje się decyzje i implementuje metryki w celu redukcji ryzyka lub utrzymania go na określonym poziomie
19.7.1. Sposoby łagodzenia ryzyka Istnieją cztery główne sposoby łagodzenia ryzyka:
łagodzenie ryzyka (ang. risk mitigation) przez przedsięwzięcie czynności prewencyjnych, zapobiegających pojawieniu się ryzyka lub zmniejszających ich ewentualną dotkliwość; plany awaryjne (ang. contingency plans) mające na celu zredukować siłę oddziaływania ryzyka, które rzeczywiście nastąpi; transfer ryzyka (ang. risk transfer), czyli przeniesienie ryzyka na stronę trzecią (np. ubezpieczyciela), który będzie ponosił skutki ewentualnego wystąpienia ryzyka; zignorowanie i zaakceptowanie ryzyka, które polega po prostu na tym, że nie podejmuje się żadnych akcji do momentu wystąpienia tego ryzyka. Wybór metody
zależy
od rodzaju ryzyka, stopnia
jego
krytyczności,
istniejących zasobów, ale także od dodatkowych, potencjalnych ryzyk związanych z każdym sposobem łagodzenia ryzyka. Przykładem takiego dodatkowego ryzyka może być utrata pieniędzy bądź czasu na przeprowadzenie czynności, których jednak można było uniknąć. Pierwsza z wymienionych opcji – opcja łagodzenia – wydaje się być najbardziej optymalna, ponieważ pozwala zredukować szansę samego wystąpienia ryzyka. Niestety nigdy nie będziemy w stanie zminimalizować wszystkich zagrożeń tylko przez łagodzenie. Ryzyka mogą pojawić się w nieoczekiwanych momentach, zwłaszcza te, których istnienia nie przewidzieliśmy w fazie identyfikacji. Dlatego zwykle korzysta się z wszystkich czterech metod. Całe zarządzanie ryzykiem polega na znalezieniu odpowiedniego balansu, „złotego środka” między założonym poziomem jakości końcowego produktu a kosztem wysiłku maksymalizującego prawdopodobieństwo osiągnięcia tego celu. W kontekście testowania relację tę można ująć liczbowo. Opiszemy to dokładnie w podrozdziale 24.2. Należy pamiętać, że czynności łagodzące ryzykiem trzeba wykonać w odpowiednim czasie (zwanym przedziałem ufności2), aby skutecznie minimalizować prawdopodobieństwo ich wystąpienia. Jeśli np. ryzyko może wystąpić w fazie projektowania, to czynności łagodzące należy zacząć już w fazie wymagań albo jeszcze wcześniej. przedział ufności (ang. confidence interval) – w zarządzaniu ryzykiem projektowym – czas, podczas którego musi zostać podjęte działanie, aby
skutecznie zredukować wpływ ryzyka
19.7.2. Łagodzenie ryzyka przez testowanie Łagodzenie ryzyka można osiągnąć w szczególności przez testowanie. Każda wykryta awaria pozwala na usunięcie defektu, który był potencjalnym źródłem ryzyka. Bezbłędne wykonanie dobrze zaprojektowanych testów daje nam większą pewność, że żadne ze zidentyfikowanych uprzednio ryzyk nie wystąpi lub że prawdopodobieństwo jego wystąpienia jest mniejsze niż sądziliśmy przed wykonaniem testów. Poziom ryzyka wyznacza zakres i dokładność testowania. Im wyższy poziom ryzyka, tym dokładniej należy przetestować obszar związany z tym ryzykiem. Dokładność może np. polegać na zastosowaniu silniejszych lub bardziej formalnych metod projektowania testów. Dobrym przykładem ilustrującym to zagadnienie jest standard przemysłowy DO-178C [8], dotyczący systemów dla awioniki. Standard ten określa poziom krytyczności systemu na podstawie wielkości skutków, jakie może nieść ze sobą awaria tego systemu. Norma DO-178C wyróżnia pięć poziomów krytyczności (patrz tab. 19.5) i dla każdego z nich narzuca wymaganie spełnienia odpowiednich kryteriów pokrycia testami białoskrzynkowymi. Kryteria te są opisane w rozdziale 9. Innym przykładem standardu uzależniającego rodzaj testowania od poziomu ryzyka może być norma IEC 61508 „Functional safety of electrical/electronic/programmable electronic safety-related systems” [175]. Norma ta wyróżnia cztery tzw. poziomy nienaruszalności bezpieczeństwa (ang. Safety Integrity Levels, SIL – w odniesieniu do oprogramowania używa się także pojęcia Software Integrity Level). Każdy z poziomów wyznacza kryteria niezawodności systemu wyrażone w postaci współczynnika tolerowanego zagrożenia (ang. Tolerable Hazard Rate, THR), czyli dopuszczalnej częstości awarii na jednostkę działania systemu. Tabela 19.5. Poziomy krytyczności wg normy DO-178C
Poziom Nazwa
Opis
W ymagane osiągnięcie pokrycia
Awaria s ys temu może s powodować utratę s tatku powietrzneg o
Pokrycie MC/DC Pokrycie decyzji Pokrycie ins trukcji
A
Katas trofalny
B
Awaria s ys temu znacznie redukuje zdolnoś ć s tatku powietrzneg o lub załog i do reakcji na neg atywne zjawis ka, powodując poważną utratę Niebezpieczny marg ines u bezpieczeńs twa lub funkcjonalnoś ci, fizyczne niebezpieczeńs two lub poważne obrażenia niewielkiej liczby pas ażerów
Pokrycie decyzji Pokrycie ins trukcji
C
Awaria redukuje zdolnoś ć załog i do reakcji na neg atywne zjawis ka, s kutkując utratą marg ines u bezpieczeńs twa, obrażeniami pas ażerów lub koniecznoś cią wykonywania dodatkowych, obciążających załog ę czynnoś ci powodując u niej s tres
Pokrycie ins trukcji
Duży
Awaria nie redukuje znacząco bezpieczeńs twa s tatku powietrzneg o, pog ars za nieznacznie marg ines bezpieczeńs twa lub funkcjonalnoś ci s tatku,
D
Mały
powoduje nieznaczny wzros t Brak dodatkowych czynnoś ci obciążających załog ę (np. zmiana planu lotu) lub pewien dys komfort ps ychiczny u załog i lub pas ażerów
E
Awaria nie ma wpływu na poziom bezpieczeńs twa i nie Znikomy/Brak powoduje koniecznoś ci wykonywania dodatkowych czynnoś ci przez załog ę
Brak
poziom nienaruszalności bezpieczeństwa (poziom integralności oprogramowania) (ang. Safety Integrity Level, Software Integrity Level, SIL) – stopień, w jakim oprogramowanie spełnia lub musi spełniać zbiór wybranych przez interesariuszy cech oprogramowania lub charakterystyk systemu (np. złożoność oprogramowania, ocenione ryzyko, poziom zabezpieczeń, poziom bezpieczeństwa, pożądana wydajność, niezawodność lub koszt), które to cechy obrazują, jak ważne jest dane oprogramowanie dla jego interesariuszy W odniesieniu do oprogramowania norma IEC 61508 rekomenduje stosowanie odpowiednich technik testowania w zależności od poziomu SIL. Opisano to w tabeli 19.6, w której znak + oznacza, że dana technika jest rekomendowana, a ++ oznacza silną rekomendację. Pozostałe przykłady technik łagodzenia ryzyka przez testerów to: priorytetyzacja testów według poziomu ryzyka; wybór odpowiednich technik projektowania testów; przeprowadzanie przeglądów i inspekcji; przeprowadzanie przeglądów projektów testów; stosowanie wczesnego prototypowania; uzyskanie określonego poziomu niezależności;
wykorzystywanie umiejętności najbardziej doświadczonych osób; przeprowadzanie szkoleń z testowania czy tworzenia testowalnego kodu o wysokiej jakości; zdefiniowanie zakresu i intensywności retestów; zdefiniowanie zakresu i intensywności testów regresji; automatyzowanie projektowania i wykonywania testów. Tabela 19.6. Poziomy nienaruszalności bezpieczeństwa wg IEC 61508
Poziom SIL
SIL 1
SIL 2
SIL 3
SIL 4
THR dla pracy ciąg łej
[10–6, 10– 5)
[10–7, 10– 6)
[10–8, 10– 7)
[10–9, 10– 8)
THR dla pracy na żądanie
[10–2, 10– 1)
[10–3, 10– 2)
[10–4, 10– 3)
[10–5, 10– 4)
Tes towanie przeciążeń
+
+
++
++
Tes towanie wydajnoś ci
++
++
++
++
+
+
T echniki testowania
Grafy przyczynowos kutkowe AWB/klas y równoważnoś ci
+
++
++
++
Zg adywanie błędów
+
+
+
+
Tes towanie białos krzynkowe
+
+
++
++
+
+
+
Pos iew us terek
Niektóre z wymienionych czynności o charakterze prewencyjnym rozpoczynają się bardzo wcześnie w cyklu życia, nawet przed fazą testów dynamicznych. W testowaniu opartym na ryzyku czynności związane
z łagodzeniem ryzyka powinny następować w ciągu całego czasu trwania projektu, a nie tylko na samym początku lub samym końcu.
19.7.3. Estymacja kosztów łagodzenia ryzyka Kadra zarządzająca, po zaprezentowaniu jej sposobów łagodzenia ryzyka, ma zwykle nastawienie entuzjastyczne, ponieważ wszystko wygląda wspaniale, a powodzenie całego projektu spoczywa na kierowniku testów traktowanym jako ekspert w tej dziedzinie. Problemy zaczynają się, gdy mowa o kosztach łagodzenia. Zwykle są one za duże. Wtedy należy być przygotowanym do odpowiedzi na trudne pytania, na przykład: czy możemy to zastąpić testowaniem eksploracyjnym? ile zaoszczędzimy, łagodząc w taki a taki sposób kryteria wyjścia z fazy testów? co się stanie, jeśli nie przeprowadzimy testów wydajnościowych? Kierownik testów powinien być przygotowany na takie pytania. Najlepiej zrobić to, szacując koszty oraz potencjalne zyski, np. w terminach redukcji ryzyka czy wzrostu jakości. Mając zidentyfikowane ryzyka wraz z oszacowaniem ich poziomu, a także mapowanie testów na te ryzyka, można stworzyć symulator w arkuszu kalkulacyjnym, który w szybki sposób będzie potrafił wykonać tzw. analizę „what if”. Dzięki wykorzystaniu takiego symulatora kierownik testów będzie mógł szybko odpowiedzieć na pytania takie, jak te wymienione wcześniej. Kadra zarządzająca zwykle bardzo lubi tego typu narzędzia, a osobę wykorzystującą je postrzega jako profesjonalnego eksperta w swojej dziedzinie. Przykładowa symulacja jest pokazana na rysunku 19.8. W lewym górnym rogu są zapisane dane „konfiguracyjne”, opisujące efektywność i pracochłonność dla każdej z możliwych do zastosowania metod. W głównej tabeli jest przedstawiona symulacja kosztu oraz zysku pochodzącego z łagodzenia ryzyka. Tabela jest skonstruowana tak, aby obliczyć zysk z stosowania analizy ryzyka wobec każdej funkcjonalności. Na przykład poziom ryzyka związany z funkcjonalnością F1 wynosi 60 000 USD. Po zastosowaniu testowania czarnoskrzynkowego usuniemy 70% defektów z F1 i szacujemy, że w związku z tym poziom ryzyka obniży się również o 70%, czyli do poziomu 30% ⋅ 60 000 USD = 18 000 USD.
Rysunek 19.8. Przykład symulatora dla łagodzenia ryzyka Złagodzone ryzyko wynosi 70% ⋅ 60 000 USD = 42 000 USD. F1 liczy 4.5 KLOC, a ponieważ pracochłonność dla tej metody to średnio 3 osobodni/KLOC, zatem testy czarnoskrzynkowe dla F1 zajmą 4,5 KLOC ⋅ 3 osobodni/KLOC = 13,5 osobodni. Koszt jednodniowej pracy jednej osoby wynosi 300 USD, zatem koszt testów czarnoskrzynkowych dla F1 szacujemy na 300 USD/osobodzień ⋅ 13,5 osobodni = 4050 USD. Szacunkowy zysk, jaki otrzymujemy przy zastosowaniu analizy ryzyka do funkcjonalności F1 to zysk z minimalizacji ryzyka pomniejszony o koszt wykonanej analizy (w tym przypadku – testowania czarnoskrzynkowego), czyli 42 000 USD – 4050 USD = 37 950 USD. Dla funkcjonalności, wobec których zastosowano więcej niż jedną metodę zakładamy, że używa się ich sekwencyjnie. Na przykład dla F5, stosując inspekcję (efektywność 90%), z poziomu ryzyka 40 000 zostaje 10% ⋅ 40 000 = 4000. Ten poziom ryzyka stosuje się dla kolejnej metody, czyli testowania czarnoskrzynkowego (efektywność 70%), w wyniku czego z poziomu ryzyka zostaje 30% ⋅ 4000 = 1200 itd. Sumując zyski z minimalizacji ryzyka we wszystkich obszarach projektu i odejmując od tej wartości sumaryczny koszt wykonania analizy ryzyka, otrzymujemy ostatecznie kwotę 359 605 USD – 250 950 USD = 108 655 USD. Możemy
zinterpretować tę wartość, jako „wirtualny” zysk (kwota, jakiej nie stracimy w wyniku ponoszenia kosztów ryzyka) z analizy ryzyka. W takim symulatorze bardzo łatwo zmieniać dane wejściowe i obserwować jak np. zmieni się całkowity zysk z zastosowania konkretnego podejścia. Oczywiście czynimy tu wiele założeń, np.: ryzyko może być wykryte przez metody weryfikacji i walidacji; koszty powodowane przez defekty rozłożone są równomiernie itd. Nawet, jeśli założenia te nie do końca są spełnione, a dane nie są idealnie dokładne, tego typu narzędzia są bardzo wartościowe. Umożliwiają bowiem, przynajmniej w przybliżeniu, udzielenie odpowiedzi na wspomniane wcześniej pytania kadry zarządzającej, a nawet na przeprowadzenie analizy wrażliwości.
19.8. Monitorowanie ryzyka Gdy zaplanowaliśmy czynności prewencyjne oraz metody radzenia sobie z ryzykiem, które rzeczywiście nastąpi, przechodzimy do fazy monitorowania. Polega ono na ciągłej obserwacji aktualnego stanu systemu. Dzięki monitorowaniu możemy na bieżąco obserwować aktualny poziom ryzyka w produkcie, kontrolować, czy proces redukcji ryzyka przebiega zgodnie z planem, identyfikować nowo powstałe problemy oraz szacować poziom ryzyka rezydualnego. Wreszcie, przez pomiary możemy określić, czy uzyskaliśmy zadowalający poziom ryzyka. Proces monitorowania jest kluczowy dla kierownictwa, ponieważ produkty tego procesu, w postaci wszelkiego rodzaju raportów, są podstawą do decydowania o tym, czy produkt może przejść do kolejnej fazy lub czy może zostać przekazany klientowi. Podczas monitorowania jest zbieranych wiele różnego rodzaju danych. Po zakończeniu projektu dane te powinny zostać zarchiwizowane tak, aby można je było wykorzystać – jako dane historyczne – w przyszłych projektach prowadzonych w organizacji. Wszystkie zebrane dane stanowią nasze doświadczenie, opisują jak rzeczywiście przebiegał projekt w zadanych okolicznościach. Z tego powodu są niezwykle cenne szczególnie przy budowie modeli predykcyjnych. Na podstawie takich modeli możemy np. oszacować, jak szybko, w wyniku testowania, będzie następować redukcja ryzyka w nowym
projekcie, co pozwoli oszacować czas i koszt testowania. Więcej informacji o tego typu modelach Czytelnik znajdzie w rozdziałach 44–49.
19.8.1. Macierz identyfikowalności ryzyk Monitorowanie dotyczy nie tylko kwestii ilościowych, lecz także jakościowych. Na przykład powinniśmy monitorować wykonywanie czynności łagodzących ryzyko, np. fakt wykonania określonych testów czy przeglądów. Powinniśmy również kontrolować, czy czynności te skutkują założonymi efektami (np. poprawą jakości produktu wyrażoną jako zmniejszona gęstość defektów czy wzrost czasu między awariami). W kontekście testowania pomiar redukcji ryzyka jest możliwy dzięki istnieniu obustronnego powiązania (ang. traceability) między testami a zidentyfikowanymi ryzykami produktowymi. Przykład takiej macierzy identyfikowalności jest pokazany na rysunku 19.9. Znak X na przecięciu przypadku testowego i ryzyka oznacza, że dany test pokrywa to ryzyko. Na rysunku tym, oprócz przedstawienia powiązania między testami a ryzykami, jest również przedstawiony aktualny stan pokrycia ryzyk. Znak X na białym polu oznacza, że dany test nie został jeszcze wykonany. Pole szare oznacza, że test był wykonany, ale nie został zdany, czyli są konieczne naprawa oraz retest. Pole ciemne oznacza, że dany test został wykonany i zdany, a co za tym idzie – odpowiednie ryzyko zostało pokryte. Niektóre ryzyka mogą być związane z więcej niż jednym testem. W takim przypadku możemy mówić o częściowym pokryciu ryzyk. Na przykład, zakładając, że ryzyko R1 jest pokrywane trzema przypadkami testowymi: PT1, PT2 i PT3, w sytuacji z rysunku 19.9 ryzyko to jest pokryte w 66%, ponieważ testy PT1 i PT2 zostały wykonane i są zdane, natomiast test PT3 nie został jeszcze wykonany. Analizując taką macierz identyfikowalności z naniesionymi informacjami o bieżącym stopniu wykonania testów, możemy określić aktualny stan pokrycia ryzyk i raportować kierownictwu o poziomie ryzyka rezydualnego. Jeśli stosujemy
Rysunek 19.9. Macierz identyfikowalności ryzyka-testy podejście ilościowe do szacowania ryzyka, możemy również wyrazić aktualny postęp w wartościach liczbowych (np.: „w bieżącej wersji produktu ryzyko rezydualne wynosi 15%”). Na podstawie wspomnianych informacji kierownik testów może zdecydować o rozszerzeniu zakresu testów, jego redukcji, zawieszeniu, zakończeniu testowania lub o innych czynnościach związanych z procesem testowym. Zaletą takiego monitorowania w podejściu opartym na ryzyku jest to, że możemy w miarę dokładnie określić moment, w którym produkt można przekazać klientowi, mając formalne potwierdzenie o akceptowalnym poziomie ryzyka rezydualnego. kryteria zawieszenia (ang. suspension criteria) – kryteria używane do (tymczasowego) zatrzymania wszystkich lub części aktywności testowych na elementach testowych [5] kryteria wznowienia (ang. resumption criteria) – kryteria używane do wznowienia części lub całości aktywności testowych, które zostały uprzednio wstrzymane
19.8.2. Aktualizacja ryzyk oraz ich parametrów Należy pamiętać o ciągłej aktualizacji zarówno samych ryzyk, jak i ich poziomu. Czynności identyfikacji i analizy ryzyka nie powinny być wykonywane wyłącznie
na początku projektu, ale w sposób okresowy, np. w każdej iteracji, przy przejściu z jednej fazy do drugiej, po osiągnięciu kamienia milowego, po spotkaniu retrospektywnym dla fazy itp. Testy powinny być wykonywane zgodnie z priorytetyzacją ryzyk, tzn. testy pokrywające ryzyka o wyższym poziomie powinny być wykonywane wcześniej i podlegać bardziej szczegółowej ocenie niż testy pokrywające ryzyka marginalne. W związku z postępem projektu, lista ryzyk powinna być okresowo przeglądana i kierownik testów powinien dla każdego ryzyka produktowego przedyskutować następujące kwestie [171]: czy dane ryzyko zostało prawidłowo oszacowane? czy czynności łagodzenia ryzyka (np. wykonanie testów) zostały przeprowadzone? jakie są efekty czynności łagodzących ryzyko (np. wyniki testów)? czy w stosunku do danego ryzyka należy przeprowadzić dodatkowe czynności, np. więcej testów? czy można dane ryzyko usunąć z listy ryzyk? Należy również zdecydować, czy do listy ryzyk powinno się dodać nowe ryzyka. Dodanie nowych ryzyk może skutkować zmianami w priorytetyzacji testów. Wszystkie tego typu dyskusje powinny odbywać się na formalnych spotkaniach z udziałem kierownika testów oraz innych interesariuszy, bezpośrednio zaangażowanych w kwestie związane z ryzykiem produktu (np.: testerzy, programiści, klient). Przykładowe sytuacje, które mogą skutkować koniecznością dodania do listy nowych ryzyk to [171]: nowe lub zmienione wymagania; odkrycie nowych, krytycznych obszarów biznesowych pominiętych we wcześniejszych analizach; odkrycie, w wyniku testowania, obszarów produktu o wysokiej gęstości defektów lub odkrycie nieoczekiwanych defektów; wczesne wyniki testów (np. w wyniku przeglądu dokumentów lub projektu architektury) są bardziej optymistyczne lub bardziej pesymistyczne niż zakładano; ryzyko testów regresji związanych z naprawianymi defektami; zmiana w podejściu do wytwarzania oprogramowania;
doświadczenie związane z jakością nabyte we wcześniejszych fazach projektu.
19.8.3. Raportowanie Testowanie oparte na ryzyku daje możliwość dokładnego raportowania o stanie produktu pod kątem występujących w nim ryzyk. Kierownictwo często nie rozumie koncepcji ryzyka w testowaniu i nierzadko zdarza się, że kierownik projektu bardziej jest zainteresowany liczbą wykonanych testów niż poziomem ryzyka rezydualnego. Podejście testowania opartego na ryzyku oznacza również raportowanie oparte na ryzyku. Dlatego raporty powinny kłaść nacisk na kwestie związane z ryzykiem, a nie na liczbę zaprojektowanych, wykonanych czy zdanych przypadków testowych. Takie podejście ma również pozytywny wpływ na sam proces testowania. Ludzie dostosowują się do miar, którymi się ich ocenia. Gdyby kierownictwo kładło nacisk np. na liczbę zaprojektowanych i wykonanych przypadków testowych, natychmiast testerzy zaczęliby produkować wiele niskiej jakości testów, które nie weryfikowałyby zidentyfikowanych ryzyk produktowych. Doprowadziłoby to do niebezpiecznej sytuacji, gdyż kierownictwo byłoby zadowolone z osiągniętego celu, ale niekoniecznie oznaczałoby to redukcję ryzyka rezydualnego! Wszyscy mieliby złudne poczucie bezpieczeństwa oraz wysokiej jakości produktu, a prawdziwe problemy zaczęłyby się dopiero w momencie przekazania produktu klientowi – czyli o wiele za późno. Raportowanie wysokopoziomowe może wykorzystywać metody takie, jak macierz identyfikowalności z rysunku 19.9, ale może też być jeszcze bardziej wysokopoziomowe. Należy pamiętać, że poziom abstrakcji informacji zawartych w raporcie musi być odpowiedni do adresata raportu. Kierownictwo wyższego szczebla niekoniecznie musi być zainteresowane dokładnymi informacjami o poszczególnych testach. Czasami wystarczy jeden wykres czy tabela z kilkoma wartościami liczbowymi, aby dać kierownictwu wgląd w ogólny stan projektu. Przykłady wysokopoziomowych raportów są pokazane na rysunku 19.10. Na lewym górnym wykresie przedstawiono aktualne pokrycia ryzyk w podziale na ich poziom. Na wykresie kołowym z prawej strony zaprezentowano zbiorczą informację o tych ryzykach: proporcja ryzyk pokrytych (odpowiednio: problemów i ryzyk niepokrytych) to proporcja testów zdanych (odpowiednio: testów niezdanych i niewykonanych) do wszystkich testów. Na dolnym wykresie
przebiegu pokazano zmianę pokrycia w czasie. Aby nie „zaśmiecać” wykresu, przedstawiono informację tylko o dwóch rodzajach ryzyk: krytycznych i dużych, łącząc je w jedną kategorię. Jest to dosyć prosty, ale czytelny dla kierownictwa raport. Można np. na jego podstawie szybko określić aktualny pozom ryzyka rezydualnego lub sprawdzić, czy redukcja ryzyka postępuje zgodnie z planem. Raport z rysunku 19.10 pokazuje na przykład, że jeśli chodzi o redukcję ryzyk o poziomie krytycznym i dużym, początkowo proces szedł bardzo dobrze (wyprzedzaliśmy plan), ale obecnie jesteśmy opóźnieni w stosunku do planu i sytuacja ta trwa od dwóch miesięcy. Należy znaleźć przyczynę tych problemów i wprowadzić korekty do procesu tak, aby nadrobić opóźnienia. Tego typu raporty, choć nieskomplikowane i zawierające bardzo zagregowaną informację, często są wystarczające dla kierownictwa wyższego szczebla do podejmowania kluczowych czy strategicznych decyzji odnośnie do tworzonego produktu.
Rysunek 19.10. Przykład wysokopoziomowego raportu o ryzykach
19.9. Techniki analizy ryzyka W podrozdziale tym opiszemy kilka istniejących podejść do analizy ryzyka. Techniki te różnią się stopniem formalizacji, wykorzystywanymi metodami czy wymaganiami czasowymi. Z grubsza można je podzielić na dwie kategorie: metody „lekkie” (PRAM, SST, PRisMa) i „ciężkie” (hazard analysis, Cost of Exposure, FMEA, QFD, FTA). Jeśli kierownik testów decyduje się na stosowanie formalnej metody analizy ryzyka (o ile nie została ona narzucona z góry np. przez strategię testowania), to podczas wyboru konkretnej techniki należy wziąć pod uwagę kilka czynników: dostępność zasobów oraz kwalifikacje personelu – niektóre metody wymagają doświadczenia w ich stosowaniu; czas poświęcony na wdrożenie oraz stosowanie metody; koszt (np. dodatkowe szkolenia czy koszt wynikający z czasu, jaki członkowie zespołu poświęcają na wykonywanie czynności wymaganych przez metodę); dostępność wymaganych przez metodę danych. Techniki lekkie są stosowane częściej i przez to zwykle są technikami dojrzałymi, zweryfikowanymi przez praktykę. Wykorzystują wiedzę i doświadczenie wielu osób, mających różne perspektywy na jakość tworzonego systemu. Produkt wyjściowy tych metod (np. macierz ryzyka czy tabela analizy ryzyka) stanowi podstawę dla planu testów, a stąd również dla wszystkich czynności związanych z zarządzaniem i analizą ryzyka wykonywanych w późniejszych fazach projektu. Niektóre techniki (np. Systematic Software Testing) wymagają podania na wejście specyfikacji wymagań. Jeśli takiego dokumentu brak, metoda nie może zostać zastosowana. Inne techniki, takie jak PRAM czy PRisMa są oparte na podejściu mieszanym, które łączy w sobie strategie oparte na ryzyku i wymaganiach, przy czym te ostatnie nie muszą być reprezentowane formalnym dokumentem – wystarczy informacja od klienta. Zaletą technik silnie opartych na wymaganiach jest dobra kontrola nad pokryciem funkcjonalności oraz ryzyk z nią związanych, jednak istnieje wtedy niebezpieczeństwo pominięcia ryzyk niezwiązanych bezpośrednio z wymaganiami, np. dotyczących charakterystyk niefunkcjonalnych
oprogramowania. Kierownik testów musi zawsze pamiętać również o tym niefunkcjonalnym aspekcie systemu, niezależnie od tego, czy jest on ujęty w wymaganiach, czy nie. Wiele technik analizy ryzyka angażuje dużą grupę interesariuszy i wymaga, aby osiągnęli oni konsensus w kwestii oceny poziomu poszczególnych ryzyk. Zadanie sprawnego przeprowadzenia tego procesu należy do moderatora spotkań, na których ryzyka są identyfikowane i analizowane i gdzie taki konsensus powinno się osiągać. Może to być problematyczne, gdyż często osiągnięcie wspólnego stanowiska jest bardzo trudne. Moderator może się posłużyć metodami pracy grupowej, opisanymi w rozdziale 29.
19.9.1. PRAM (Pragmatic Risk Analysis and Management) Jest to metoda opisana przez Blacka w książce „Managing the Testing Process” [31]. PRAM zakłada, że efektywne testowanie oraz dobra komunikacja stanowią integralną część procesu zarządzania ryzykiem, co pomaga w osiągnięciu wysokiego poziomu jakości. Głównym narzędziem łagodzenia ryzyka jest testowanie. PRAM kładzie bardzo duży nacisk na dogłębną analizę ryzyka, w szczególności na to co testować oraz w jakiej kolejności to robić. Priorytetyzacja testów na podstawie tej analizy ma na celu wczesne wykrywanie najważniejszych defektów. W przypadku konieczności ograniczenia testów priorytetyzacja pozwala opuścić te najmniej istotne. W kwestii komunikacji PRAM wymaga, aby raportowanie odbywało się w formie zrozumiałej dla odbiorców raportów, a nie dla ich autorów (testerów). Podstawą do wszelkich działań jest szczegółowy plan testów. Kolejnym wymogiem jest to, aby systemy testowe były dobrze zaprojektowane. Ułatwia to łagodzenie ryzyka produktowego i zwiększa zaufanie, gdy test ostatecznie przejdzie, a defekty zostaną usunięte. Zespół testowy musi podążać za wyobrażeniem jakości przez klienta – proces testowy powinien uwzględniać przede wszystkim potrzeby użytkownika, tak aby końcowa jakość była jak najwyższa z punktu widzenia klienta. Innymi słowy, testerzy muszą wczuć się w rolę klienta, zrozumieć dogłębnie jego potrzeby i oczekiwania wobec systemu i na podstawie tej wiedzy projektować cały proces testowy. Stopień spełnienia tego „wyobrażenia klienta” PRAM nazywa wiernością (ang. fidelity). Systemy mogą charakteryzować się wysoką lub niską wiernością.
Rysunek 19.11. Systemy o wysokiej i niskiej wierności Koncepcja ta jest pokazana na rysunku 19.11. Przedstawiono na nim sytuację dwóch systemów, A i B. W systemie A (po lewej stronie) co prawda testy nie pokrywają całego obszaru produktu, ale za to wyobrażenia klienta o systemie pokrywają się prawie idealnie z tym, jak jakość tego systemu jest postrzegana przez testera. W rezultacie klient otrzyma system o wysokiej – ze swojego punktu widzenia (!) – jakości, gdyż jego wyobrażenie jakości było zbieżne z wyobrażeniem zespołu testowego. Odmienną sytuację mamy w przypadku projektu B (prawa strona rysunku). Sposób postrzegania jakości produktu przez testera jest zupełnie odmienny od punktu widzenia klienta. Oczekiwania obu stron są rozbieżne. W efekcie otrzymujemy produkt spełniający wszystkie wymagania jakościowe zespołu testerskiego, ale prawie żadnych wymagań klienta. Przydatność takiego systemu jest więc bliska zeru. Przykład ten pokazuje, jak ważna jest komunikacja między zespołem projektowym a klientem, zwłaszcza w aspekcie zrozumienia przez testerów potrzeb jakościowych klienta wobec tworzonego oprogramowania. W metodzie PRAM atrybuty ryzyka są definiowane klasycznie, jako prawdopodobieństwo oraz wpływ. Po etapie identyfikacji i analizy ryzyk należy się nimi w odpowiedni sposób zająć. Metoda PRAM wyróżnia tu cztery rodzaje aktywności:
alokacja wysiłku – podczas planowania, projektowania i wykonywania testów każdemu ryzyku należy przypisać pewien wysiłek związany z testowaniem, proporcjonalny do poziomu tego ryzyka; priorytetyzacja testów – ustalanie kolejności testów w zależności od poziomu ryzyka, które te testy pokrywają; optymalna redukcja suity testowej – w razie konieczności usunięcie tych testów, które wnoszą najmniej do redukcji ryzyka; raportowanie wyników testów odzwierciedlać przede wszystkim z poziomem ryzyka.
– raportowanie punkt widzenia
powinno związany
Jedną z cech metody PRAM jest możliwość korekty niedokładnych założeń wykorzystywanych we wczesnych fazach planowania, kiedy to oszacowania obarczone są dużym błędem. PRAM zaleca stosowanie strategii mieszanej w wykorzystywaniu reaktywnych strategii testowych, takich jak polowanie na błędy, ataki usterkowe czy testowanie eksploracyjne, omówionych w rozdziale 10.
19.9.2. SST (Systematic Software Testing) SST to kolejna z lekkich metodyk, w której podejście oparte na ryzyku stanowi tak naprawdę jedną z jej części. Metoda opisana jest w podręczniku [176], powstałym na podstawie materiałów kursów Systematic Software Testing oraz Test Management prowadzonych w Software Engineering Institute przez autorów książki, Craiga i Jaskiela. SST wykorzystuje podejście opisane w metodologii STEP (omówionej w p. 32.5.1) służącej do ulepszania procesu testowego i opisującej praktyczny sposób wdrożenia standardu IEEE 829 – Software Test Documentation [5] w ramach procesu testowego. SST opisuje całościowo podejście do testowania, dlatego tu skupimy się jedynie na omówieniu tych jej aspektów, które dotyczą strategii testowania opartego na ryzyku. Metoda SST jest dosyć podobna do opisanej wcześniej metody PRAM, co wynika poniekąd z faktu, że oba podejścia są podejściami lekkimi i całościowo ujmującymi proces testowy. SST kładzie nacisk na testowanie prewencyjne, czyli stoi na stanowisku, że testowanie może zwiększyć jakość oprogramowania, jeśli rozpocznie się odpowiednio wcześnie w cyklu życia oprogramowania. W szczególności SST zwraca uwagę na wczesne wykrywanie defektów
w wymaganiach oraz projektach architektonicznych oprogramowania. Tak jak w metodologii STEP, SST zakłada, że planowanie testów rozpoczyna się w fazie definicji wymagań, a projektowanie testaliów (testów, skryptów itd.) równolegle z projektowaniem i przed fazą kodowania. W metodzie SST testy są grupowane w suity testowe wraz z informacją o pokryciu, jakiego dostarczają. Pokrycie to można obliczyć dzięki istnieniu powiązań między przypadkami testowymi a wymaganiami, elementami projektowymi i kodem źródłowym. SST wyróżnia dwa rodzaje defektów: ukryty (ang. latent) – istniejący w oprogramowaniu defekt, który nie spowodował jeszcze awarii, ponieważ nie wystąpiły warunki umożliwiające wystąpienie tego defektu; zamaskowany (ang. masked) – istniejący w oprogramowaniu defekt, który nie spowodował jeszcze awarii, ponieważ inny defekt uniemożliwił wykonanie odpowiedniego fragmentu kodu. W kontekście testowania opartego na ryzyku, SST rozróżnia zarządzanie ryzykiem (kontrola i monitorowanie efektywności zastosowanych metod) od analizy ryzyka (identyfikacja, szacowanie i analiza ryzyka), czyli stosuje klasyczny model przedstawiony na rysunku 19.3, przy czym SST idzie tu jeszcze dalej i dzieli analizę ryzyka na dwie podstawowe aktywności: analizę ryzyka software’owego oraz analizę ryzyka w planowaniu i plany awaryjne. Schemat ten pokazany jest na rysunku 19.12.
Rysunek 19.12. Czynności w analizie ryzyka według metody SST Rozdzielenie tych dwóch aktywności pozwala na rozróżnienie między ryzykiem związanym z awarią dotyczącą cechy lub atrybutu systemu a ryzykiem związanym z wdrożeniem planu testów. Ryzyka planowania obejmują takie zdarzenia jak: daty dostarczenia produktów, dostępność personelu, budżet, dostępność środowiska i narzędzi, potrzeby szkoleniowe, zakres testowania, brak wymagań, założenia dotyczące ryzyk, zasoby, niska jakość oprogramowania itp. Czynniki te mogą wpływać negatywnie na harmonogram i powodować opóźnienia w projekcie. Według autorów SST istnieją cztery sensowne metody łagodzenia tych ryzyk: redukcja zakresu projektu; opóźnienie implementacji; dodanie zasobów; redukcja procesów jakościowych. Analiza ryzyka w metodzie SST składa się z 10 kroków: 1. Utworzenie zespołu do przeprowadzenia burzy mózgów (włączając w to klientów, użytkowników końcowych, programistów, testerów,
marketingowców i analityków biznesowych; zespół powinien być jak najbardziej interdyscylinarny). 2. Stworzenie listy cech i atrybutów systemu. 3. Określenie prawdopodobieństwa awarii cech i atrybutów systemu (stosując co najmniej skalę porządkową). 4. Ocena wpływu awarii cech i atrybutów na użytkownika (stosując co najmniej skalę porządkową). 5. Przypisanie wartości liczbowych prawdopodobieństwu i wpływowi. 6. Priorytetyzacja ryzyk (przez dodanie do siebie oceny prawdopodobieństwa 7.
i wpływu). Przegląd i modyfikacja wartości (uwzględniając np. złożoność cyklomatyczną danego modułu, analizę Pareto, nowe lub zmodyfikowane
cechy, wybór metody tworzenia oprogramowania, dostęp do środowiska, użyteczność). 8. Priorytetyzacja cech i atrybutów według priorytetu ryzyka. 9. Ustalenie granicy (ang. cut line) między cechami wymagającymi testowania i tymi, które testowaniu nie będą podlegać. 10. Łagodzenie ryzyka (wybranie ryzyk, które można złagodzić przez dodanie zasobów, zmianę metodologii bądź inne czynności prewencyjne). W ocenie poziomu ryzyka SST stosuje technikę podobną do tej używanej w metodzie FMEA (patrz p. 19.9.8). O ile prawdopodobieństwo i wpływ ocenia się na skali porządkowej, o tyle poziom ryzyka powstaje w wyniku dodania tych dwóch wartości, stając się tym samym wartością na skali interwałowej lub ilorazowej. Jest to zilustrowane w tabeli 19.7. Tabela 19.7. Obliczanie poziomu ryzyka w metodzie SST
Prawdopodobieństwo awarii
Poziom ryzyka
W pływ awarii
Niskie (1)
Średnie (2)
Duże (3)
Nis ki (1)
2
3
4
Ś redni (2)
3
4
5
Wys oki (3)
4
5
6
Prawdopodobieństwo i wpływ są ustalane na skali porządkowej, ale są przypisane im konkretne wartości liczbowe (1, 2, 3). Poziom ryzyka wyraża się wtedy wzorem: Poziom ryzyka = Prawdopodobieństwo + Wpływ Skale prawdopodobieństwa i wpływu nie są ilorazowe (tzn. nie mają „zera bezwzględnego”), więc nie możemy mówić, że ryzyko o poziomie 6. jest dwa razy większe niż ryzyko o poziomie 3. Poziomy możemy jedynie porównywać między sobą, co jest na szczęście wystarczające dla potrzeb priorytetyzacji.
19.9.3. Przykład: zastosowanie SST do systemu ELROJ Rozważmy analizę ryzyka dla systemu ELROJ opisanego w Dodatku A, przeprowadzoną według metody SST. Załóżmy, że zebrano interdyscyplinarny zespół i stworzono na podstawie wymagań następujący zbiór cech oraz atrybutów systemu, wraz z oceną ich prawdopodobieństwa oraz wpływu (kroki 1.–5.): Cechy: wyświetlanie rozkładu na ekranie; prawdopodobieństwo=wysokie, wpływ=wysoki; zarządzanie rozkładem (dodawanie i usuwanie kursów oraz linii); prawdopodobieństwo=średnie, wpływ=wysoki; komunikacja oprogramowania prawdopodobieństwo=wysokie, wpływ=niski; użyteczność (obsługa interfejsu prawdopodobieństwo=średnie, wpływ=wysoki;
z
ekranem; użytkownika);
wyświetlanie komunikatu na ekranie; prawdopodobieństwo =średnie, wpływ=średni. Atrybuty: bezpieczeństwo (możliwość modyfikacji wyświetlanych danych przez nieuprawnione osoby); prawdopodobieństwo=średnie, wpływ=wysoki; wydajność (szybkość przesyłania informacji z centrali do ekranów stojących na przystankach); prawdopodobieństwo=niskie, wpływ=średni.
Następnie obliczono priorytet i uszeregowaliśmy według niego wszystkie ryzyka (krok 6.). Podczas przeglądania tak stworzonej listy (krok 7.) zdecydowano, że ze względu na niską złożoność modułu odpowiadającego za obsługę komunikatów prawdopodobieństwo wystąpienia awarii związanej z wyświetlaniem komunikatu jest zbyt duże. Obniżono jego wartość z poziomu „średnie” do poziomu „niskie”, co równocześnie wpłynęło na obniżenie poziomu ryzyka. Ostateczna, spriorytetyzowana lista ryzyk (krok 8) jest przedstawiona w tabeli 19.8. Ryzyka o poziomie mniejszym niż 4 odcięto – w tabeli jest to zaznaczone pogrubioną linią. Zdecydowano, że ryzyka te nie będą podlegały testowaniu (krok 9.). Następnie określono akcje łagodzące ryzyko dla tych cech i atrybutów, dla których uznano to za wykonalne. Komunikacja z ekranem dotyczy kwestii sprzętowych, dlatego zespół uznał, że łagodzenie tego ryzyka nie wchodzi w zakres kompetencji testerów. Tabela 19.8. Lista ryzyk dla programu ELROJ
System ELROJ Cechy
Atrybuty
Prawdopodobieństwo W pływ Prioryt
Wyś wietlacz rozkładu jazdy
wys okie
wys oki 6
Zarządzanie rozkładem
ś rednie
wys oki 5
Użytecznoś ć (interfejs )
ś rednie
wys oki 5
Bezpieczeńs two ś rednie s ys temu
wys oki 5
Komunikacja z ekranem
wys okie
nis ki
4
Wyś wietlanie komunikatu Wydajnoś ć (trans fer danych)
nis kie
ś redni
3
nis kie
ś redni
3
19.9.4. PRisMa (Product Risk Management) Metoda PRisMa została stworzona przez van Veenendaala i opublikowana w [171]. Jest to bardzo lekka i prosta w zastosowaniu metoda. Głównym jej elementem jest tzw. macierz ryzyka produktowego (ang. product risk matrix), złożona z czterech części (kwadrantów) powstałych w wyniku podzielenia każdego z czynników ryzyka (prawdopodobieństwa oraz wpływu) na dwie grupy (wartości niskie oraz wysokie) i stworzeniu w ten sposób układu 2×2 możliwych kombinacji poziomu tych czynników. Macierz ta jest przedstawiona na rysunku 19.13a. Każdy kwadrant określa inny typ ryzyka, a co za tym idzie
Rysunek 19.13. Dwie postaci macierzy ryzyka produktowego w metodzie PRisMa – wymaga innego podejścia do jego łagodzenia. Oznaczenia kwadrantów (I, II, III, IV) są jedynie etykietami i nie mają znaczenia liczbowego. Liczba części macierzy ryzyka wynosi 4 z prostego powodu: gdyby każdy czynnik ryzyka podzielić na 3 obszary, macierz miałaby 3 ⋅ 3 = 9 części i trudno byłoby przypisywać ryzyka do aż tylu możliwych kategorii. Cztery części z jednej
strony są wystarczające dla klasyfikacji, a z drugiej – ich liczba sprawia, że proces kategoryzacji jest zrozumiały i łatwy do opanowania. Dla aplikacji o znaczeniu krytycznym do macierzy ryzyka dodaje się piątą część, opisującą ekstremalny poziom ryzyka biznesowego. Powodem tego jest fakt, iż w systemach o znaczeniu krytycznym awarie mogą powodować utratę zdrowia lub życia osób. Zauważmy, że poziom prawdopodobieństwa nie ma tu żadnego znaczenia – niezależnie od tego, czy jest ono wysokie czy marginalne, mamy do czynienia ze zbyt dużym ryzykiem, aby je różnicować z tego względu. Ryzyka, które znajdą się w polu V muszą być przetestowane w maksymalnym możliwym stopniu, używając wszystkich dostępnych sił i środków. Macierz ryzyka produktowego z naniesionymi na nią ryzykami daje interesariuszom czytelny wgląd w poszczególne zagrożenia. Graficzna prezentacja sprawdza się zwykle znacznie lepiej niż długa lista ryzyk z przypisanymi do nich wartościami prawdopodobieństwa, wpływu, poziomu itp. Jak mawia znane powiedzenie, jeden obrazek jest więcej wart niż tysiąc słów. Ludzie generalnie łatwiej radzą sobie z danymi prezentowanymi w postaci graficznej i lepiej je interpretują. Poszczególne kwadranty macierzy reprezentują różne spojrzenia na ryzyka produktowe. Kwadranty I oraz II dotyczą wysokiego ryzyka technicznego (prawdopodobieństwa). Tymi ryzykami zwykle zajmować się będziemy na poziomie testów jednostkowych oraz integracyjnych. Z kolei kwadranty II i IV reprezentują wysokie ryzyko biznesowe, dlatego te typy ryzyk są zwykle łagodzone przez testy wysokopoziomowe: systemowe oraz akceptacyjne. Należy jednak podkreślić, że jest to raczej wskazówka niż reguła. W każdym projekcie sytuacja może wyglądać zupełnie odmiennie. PRisMa, tak jak poprzednio opisane metody, wymaga identyfikacji i analizy ryzyka, jednak różni się od pozostałych metod tym, że nie definiuje wprost poziomu ryzyka. Ryzyko jest charakteryzowane prawdopodobieństwem i wpływem, które nie są ze sobą w żaden sposób łączone w jedną miarę. Dlatego ryzyko jest reprezentowane nie liczbowo, ale jako punkt na dwuwymiarowej macierzy ryzyka. Jeśli idzie o identyfikację ryzyka, PRisMa wykorzystuje dwie metody: burzę mózgów oraz metodę opartą na wymaganiach, przy czym w typowym wykorzystaniu tej techniki burza mózgów jest przeprowadzana jako wspomaganie metody opartej na wymaganiach w celu poszerzenia i zweryfikowania listy ryzyk uzyskanych za pomocą tej ostatniej.
Formalny proces PRisMa jest przedstawiony na rysunku 19.14. Zadania „nadzór i kontrola” oraz „ewaluacja” nie są unikalną częścią procesu, dlatego przedstawiono je w prostokątach o przerywanych liniach. Wysokość poszczególnych zadań na rysunku oznacza poziom organizacyjny, na którym typowo odbywa się zadanie. Na przykład faza Przygotowania zwykle ma miejsce na poziomie organizacji lub programu (zbioru projektów). Projekt przebiega zwykle od zadania Planowania do Ewaluacji, przy czym kroki Spotkanie inicjujące (ang. kick-off meeting) oraz Rozszerzona identyfikacja ryzyk są opcjonalne. Właściwa identyfikacja ryzyk odbywa się podczas etapu Planowania.
Rysunek 19.14. Proces PRisMa Metodologia PRisMa zaleca, aby wyznaczyć tzw. lidera zespołu PRisMa, który będzie odpowiedzialny za organizację i moderowanie spotkań, upewnianie się, czy zespół ma odpowiednie zasoby oraz weryfikację postępów zespołu w pracach nad szacowaniem ryzyka. Lider powinien również przeprowadzić niezbędne szkolenia z metodyki PRisMa dla osób o małym lub zerowym doświadczeniu w jej stosowaniu. PRisMa jest metodyką lekką, dlatego nadaje się do stosowania w procesach opartych na zwinnych technikach wytwarzania, takich jak Scrum czy XP. Jednym z głównych celów metodyk zwinnych jest łagodzenie ryzyka, stąd PRisMa idealnie może się wpasować w projekty agile’owe. Należy jednak wziąć pod uwagę, że na wdrożenie PRisMa trzeba poświęcić określoną ilość czasu i wysiłku. Wymaga ona
również dobrego przygotowania organizacji, w szczególności uzyskania wsparcia wszystkich członków zespołu lub firmy. Omówimy teraz poszczególne fazy procesu PRisMa.
w zakresie
Przygotowanie. Obejmuje zdefiniowanie procesów, identyfikację czynników ryzyka technicznego i biznesowego, ustalenie wag dla każdego czynnika, zdefiniowanie zakresu wykorzystywanej skali ocen oraz zdefiniowanie reguł postępowania w procesie rangowania i szacowania. Nie ma uniwersalnej listy czynników, które można by użyć przy ocenie prawdopodobieństwa lub wpływu ryzyka. PRisMa proponuje w kroku Przygotowania rozważenie w szczególności następujących czynników: Czynniki dla wpływu: obszary krytyczne (identyfikowane na podstawie analizy użycia systemu oraz tego, w jaki sposób system może ulec awarii); obszary widoczne (czyli takie, w których użytkownicy mogą bezpośrednio odczuć awarię, jeśli coś pójdzie nie tak, jak trzeba); najczęściej używane obszary (podział funkcji na użytkownicy używają zawsze, często, okazjonalnie
te, których lub rzadko
i oszacowanie wpływu na podstawie tej klasyfikacji); istotność z biznesowego punktu widzenia (tzn. jaka jest ważność poszczególnych cech systemu z punktu widzenia biznesowego celu jego działania); koszt zmian (zwykle wykorzystywany w tzw. systemach systemów). Czynniki dla prawdopodobieństwa: złożoność (np. w sensie złożoności cyklomatycznej, skomplikowanej logiki); liczba zmian (jest ona ważnym czynnikiem defektotwórczym [177]); nowe technologie i metody; presja czasu; brak doświadczenia; rozproszenie geograficzne zespołu; kod nowy vs. re-używalny; interfejsy; rozmiar;
historia defektów; jakość wymagań. Wagi czynników można ustalić według własnego uznania lub zastosować typowe wartości zdefiniowane w PRisMa, pokazane w tabeli 19.9. Tabela 19.9. Wagi czynników w PRisMa
W aga Uzasadnienie 0,5
Czynnik jes t ważny, ale mniej ważny niż inne czynniki w identyfikacji teg o, co należy tes tować
1,0
S tandardowa wag a (wartoś ć początkowa)
1,5
Czynnik jes t ważniejs zy w identyfikacji teg o, co należy tes tować
2,0
Czynnik jes t krytyczny w identyfikacji teg o, co należy tes tować
Jeśli chodzi o oceny, PRisMa oferuje trzy możliwe skale: od 1 do 3, od 1 do 5 lub zbiór wartości 0, 1, 3, 5, 9. Pierwsze dwie omówiliśmy w punkcie 19.6.3. Trzecia, tak jak druga, jest skalą typu Likerta zaczerpniętą z metody Quality Function Deployment omówionej w punkcie 19.9.10. Jest ona pewnego rodzaju kompromisem między wykorzystaniem dużej, dziesięciostopniowej skali a tym, że używając skal o dużym zasięgu, ludzie niechętnie wybierają wartości skrajne. Zdefiniowanie reguł postępowania jest ważne ze względu na konieczność stosowania całego zakresu wykorzystywanej skali. PRisMa stosuje techniki matematyczne (test chikwadrat) do statystycznej weryfikacji równomiernego wykorzystania wszystkich stopni skali w ocenie poszczególnych ryzyk. Ma to zapobiec powszechnie występującej tendencji do klastrowania wyników, tzn. grupowania wokół konkretnych wartości. Ponadto istnieją ogólne zasady związane z procedurą oceniania: Reguła oceny – każdy interesariusz zaangażowany w proces oceny musi ocenić każdy przydzielony do ewaluacji element. Reguła oceny końcowej – lider zespołu PRisMa musi obliczyć końcową ocenę każdego elementu na podstawie wszystkich ocen cząstkowych.
Reguła koła3 (ang. circle rule) – obszar koła jest zdefiniowany w centralnej części macierzy ryzyka. Elementy, które znajdują się w obszarze tego koła muszą zostać poddane procesowi uzgodnienia oceny, w wyniku którego nastąpi ostateczne przyporządkowanie ryzyka do jednego z kwadrantów (patrz rys. 19.15). Koło obejmuje obszar „niepewności”, ponieważ im bliżej środka macierzy znajduje się ryzyko, tym większa niepewność co do przynależności do określonego kwadrantu. Przypisanie ryzyka do jednego z czterech pól jest kluczowe, ponieważ skutkuje przyjęciem odmiennych strategii łagodzenia ryzyka. W narzędziach wspomagających proces PRisMa promień koła zdefiniowany jest na podstawie określonej przez użytkownika procentowej powierzchni, jaką ma zajmować. Na rysunku 19.15 widzimy, że przynależność elementu nr 3 musi zostać ustalona na spotkaniu uzgadniającym, ponieważ jako jedyny wpada w obszar koła.
Rysunek 19.15. Macierz ryzyka PRisMa i reguła koła Reguła uzgadniania. Wynik uznaje się za uzgodniony, jeśli rozkład ocen danego elementu ryzyka dokonanych przez wszystkich interesariuszy mieści się w założonym przedziale lub ma odpowiednio niską wariancję. Planowanie. W fazie planowania, przeprowadzanej typowo przez kierownika testów, wykonuje się następujące zadania: zdefiniowanie zakresu produktu, który będzie poddany analizie ryzyka;
dostrojenie czynników i reguł; zebranie niezbędnej dokumentacji, np. dokumentu wymagań czy projektu architektury; identyfikacja ryzyk produktowych – bazuje ona na zbiorze wymagań oraz innych dostępnych dokumentach, np. projekcie architektury; lista ryzyk może być rozszerzona lub uszczegółowiona w wyniku przeprowadzenia sesji burzy mózgów; identyfikacja i wybór interesariuszy – zespół analizy ryzyka powinien być możliwie interdyscyplinarny, umożliwiając różnorodne punkty widzenia na jakość systemu; nie powinien liczyć więcej niż 10 osób; przypisanie czynników do interesariuszy – nie każdy interesariusz musi oceniać poziom danego ryzyka; każdy powinien oceniać te aspekty systemu, które są ważne z jego punktu widzenia. Podczas
identyfikacji
ryzyk
produktowych
każde
ryzyko
musi
być
jednoznacznie opisane unikatowym identyfikatorem i wyrażone w języku zrozumiałym dla interesariuszy. Dla każdego ryzyka powinien być określony jego typ (np. funkcjonalność, użyteczność, niezawodność) oraz powiązanie z odpowiadającym mu wymaganiem. Spotkanie inicjujące (krok opcjonalny). Jego celem jest szczegółowe poinformowanie członków zespołu o planowanym procesie analizy ryzyka oraz uzyskanie odpowiedniego zaangażowania ludzi. Rozszerzona identyfikacja ryzyk (krok opcjonalny). Celem tego kroku jest rozszerzenie początkowego zbioru ryzyk określonego w fazie Planowania, jego ocena, a także wykrycie potencjalnych niezgodności między wymaganiami a ryzykami produktowymi. W tej fazie używa się techniki burzy mózgów, w szczególności jednej z jej odmian, mianowicie tzw. katastrofalnej burzy mózgów. W technice tej każdy z uczestników jest proszony o wyobrażenie sobie kilku najgorszych, katastrofalnych, czarnych scenariuszy związanych z analizowanym systemem. Podczas analizy w szczególności mogą pojawić się dwa typy problemów: jest ryzyko, ale nie ma wymagania – w takim przypadku należy dodać wymaganie (aby wykryć defekty możliwie wcześnie) lub usunąć ryzyko (jeśli nie chcemy testować więcej, niż jest to konieczne);
jest wymaganie, ale nie ma ryzyka – w takim przypadku należy rozszerzyć listę istniejących ryzyk (w celu uzyskania lepszego pokrycia testami) lub usunąć wymaganie (jeśli nie chcemy tworzyć więcej, niż jest to konieczne). Indywidualne przygotowania. Jego celem jest dostarczenie indywidualnych, niezależnych danych wejściowych do procesu analizy ryzyka oraz ocena czynników przypisanych poszczególnym ryzykom i przesłanie wyników do kierownika testów. PRisMa wymaga, aby rozkład poszczególnych ocen dla każdego czynnika ryzyka był równomierny, tzn. aby oceny nie skupiały się np. na granicach skali czy w jakimś jednym miejscu. Aby uzyskać taki rozkład, należy po kolei rozważać każde ryzyko i porównywać je z już ocenionymi (czy jest ważniejsze czy mniej istotne od innych). W ten sposób każde ryzyko otrzyma swoją pozycję w szeregu. Ryzyka najważniejsze otrzymują maksymalną ocenę, a najmniej istotne – najniższą. Przetwarzanie indywidualnych ocen. Celem tego kroku jest upewnienie się, że wszyscy uczestnicy przesłali swoje oceny, zebranie wszystkich ocen i stworzenie na ich podstawie pierwszej, roboczej wersji macierzy ryzyka. Należy również sprawdzić, czy nie zostały złamane reguły oceniania (w takim przypadku należy to udokumentować) oraz przygotować się do spotkania uzgadniającego. Spotkanie uzgadniające. Na spotkaniu konfrontuje się uczestników ze sposobem postrzegania ryzyk przez pozostałych członków zespołu. Dyskutuje się prawdopodobieństwa oraz wpływy poszczególnych ryzyk. W przypadku istnienia uwag dotyczących złamania reguł (patrz faza Przetwarzania indywidualnych ocen), należy te uwagi przedyskutować oraz poprawić lub uzupełnić błędne oceny. W fazie tej należy uzyskać możliwie najdalej idący konsensus dotyczący poziomu poszczególnych ryzyk oraz ich pozycji na macierzy ryzyka. W szczególności trzeba dojść do porozumienia w kwestii przypisania właściwych kwadrantów ryzykom znajdującym się w obszarze koła. PRisMa proponuje kilka technik uzyskiwania konsensusu: głosowanie; zaangażowanie eksperta; upoważnienie jednego z członków zespołu do podjęcia ostatecznej decyzji; przedyskutowanie sprawy;
wybór surowszej oceny (spowoduje to więcej prac nad łagodzeniem ryzyk, ale jest szybką metodą dojścia do porozumienia). Zdefiniowanie zróżnicowanych podejść testowania opartego na ryzyku. Celem tego kroku jest zdefiniowanie podejścia do testowania zgodnego z pozycją poszczególnych ryzyk na macierzy ryzyka. Podejście to musi być skuteczne (koncentracja na najważniejszych ryzykach) i efektywne (nie marnowanie czasu na długie testowanie ryzyk o niskim priorytecie). W fazie tej należy również upewnić się, że testerzy przeprowadzają czynności testowe w zgodzie z ryzykami produktowymi. Istnieje wiele technik testowania. W dokumencie „Test Management Quick Reference Card” opublikowanym przez Improve Quality Services [178] można znaleźć rozkład tych technik w poszczególnych kwadrantach macierzy ryzyka. Oczywiście każdy system wymaga innego podejścia w testowaniu, niemniej jednak rozkład ten można traktować jako sugestię czy źródło inspiracji. Jest on pokazany na rysunku 19.16. Poszczególne skróty pojęć z rysunku oznaczają: 0-switch – pokrycie 0-przełączeń (krawędziowe); >=1-switch – pokrycie 1-przełączeń (par krawędzi) i wyższe (ścieżek długości 3, 4 itd.); AWB – analiza wartości brzegowych; DrzKl – drzewa klasyfikacji;
Rysunek 19.16. Techniki projektowania testów a kwadranty macierzy PRisMa ECT – Elementary Comparison Test (testowanie decyzji, rozgałęzień itp.); GrP-S – grafy przyczynowo-skutkowe; KR – testowanie klas równoważności; PCT TMn – Process Cycle Test, Test Measure n (testowanie procesów biznesowych tak, aby pokryć wszystkie kombinacje przepływu przechodzącego przez n kolejnych decyzji; jest to metoda analogiczna do pokrycia ścieżek o długości n w testowaniu białoskrzynkowym); PIT – Program Interface Test (testowanie integracyjne interfejsów); PrzUż – testowanie przypadków użycia (P = przebieg podstawowy, A = przebieg alternatywny, W = przebieg z wyjątkiem);
RLT – Real Life Test (testowanie na podstawie profilu operacyjnego); Sem – testowanie semantyczne (ang. semantic testing), tzn. testowanie poprawności danych; Składnia – testowanie składni (ang. syntax testing), tzn. testowanie poprawności struktury; TExp – testowanie eksploracyjne; ZgadBłędów – zgadywanie błędów. podstawowe testowanie porównawcze (ang. Elementary Comparison Testing, ECT) – czarnoskrzynkowa technika projektowania przypadków testowych projektowanych tak, aby wykonać kombinację wejść, wykorzystując metodę testowania zmodyfikowanego pokrycia warunków wielokrotnych (MC/DC) [28] test cyklu procesu (ang. Process Cycle Test, PCT ) – czarnoskrzynkowa technika projektowania przypadków testowych, w której testy są projektowane w celu wykonania procesu lub procedury biznesowej. Polega na pokryciu wszystkich ścieżek o zadanej długości w diagramie przepływu procesu [28] Poza samymi technikami projektowania testów PRisMa proponuje jeszcze inne podejścia do testowania, takie jak: przeglądy i inspekcje, spotkania inicjujące projekt testów, przeglądy projektów testów, stosowanie odpowiedniego poziomu szczegółowości przypadków testowych, utrzymywanie określonego poziomu niezależności testerów, wykorzystanie doświadczonych pracowników, stosowanie testów potwierdzających i testów regresji oraz wykorzystywanie kryteriów zakończenia.
19.9.5. Przykład: zastosowanie PRisMa do systemu ELROJ Rozważmy ponownie system ELROJ opisany w Dodatku A. Stosujemy metodę PRisMa do analizy ryzyka w tym systemie. W fazie Przygotowania zdefiniowano następujące czynniki. Dla wpływu: widoczne obszary (WidOb) oraz istotność z biznesowego punktu widzenia (IstBiz) z wagami odpowiednio 1,0 oraz 2,0. Dla prawdopodobieństwa: złożoność (Złoż) oraz nowe technologie i metody (NTech), ponieważ zespół będzie pierwszy raz pracował nad systemem komunikującym się ze sprzętem (ekrany na przystankach). Wagi dla tych czynników określono odpowiednio jako 1,0 oraz 2,0. Ustalono, że wykorzystana będzie skala Likerta (od
1 do 5). Wykorzystane zostaną standardowe, ogólne zasady oceniania zdefiniowane w PRisMa. W fazie Planowania określono trzech interesariuszy: kierownika projektu, analityka biznesowego, oraz architekta systemu. Ponadto zidentyfikowano następujące ryzyka: 1) brak komunikacji systemu z ekranem; 2) niepoprawność wyświetlanych wyników na ekranie; 3) niewygodny interfejs użytkownika; 4) błędy w logice biznesowej systemu (zarządzanie liniami i kursami); 5) wolne działanie systemu. Interesariuszom przypisano następujące czynniki: Kierownikowi projektu: widoczne obszary, istotność biznesowa, złożoność, nowe technologie. Analitykowi biznesowemu: widoczne obszary, istotność biznesowa. Architektowi systemu: złożoność, nowe technologie. Analityk biznesowy nie powinien oceniać ryzyk pod kątem czynników technicznych (bo się na tym nie zna i jego ocena może być wypaczona). Podobnie architekt nie powinien wykorzystywać czynników biznesowych. W fazie indywidualnego przygotowania wszyscy trzej członkowie zespołu dokonali analizy ryzyk, oceniając je na podstawie przypisanych im czynników. Wyniki są zebrane w tabelach 19.10–19.12. Tabela 19.10. Szacowanie ryzyka przez kierownika projektu
Kierownik projektu
W pływ
Prawdopodobieństwo
Czynniki
W idOb IstBiz Złoż
NT ech
W agi
1,0
2,0
1,0
2,0
R1: brak komunikacji z ekranem
4
5
1
5
R2: niepoprawna informacja na ekranie
5
5
3
1
R3: niewyg odny interfejs
2
1
1
2
R4: błędy w log ice biznes owej 1
3
5
4
R5: wolne działanie s ys temu
4
4
4
2
Tabela 19.11. Szacowanie ryzyka przez analityka biznesowego
Analityk biznesowy
W pływ
Prawdopodobieństwo
Czynniki
W idOb IstBiz Złoż
NT ech
W agi
1,0
2,0
2,0
R1: brak komunikacji z ekranem
5
3
R2: niepoprawna informacja na ekranie
5
5
R3: niewyg odny interfejs
1
1
R4: błędy w log ice 3 biznes owej
2
R5: wolne 4 działanie s ys temu
3
1,0
te czynniki nie podleg ają ocenie, g dyż nie zos tały przypis ane analitykowi biznes owemu
Tabela 19.12. Szacowanie ryzyka przez architekta systemu
Architekt systemu
W pływ
Prawdopodobieństwo
Czynniki
W idOb
IstBiz
Złoż
NT ech
W agi
1,0
2,0
1,0
2,0
R1: brak komunikacji z ekranem
3
5
R2: niepoprawna informacja na ekranie
5
1
1
1
R4: błędy w log ice biznes owej
4
4
R5: wolne działanie s ys temu
1
3
te czynniki nie podleg ają ocenie, g dyż nie zos tały R3: niewyg odny przypis ane architektowi interfejs s ys temu
W kroku Przetwarzanie indywidualnych ocen kierownik testów uśrednia oceny, otrzymując wynik przedstawiony w tabeli 19.13. Tabela 19.13. Uśrednione oceny
Uśrednione oceny
W pływ
Czynniki
W idOb IstBiz Złoż
Prawdopodobieństwo NT ech
W agi
1,0
2,0
1,0
2,0
R1: brak komunikacji z ekranem
4,5
4,0
2,0
5,0
R2: niepoprawna informacja na ekranie
5,0
5,0
4,0
1,0
R3: niewyg odny interfejs
1,5
1,0
1,0
1,5
R4: błędy w log ice biznes owej 2,0
2,5
4,5
4,0
R5: wolne działanie s ys temu
3,5
2,5
3,5
3,0
Uśrednione oceny są mnożone przez odpowiadające im czynniki. W wyniku otrzymujemy ważoną ocenę. Rezultat jest przedstawiony w tabeli 19.14. Tabela 19.14. Ważona ocena ryzyk produktowych
Ryzyko
W pływ
R1: brak komunikacji z ekranem 4,5+8,0=12,5
Prawdopodobieństwo 2,0+10,0=12,0
R2: niepoprawna informacja na 5,0+10,0=15,0 4,0+2,0=6,0 ekranie R3: niewyg odny interfejs
1,5+2,0=3,5
1,0+3,0=4,0
R4: Błędy w log ice biznes owej
2,0+5,0=7,0
4,5+8,0=12,5
R5: Wolne działanie s ys temu
3,0+7,0=10,0
2,5+7,0=9,5
Każde z ryzyk jest nanoszone na macierz ryzyka. W wyniku otrzymujemy macierz przedstawioną na rysunku 19.17. Współrzędne każdego z ryzyk odpowiadają wartościom jego wpływu oraz prawdopodobieństwa. Wartości minimalne i maksymalne obu osi są wyznaczane na podstawie teoretycznych wartości skrajnych ocen, jakie można uzyskać. Minimalna ocena wpływu to 1,0 ⋅ 1 + 2,0 ⋅ 1 = 3, natomiast maksymalna to 1,0 ⋅ 5 + 2,0 ⋅ 5 = 15. Takie same wartości skrajne przyjmuje skala dla prawdopodobieństwa. Środkową wartością na obu skalach jest 9, zatem punkt o współrzędnych (9,9) stanowi środek macierzy. W fazie Spotkanie uzgadniające należy ustalić przynależność do odpowiedniego kwadrantu ryzyka R5 (wolne działanie systemu), które jako jedyne znalazło się w obszarze koła. Zespół ustalił, że ze względu na niedużą liczbę przetwarzanych
danych oraz dobrą przepustowość sieci ryzyko to cechować się będzie niskim prawdopodobieństwem. W rezultacie ryzyko przechodzi z kwadrantu II (wysoki wpływ, wysokie prawdopodobieństwo) do kwadrantu IV (wysoki wpływ, niskie prawdopodobieństwo). W ostatniej fazie (zdefiniowanie zróżnicowanych podejść testowania opartego na ryzyku) są ustalane metody łagodzenia ryzyka. W naszym przypadku będzie to ustalenie odpowiednich metod testowania. Wyniki tej fazy są przedstawione w tabeli 19.15.
Rysunek 19.17. Macierz ryzyka produktowego dla systemu ELROJ Tabela 19.15. Metody łagodzenia ryzyka dla systemu ELROJ
Ryzyko Kwadrant W pływ Prawdopodobieństwo
Metody łagodzenia
R1
wys oki wys okie
wczes ny prototyp s ys temu s prawdzający połączenie z ekranem
wys oki nis kie
tes towanie białos krzynkowe (kryterium MC/DC) oraz ins pekcje kodu
wys oki nis kie
tes ty wydajnoś ciowe, ins pekcja projektu bazy danych
nis ki
wys okie
tes towanie eks ploracyjne oraz tes towanie oparte na przypadkach użycia
nis kie
brak (poziom znikomy, nie ma potrzeby alokacji zas obów na tes towanie teg o ryzyka)
R2
R5
R4
R3
II
IV
IV
I
III
nis ki
W wyniku przeprowadzenia analizy ryzyka metodą PRisMa mamy zidentyfikowane ryzyka, określony ich typ oraz dobrze zdefiniowane akcje łagodzenia tych ryzyk. Przeprowadzenie takiego procesu pozwala w olbrzymim stopniu zredukować poziom ryzyka w systemie. Już sam fakt identyfikacji ryzyk (stworzenie macierzy ryzyka) jest dużym sukcesem, a i tak wciąż – jeśli chodzi o analizę ryzyka – wiele firm nie dochodzi nawet do tego etapu!
19.9.6. Analiza zagrożeń (hazard analysis) Analiza zagrożeń to metoda identyfikacji i łagodzenia zagrożeń, stosowana najczęściej w systemach o znaczeniu krytycznym, gdzie awaria może spowodować poważne straty, włącznie z utratą zdrowia lub życia. Zwykle analiza zagrożeń dotyczy kwestii bezpieczeństwa (ang. safety) systemu. bezpieczeństwo systemu (ang. system safety) – jest osiągane przez stosowanie zasad, kryteriów i technik z zakresu inżynierii oraz zarządania w celu osiągnięcia akceptowalnego poziomu ryzyka awarii (ang. mishap risk), uwzględniając ograniczenia operacyjnej efektywności, odpowiedniości, czasu i kosztu w ciągu całego cyklu życia systemu [179] analiza zagrożeń (ang. hazard analysis) – technika używana do charakteryzowania elementów systemu wpływających na pojawienie się ryzyka; wynik analizy zagrożeń rzutuje na wybór metod używanych do wytwarzania i testowania oprogramowania. Patrz także: analiza ryzyka Pojęcie „analizy zagrożeń” obejmuje tak naprawdę wiele różnych metod. Dwie z nich – FMEA oraz FTA – omówimy dokładniej w punktach 19.9.8 oraz 19.9.12. Inne przykładowe metody analizy zagrożeń bezpieczeństwa to: Preliminary Hazard List, Preliminary Hazard Analysis, Subsystem Hazard Analysis, System Hazard Analysis, Operating and Support Hazard Analysis, Health Hazard Assessment, Safety Requirements/Criteria Analysis (formalnie zdefiniowane i promowane w [179]); Event Tree Analysis;
Fault Hazard Analysis; Functional Hazard Analysis; Sneak Circuit Analysis; Petri Net Analysis; Markov Analysis; Barrier Analysis; Bent Pin Analysis; Hazard and Operability Analysis; Cause-Consequence Analysis; Common Cause Failure Analysis; Management Oversight Risk Tree Analysis; Software Safety Assessment. Istnieją również inne metody analizy zagrożeń stosowane w różnych dziedzinach przemysłu (np. spożywczego, chemicznego, samochodowego). W inżynierii oprogramowania najczęściej wykorzystywaną metodą jest FMEA lub jej odmiany (SFMEA, FMECA). W analizie zagrożeń wyróżnia się trzy kluczowe pojęcia: zagrożenia (ang. hazard), awarii (ang. mishap) oraz ryzyka (ang. risk). W celu uzyskania bezpiecznego systemu zagrożenia muszą być wyeliminowane lub łagodzone (redukcja ryzyka). Identyfikacja zagrożeń jest najważniejszą czynnością dla osiągnięcia tego celu. Awaria to inaczej wypadek lub nieplanowane zdarzenie skutkujące śmiercią, obrażeniami, utratą zdrowia, stratą sprzętu lub zanieczyszczeniem środowiska. Zagrożenie to każda rzeczywista lub potencjalna okoliczność powodująca powstanie ryzyka awarii. Ryzyko to wpływ oraz możliwość awarii wyrażona w terminach jej uciążliwości (ang. severity) oraz prawdopodobieństwa wystąpienia. Aby dobrze zrozumieć różnicę oraz zależności między zagrożeniem a awarią, można wyobrazić sobie te dwa pojęcia jako dwa odrębne stany tego samego zjawiska: stan „przed” i stan „po”. Przejście z jednego do drugiego, czyli z zagrożenia do awarii następuje w wyniku zaistnienia czynników ryzyka. Sytuację tę zilustrowano na rysunku 19.18. Awarie to zmaterializowane zagrożenia.
Rysunek 19.18. Związek między zagrożeniem, ryzykiem a awarią Ryzyko awarii oblicza się tak, jak to zdefiniowaliśmy wcześniej – jako iloczyn wpływu oraz prawdopodobieństwa. Różnica między metodami typu SST czy PRisMa a analizą zagrożeń jest dosyć subtelna. W analizie zagrożeń nie poprzestajemy na określeniu ryzyk, ale szukamy głębszych przyczyn, najlepiej przyczyn źródłowych powodujących wystąpienie tych ryzyk. Zagrożeniu przypisuje się prawdopodobieństwo 0 lub 1, bo albo zagrożenie występuje, albo nie. Wystąpienie awarii z kolei ma prawdopodobieństwo między 0 a 1. Jego wielkość określana jest na podstawie tzw. czynników zagrożeń (ang. Hazard Casual Factor, HCF), którymi są elementy zagrożeń (ang. Hazardous Elements, HE) oraz mechanizmy inicjujące (ang. Initiating Mechanisms, IM). Przykładem elementu zagrożeń może być fragment kodu o wysokim stopniu złożoności, a mechanizmem inicjującym – wywołanie tego kodu w określonych okolicznościach.
19.9.7. Koszt ekspozycji ryzyka (cost of exposure) Koszt ekspozycji ryzyka (ang. cost of exposure) to inaczej ilościowe określanie poziomu ryzyka. Przykład jej zastosowania pokazaliśmy wcześniej w podrozdziale 19.1 i punkcie 19.6.5. Koncepcja ta została zaczerpnięta ze świata finansów. Wielu ekspertów stoi na stanowisku, że o ile w finansach czy bankowości metoda ta jest bardzo pomocna, o tyle w inżynierii jakości oprogramowania ilościowe szacowanie ryzyka nie jest zbyt skuteczne i jako takie jest działaniem niezalecanym. Tak naprawdę powodzenie metody zależy od
stopnia precyzji, z jaką jesteśmy w stanie określić stratę poniesioną w wyniku zaistnienia
ryzyka
oraz prawdopodobieństwo
jego
wystąpienia. Niestety,
w praktyce szacunki te są zwykle obarczone zbyt dużym błędem. Niezależnie od tych krytycznych uwag, koszt ekspozycji jest metodą przydatną do wyboru czynności łagodzących ryzyko, stosowaną w tzw. analizie zysków i kosztów. Boehm [180] zdefiniował pojęcie wpływu redukcji ryzyka (ang. Risk Reduction Leverage, RRL) pozwalające ilościowo ocenić stosunek zysków do kosztów wynikający z przeprowadzenia danych akcji łagodzących. Miara ta wyraża zysk z inwestycji (ang. Return On Investment, ROI) polegającej na redukcji określonego ryzyka przy użyciu wartości oczekiwych, czyli koszt ekspozycji. RRL jest definiowany jako różnica między kosztem ekspozycji ryzyka (ang. – Risk Exposure, RE) przed zastosowaniem czynności łagodzących i po nim podzielona przez koszt tych czynności:
Rozważmy prosty przykład. Załóżmy, że zidentyfikowaliśmy ryzyko R o prawdopodobieństwie 25% i wpływie (stracie) w wysokości 300 000 USD i że możemy je złagodzić na trzy sposoby: 1) stosując dodatkową fazę testów – koszt to 150 000 USD; czynność ta redukuje prawdopodobieństwo do 4%, nie zmniejszając wartości straty; 2) dokonując transferu ryzyka i stosując kary umowne – koszt transferu to 20 000 USD; prawdopodobieństwo się nie zmieni, ale ze względu na kary umowne nastąpi redukcja wpływu do poziomu 180 000 USD; 3) przeprowadzając inspekcję – jej koszt to 2000 USD; powoduje ona redukcję prawdopodobieństwa do 15%, nie zmniejszając wartości wpływu. Wyniki analizy wpływu redukcji ryzyka są przedstawione w tabeli 19.16. Wartości REpo są obliczane jako iloczyny prawdopodobieństw i wpływów poszczególnych ryzyk. Znając koszt każdego wariantu oraz dane dotyczące oryginalnego ryzyka, możemy obliczyć RRL dla każdego z wariantów. Na przykład dla wariantu 1 wynosi on
Tabela 19.16. Wpływ redukcji ryzyka
Ryzyko
Prawdopodobieństwo przed
W pływ przed RE przed [USD] [USD]
R
0,25
300 000
75,000
Alternatywy: Prawdopodobieństwo po
W pływ po
RE po
Koszt
Wariant 1
0,04
300 000
12 000
150 000
Wariant 2
0,25
180 000
45 000
20 000
Wariant 3
0,20
300 000
60 000
2 000
Jeśli współczynnik RRL jest mniejszy od jedności, to odpowiedni wariant jest dla nas opłacalny, jeśli większy od jedności – nie jest opłacalny. Im bardziej RRL jest oddalony na lewo od 1, tym większy zysk z wdrożenia danego wariantu. Z naszej analizy wynika, że opłaca się wdrożyć czynności łagodzące ryzyko w wariancie pierwszym, czyli stosując dodatkową fazę testów.
19.9.8. FMEA (Failure Mode and Effect Analysis) Failure Mode and Effect Analysis (FMEA), czyli analiza przyczyn i skutków awarii to systematyczna metoda służąca do rozpoznania i oceny potencjalnych awarii systemu oraz poziomu tych zagrożeń. Została ona wprowadzona w 1949 roku przez armię Stanów Zjednoczonych jako formalna technika analizy i obecnie jest zdefiniowana jako standard MIL-STD-1629A [181]. Jest to metoda typu bottom-up, gdzie analizę ryzyk rozpoczyna się od najniższego poziomu obejmującego najdrobniejsze detale i na tej podstawie dokonuje ogólnej oceny powodzenia całego projektu lub ryzyk związanych z częścią systemu, która jest poddana analizie. FMEA ma możliwość włączenia do analizy wskaźników ryzyk (ang. failure rate) dla każdej potencjalnej awarii, co pozwala na przeprowadzenie ilościowej, probabilistycznej analizy. Technika ta umożliwia również analizę trybów awarii powodujących przejście systemu w niepożądany stan, co daje możliwość traktowania jej jako narzędzia do analizy zagrożeń. FMECA (Failure Mode, Effects
and Criticality Analysis) jest odmianą FMEA, w której przeprowadza się bardziej szczegółową analizę przyczyn awarii, uwzględniając w niej poziom krytyczności oraz ocenę sposobów wykrywania potencjalnych awarii. analiza przyczyn i skutków awarii (oprogramowania) (ang. (Software) Failure Mode and Effect Analysis, (S)FMEA) – systematyczne podejście do identyfikacji i analizy ryzyka polegające na wskazywaniu możliwych trybów (ang. mode) awarii i metod zapobiegających ich wystąpieniu lub łagodzących ich wystąpienia analiza przyczyn, skutków i krytyczności awarii (oprogramowania) (ang. – (Software) Failure Mode, Effect and Criticality Analysis, (S)FMECA) – rozszerzenie FMEA o analizę krytyczności, która jest używana do wyznaczania prawdopodobieństw wystąpienia stanów awarii z uwzględnieniem poziomu ich dotkliwości; wynikiem jest uwypuklenie stanów awarii z odpowiednio wysokim
prawdopodobieństwem
i
dotkliwością,
co
pozwala
na
ukierunkowanie działań zapobiegawczych tam, gdzie przyniosą największe korzyści Celem FMEA jest ocena efektów każdej możliwej przyczyny awarii tak, aby stwierdzić czy istnieje konieczność wprowadzenia zmian projektowych do określonych funkcjonalności ze względu na nieakceptowalny poziom niezawodności, bezpieczeństwa czy jakości działania systemu. FMEA oryginalnie została zaprojektowana do oceny wpływu awarii na niezawodność, ale w praktyce jest używana do identyfikowania wszelkich zagrożeń wynikających ze zidentyfikowanych trybów awarii. Nie zaleca się stosowania FMEA do analizy bezpieczeństwa, ponieważ metoda ta nie uwzględnia awarii powstałych w wyniku zaistnienia kombinacji różnych czynników. W takim przypadku lepszą metodą jest np. Fault Tree Analysis omówiona w punkcie 19.9.12. Przeprowadzenie FMEA pozwala na udzielenie odpowiedzi na następujące pytania [182]: co może ulec awarii? w jaki sposób może to ulec awarii? jak często będzie to ulegało awarii? jakie są konsekwencje awarii?
jak awaria wpływa na niezawodność/bezpieczeństwo systemu? W ogólnym zarysie FMEA składa się z następujących kroków: 1. Zdefiniowanie analizowanego systemu. Aby przeprowadzać jakąkolwiek analizę, musimy dokładnie znać system, jego architekturę, części składowe, wewnętrzne i zewnętrzne interfejsy itd. 2.
Zdefiniowanie
możliwych
typów
awarii
oraz
oszacowanie
ich
częstotliwości. 3. Analiza systemu. Identyfikacja potencjalnych przyczyn awarii oraz ich efektów. 4. Identyfikacja metod detekcji awarii oraz działań korekcyjnych, naprawczych bądź prewencyjnych. 5. Określenie przyczyn awarii, ich konsekwencji, predykcja niezawodności, lista zagrożeń i ryzyk, lista krytycznych elementów (ang. Critical Item List, CIL). FMEA przeprowadzana jest zwykle przy użyciu arkusza kalkulacyjnego, w którym uzupełnia się tabelę FMEA. Istnieje wiele różnych wersji takiej tabeli, które różnią się poziomem szczegółowości niektórych informacji. Tabela używana przez nas w przykładzie w punkcie 19.9.9 będzie zawierała następujące informacje: nazwa funkcji, w której może wystąpić awaria; możliwa awaria (ryzyko produktowe); możliwa przyczyna awarii; konsekwencje awarii; Pr. – prawdopodobieństwo wystąpienia awarii (ang. likelihood); W. – wpływ; Dotkl. – dotkliwość (ang. severity); w niektórych wersjach FMEA ten parametr się pomija; RPN – priorytet ryzyka (ang. Risk Priority Number), definiowany jako iloczyn: RPN = Pr ⋅ W ⋅ Dotkl;
metoda wykrywania i zalecane czynności. Prawdopodobieństwo, wpływ oraz dotkliwość można wyrażać na skali porządkowej. Najczęściej stosuje się tu skalę typu Likerta (od 1 do 5, gdzie 1 oznacza efekt znikomy, a 5 – olbrzymi). Priorytet ryzyka jest formalnie ujęty na skali interwałowej, ponieważ jest wynikiem mnożenia trzech liczb. Wartość współczynnika RPN pozwala na priorytetyzację zidentyfikowanych i oszacowanych ryzyk.
19.9.9. Przykład: zastosowanie FMEA do systemu ELROJ W przykładzie tym nie przeprowadzimy pełnej analizy, ponieważ zajęłoby to zbyt dużo miejsca. Przykład ma tylko ilustrować zastosowanie metody. W rzeczywistych projektach produktem wyjściowym FMEA jest bardzo obszerna lista awarii. Aby zastosować FMEA do systemu ELROJ (patrz Dodatek A), najpierw musimy przeanalizować system i jego strukturę. Pomocny w tym będzie wysokopoziomowy projekt architektury systemu ELROJ, przedstawiony na rysunku A.2 (s. 1011). ELROJ składa się z następujących komponentów: system plików; baza danych; aplikacja; GUI; ekran. Aplikacja ma różne funkcjonalności, takie jak: zarządzanie komunikatami; zarządzanie liniami autobusowymi; zarządzanie kursami; wyświetlanie informacji na ekranie; ustawianie daty. Dla każdego z komponentów i dla każdej z funkcji określamy możliwe przyczyny i skutki awarii wraz z oceną prawdopodobieństwa, wpływu i dotkliwości. Przykładowy wynik takiej analizy pokazany jest w tabeli 19.17.
Z przeprowadzonej analizy wynika, że ryzyka: fizycznego uszkodzenia systemu plików, braku komunikacji z ekranem, awarii ekranu oraz wyświetlanie błędnej informacji są awariami o największym priorytecie. Identyfikacja możliwych przyczyn pozwala na przeprowadzenie czynności, które powinny złagodzić lub wyeliminować te zagrożenia. FMEA pokazuje nam dużą rolę inspekcji formalnych oraz czynności związanych z kontrolami okresowymi infrastruktury. Okazuje się, że – przynajmniej w tym niewielkim, uproszczonym przykładzie – błędy związane z oprogramowaniem wcale nie są najważniejsze. Analiza pozwoliła nam dostrzec dużą rolę sprzętu i komunikacji w zapewnianiu jakości całego systemu. Ryzyka zduplikowania kursu oraz błędu zapisu do bazy danych mają najniższy priorytet ze względu na niskie prawdopodobieństwo ich wystąpienia lub zaniedbywalny poziom wpływu bądź dotkliwości. Akcje z nimi związane możemy zostawić na sam koniec lub – w przypadku braku czasu – opuścić. Tabela 19.17. FMEA dla programu ELROJ
Funkcja
Możliwa awaria
Możliwa przyczyna awarii
S ys tem plików
Fizyczne us zkodzenie
Mechaniczna Utrata danych
5
1
1
S ys tem plików
Błąd odczytu
Wyś wietlenie Błąd aplikacji niepoprawnej informacji
4
3
2
Baza danych
Błąd zapis u
Narus zenie klucza
5
3
3
Konsekwencje Pr. W . Dotk awarii
Brak możliwoś ci
operowania
Zarządz. kurs ami
Redundantna informacja Możliwoś ć Błąd aplikacji w bazie oraz – zduplikowania w module 2 być może – na kurs u addBus ekranie informacyjnym
Informacja Wyś wietl. niepełna lub informacji błędna
Zmniejs zenie Błąd aplikacji użytecznoś ci dla pas ażera
5
4
3
3
1
Ekran
Brak komunikacji z ekranem
Błąd s ieci
Irytacja pas ażerów
3
2
1
Ekran
Awaria ekranu
Mechaniczne Irytacja us zkodzenie pas ażerów
4
2
1
19.9.10. QFD (Quality Function Deployment) Quality Function Deployment po polsku tłumaczy się jako „wdrożenie funkcji jakości”, „rozwinięcie funkcji jakości” czy „dopasowanie funkcji jakości”. Jest to metodologia oparta na pracy interdyscyplinarnego zespołu służąca do wdrożenia „głosu klienta” (ang. voice of the customer) do produktu. Innymi słowy, chodzi o przełożenie wymagań, potrzeb i oczekiwań klientów na charakterystyki tworzonego systemu lub oprogramowania. Technika ta została po raz pierwszy
zastosowana w 1972 roku w Japonii, w koncernie Mitsubishi. W ciągu kilku lat zdobyła popularność również w Stanach Zjednoczonych, gdzie z powodzeniem została wdrożona w takich firmach jak Ford, General Motors, Hewlett-Packard, AT&T czy ITT. Metoda nie była oryginalnie zaprojektowana pod kątem rozwiązań informatycznych, ale można ją zaadaptować do procesu wytwarzania oprogramowania. Jako system zarządzania jakością, QFD charakteryzuje się następującymi cechami [183]: wykorzystuje elementy myślenia systemowego (tzn. traktuje proces wytwarzania jako system) oraz psychologię (zrozumienie potrzeb klienta, jego pojęcia „wartości” czy „jakości” produktu); jest oparta na wiedzy i poznaniu (skąd wiemy, jakie są potrzeby użytkownika? w jaki sposób decydować o tym, które cechy systemu zaimplementować?); uwzględnia aspekt konkurencyjności na poziomie strategicznym (umożliwia uzyskanie przewagi konkurencyjnej przez poznanie świadomych i nieświadomych potrzeb klienta, ich właściwego przetłumaczenia na język techniczny i priorytetyzacji oraz optymalizacji najistotniejszych cech systemu); jest całościowym systemem ukierunkowanym na zadowolenie klienta w ciągu całego cyklu produkcji systemu. Jako metoda zapewniająca właściwe wdrożenie potrzeb jakościowych klienta do produktu, QFD jest techniką opartą na ryzyku w tym sensie, że zwiększenie zadowolenia klienta jest równoważne z redukcją ryzyka związanego z jakością użytkową oraz jakością produktu (patrz rozdz. 15 i 16). QFD dowiodła swej skuteczności zarówno w osiąganiu celów krótkookresowych, jak i strategicznych. Jako technika pracy zespołowej przyczynia się do redukcji barier między różnymi zespołami w organizacji (np. analitycy vs. deweloperzy vs. testerzy vs. marketingowcy). Jeśli chodzi o korzyści długookresowe, QFD przez dostarczenie precyzyjnej informacji o potrzebach klienta i o tym, jak należy je przełożyć na „język techniczny” przyczynia się do skrócenia cyklów wytwórczych, obniżenia kosztów wytwarzania oprogramowania oraz zwiększenia produktywności [184]. Metoda QFD jest znana głównie z jednego ze stosowanych w niej narzędzi, mianowicie tzw. domu jakości (ang. House Of Quality, HOQ). Jest to macierz
reprezentująca zależności między wymaganiami klienta a charakterystykami technicznymi produktu, nazwana tak ze względu na swoją postać, kształtem przypominającą dom. Nie należy jednak utożsamiać QFD z domem jakości. QFD składa się z czterech zasadniczych faz: 1.
Dokumentowanie
wymagań
klienta,
możliwości
zwiększenia
konkurencyjności, miar produktu, miar produktów konkurencji oraz technicznych możliwości organizacji dla spełnienia potrzeb klienta. Otrzymanie dobrej jakości danych od klienta jest kluczowe dla powodzenia całego procesu QFD. Krok ten zwykle jest przeprowadzany przez dział marketingu. 2. Faza przeprowadzana przez dział techniczny. Tworzone są wstępne koncepcje produktu oraz częściowe specyfikacje. Części systemu uznane za najważniejsze z punktu widzenia klienta są przekazywane do kolejnej fazy – procesu planowania. 3. Proces planowania – w przypadku tworzenia oprogramowania krok ten przeprowadzany jest przez kierowników zespołów przy udziale pracowników działu jakości. Definiowane są procesy oraz ich parametry. 4. Faza planowania produkcji – określane są w niej wskaźniki służące do pomiaru procesu produkcji i harmonogramów. W tej fazie wykonuje się również czynności związane z analizą ryzyka – ocenia się procesy pod kątem tego, jak duże niosą ze sobą ryzyko i wdraża się proces monitorowania i nadzoru w celu łagodzenia tych ryzyk. Krok ten jest prowadzony wspólnie przez dział jakości oraz zespół wytwórczy.
Rysunek 19.19. Macierz QFD (tzw. dom jakości) Głównym elementem analitycznym QFD jest wspomniana już macierz jakości, przedstawiona na rysunku 19.19. Składa się na nią 9 elementów: I) wymagania, potrzeby i oczekiwania klienta; II) istotność każdego z wymagań wraz z oceną porównawczą konkurencji; III) charakterystyki techniczne tworzonego produktu;
IV) powiązania między potrzebami klienta a charakterystykami technicznymi; V) względna ocena charakterystyk technicznych; VI) korelacje charakterystyk technicznych, pokazujące zgodność lub antagonizm między charakterystykami; VII) pożądane atrybuty charakterystyk technicznych; VIII) techniczna ocena porównawcza z produktami konkurencji; IX) dodatkowe wymagania nałożone na produkt, związane z regulacjami prawnymi, kwestiami bezpieczeństwa itp.
19.9.11. Przykład: zastosowanie QFD do systemu ELROJ Przedstawimy teraz uproszczoną analizę QFD oraz pokażemy na tym przykładzie jej zalety. Załóżmy, że mamy dwóch klientów systemu ELROJ: bezpośredniego (operator systemu) oraz pośredniego (pasażer). Z klientem bezpośrednim pracownik działu marketingu wraz z analitykiem biznesowym przeprowadzili wywiad, w wyniku którego okazało się, że dla operatora dwie najważniejsze cechy systemu to łatwość obsługi oraz kontrola błędów (np. próba usunięcia nieistniejącego kursu lub dodania już istniejącej linii). Klient pośredni nie jest konkretną osobą, a co więcej klientów takich jest bardzo wielu, dlatego dział marketingu zdecydował się na przeprowadzenie ankiety wśród losowo wybranych użytkowników komunikacji miejskiej. Z ankiet wynika, że pasażerowie chcą przede wszystkim mieć szybką i aktualną informację na ekranach stojących na przystankach autobusowych. Wyniki ankiety zostały przekazane analitykom biznesowym, działowi jakości oraz deweloperom. Uznano za logiczne, że pasażerowie są ważniejsi od operatora systemu, dlatego potrzeby pasażerów mają wyższy priorytet niż potrzeby operatora (aktualna informacja = 4, szybkość = 3, łatwość obsługi = 2, kontrola błędów = 1). Następnie analitycy biznesowi zdefiniowali pięć charakterystyk technicznych systemu ELROJ: ergonomiczny interfejs, obsługę wyjątków, wydajny kod i bazę danych, szybkie łącze internetowe oraz wysokiej jakości interfejs programowo-sprzętowy do komunikacji między programem a ekranem. Stwierdzono, że ta ostatnia cecha koreluje słabo pozytywnie z szybkością łącza oraz z obsługą wyjątków, a cecha „wydajny kod i baza” koreluje negatywnie z obsługą wyjątków (im więcej kontroli, tym niższa efektywność) oraz pozytywnie z ergonomicznością interfejsu. Korelacje oznaczono, używając skali: –6, –3, –1, 1, 3, 6, gdzie –6 oznacza silnie negatywną, a 6 – silnie pozytywną korelację.
Wartości te oraz zależności przedstawiony na rysunku 19.20.
zostały
naniesione
na
„dom
jakości”
Ocenę poszczególnych wymagań klienta zdefiniowano jako sumę wartości znajdujących się w odpowiednim wierszu macierzy relacji wymaganiacharakterystyki, ważoną priorytetem wymagania. Na przykład ocena łatwości obsługi błędów wynosi 2 ⋅ (6 + 3 + 1) = 20. Znaczenie charakterystyk zdefiniowano jako sumę wartości z odpowiedniej kolumny tej samej macierzy ważonych przez priorytety odpowiadających im wymagań. Na przykład znaczenie charakterystyki „obsługa wyjątków” wynosi 3 ⋅ 2 + 6 ⋅ 1 + 3 ⋅ 4 = 6 + 6 + 12 = 24. Na podstawie tych wartości zdefiniowano priorytety charakterystyk, szeregując je według
Rysunek 19.20. Macierz QFD dla systemu ELROJ malejącej oceny ich znaczenia. Na końcu dokonano oceny podobnych do systemu ELROJ produktów dwóch konkurencyjnych firm, analizując cechy tych produktów pod kątem wymagań użytkowników. Macierz QFD zawiera bardzo wiele istotnych informacji. Oto niektóre z nich:
Priorytetyzacja wymagań klienta – najważniejszym wymaganiem jest szybkość transmisji danych; zauważmy, że chociaż aktualność informacji była najważniejszym wymaganiem, ostatecznie znalazła się na trzecim miejscu, za łatwością obsługi GUI. Wynika to stąd, że cechy techniczne oprogramowania mają niewielki wpływ na spełnienie tego wymagania. Priorytetyzacja
cech
systemu
–
po
analizie
zależności
między
wymaganiami a charakterystykami technicznymi najważniejszą cechą systemu okazała się obsługa wyjątków i na nią trzeba będzie położyć duży nacisk podczas prac projektowych. Wykorzystanie informacji o korelacji – obsługa wyjątków jest silnie negatywnie zależna od wydajności kodu i bazy, która to cecha ma również wysoki priorytet. Oznacza to potencjalny konflikt między jakością tych dwóch wymagań. Porównując względne oceny tych dwóch charakterystyk, możemy ustalić poziom rozwiązania kompromisowego (ang. trade-off), kładąc nacisk na jakość poszczególnych charakterystyk proporcjonalnie do ich ocen (24 i 14). Możliwość wykorzystania benchmarkingu – produkt konkurenta A lepiej się sprawdza w dostarczaniu aktualnej informacji, a produkt B w szybkim przekazie informacji. Możemy przeanalizować odpowiednie cechy tych produktów, aby porównać je z naszym, planowanym produktem i wdrożyć w organizacji dobre/najlepsze praktyki lub rozwiązania stosowane przez te firmy w ich produktach. najlepsze praktyki, dobre praktyki (ang. best practices, good practices) – zalecane metody lub nowatorskie praktyki, które przyczyniają się do lepszych wyników organizacji w konkretnym kontekście, zwykle uznawane za „najlepsze” przez inne podobne organizacje
19.9.12. FTA (Fault Tree Analysis) Analiza drzewa awarii (ang. Fault Tree Analysis, FTA) to formalna, systematyczna metoda służąca do określania przyczyn źródłowych (ang. root cause) awarii oraz określania prawdopodobieństwa wystąpienia niepożądanych zdarzeń. Technika ta sprawdza się bardzo dobrze w przypadku dużych, skomplikowanych
i złożonych systemów oraz systemów o znaczeniu krytycznym – pomaga w zrozumieniu i zapobieganiu potencjalnych problemów. Drzewo awarii to model reprezentujący w sposób graficzny zależności między kombinacjami
zdarzeń (zarówno
awaryjnych, jak
i
„normalnych”)
przy
wykorzystaniu aparatu logiki formalnej (a ściślej rzecz ujmując, bramek logicznych). Analiza drzewa awarii jest – w przeciwieństwie do metod typu bottom-up takich jak np. FMEA – metodą dedukcyjną. Wychodzi ona od głównego, podstawowego problemu, a następnie, za pomocą metody „top-down” uszczegóławia model, odnajdując przyczyny aktualnie zidentyfikowanych zdarzeń. Metoda schodzi więc w głąb, tworząc coraz głębsze poziomy drzewa awarii, docierając w końcu na poziom najniższy, reprezentujący przyczyny źródłowe. analiza drzewa usterek, analiza drzewa usterek oprogramowania (ang. Fault Tree Analysis, FTA, Software Fault Tree Analysis) – metoda używana do analizy przyczyn usterek (defektów) modelująca wizualnie, w jaki sposób związki logiczne między awariami, błędami człowieka i zewnętrznymi zdarzeniami mogą powodować powstawanie specyficznych defektów FTA jest metodą ilościową (choć można też wykorzystywać ją w sposób jakościowy). Określając prawdopodobieństwa zajścia poszczególnych zdarzeń i tworząc całe drzewo awarii, możemy określić: przyczyny źródłowe awarii; prawdopodobieństwo
wystąpienia
głównej
awarii
(zdarzenia
reprezentowanego wierzchołkiem drzewa); prawdopodobieństwo i znaczenie grup zdarzeń (tzw. zbiorów przekrojowych, ang. cut sets), których zajście powoduje wystąpienie głównej awarii; znaczenie ryzyka poszczególnych komponentów systemu; tzw. ścieżki awarii wysokiego ryzyka (ang. high risk fault paths) oraz mechanizmy ich działania. Wyniki FTA można reprezentować w czytelny, graficzny sposób i wykorzystywać je w komunikowaniu oraz wspieraniu decyzji dotyczących łagodzenia ryzyk. FTA może być użyta w dowolnym etapie cyklu życia, jednak
zaleca się, aby przeprowadzać ją odpowiednio wcześnie, najlepiej na wczesnym etapie projektowania. Notacja FTA jest bogata, ale na potrzeby prezentacji w książce będziemy wykorzystywać uproszczoną symbolikę. Szczegółowe informacje o metodzie, wraz z wykorzystaniem wszystkich jej możliwości Czytelnik znajdzie np. w [182]. Drzewo awarii składa się z wierzchołków oraz krawędzi. Wierzchołki stanowią zdarzenia, a z każdym z nich (oprócz zdarzeń w liściach drzewa) jest związana bramka logiczna połączona ze zdarzeniami na niższym poziomie. Zwykle w FTA wykorzystuje się tylko dwa typy bramek logicznych: AND i OR. Na rysunku 19.21 są przedstawione graficzne oznaczenia trzech bramek logicznych. Oprócz bramek AND i OR jest jeszcze pokazana tzw. bramka głosująca (ang. voting gate) typu m/n.
Rysunek 19.21. Notacja drzew awarii
Bramka AND wyśle na wyjściu sygnał tylko wtedy, gdy otrzyma na wejście sygnały z wszystkich zdarzeń do niej wchodzących. Bramka OR wyśle na wyjściu sygnał wtedy, gdy otrzyma sygnał wejściowy od co najmniej jednego zdarzenia wejściowego. Bramka głosująca m/n ma n wejść i wysyła sygnał wyjściowy tylko wtedy, gdy otrzyma co najmniej m spośród wszystkich n możliwych sygnałów wejściowych. W środkowej części rysunku 19.21 są pokazane dwa proste drzewa awarii. W lewym drzewie zdarzenie A połączone jest ze zdarzeniami B i C bramką AND. Zdarzenia A zajdzie tylko wtedy, gdy jednocześnie nastąpią zdarzenia B i C. Jeśli prawdopodobieństwa zajścia zdarzeń B i C wynoszą odpowiednio PB oraz PC, to prawdopodobieństwo wystąpienia zdarzenia A wynosi PA = PB ⋅ PC, przy czym zakłada się, że wystąpienia zdarzeń B i C są od siebie niezależne. W drzewie po prawej stronie mamy zdarzenie D połączone bramką OR ze zdarzeniami E i F. Jeśli prawdopodobieństwa zajścia zdarzeń E i F wznoszą odpowiednio PE oraz PF , to
prawdopodobieństwo zdarzenia D, z reguły włączeń i wyłączeń (patrz Dodatek C), wyniesie PD = PE + PF – PE ⋅ PF . Tak jak poprzednio, zakładamy, że zdarzenia E i F są od siebie niezależne. Gdyby zdarzenie D było połączone bramką OR z trzema zdarzeniami: E, F i G, to wtedy PD=PE+ PF + PG – PE ⋅ PF – PE ⋅ PG – PF ⋅ PG + PE ⋅ PF ⋅ PG . Ogólny wzór, dla dowolnej liczby składników, jest podany w Dodatku C.
Tworzenie drzewa awarii jest procesem iteracyjnym. Rozpoczyna się stworzeniem korzenia drzewa, reprezentującego główną awarię, a następnie rozbudowuje drzewo o kolejne poziomy. Przy tworzeniu każdej bramki logicznej obowiązuje ta sama logika i jest wykorzystywany ten sam zbiór pytań. Dla każdego zdarzenia X należy zidentyfikować zbiór zdarzeń, których zajście może wywołać X oraz zdefiniować sposób zajścia tego zdarzenia (czyli połączyć zdarzenia będące dziećmi X odpowiednią bramką logiczną). W każdym momencie tworzenia drzewa, przy identyfikacji zdarzeń odpowiadających zdarzeniu X wykorzystuje się trzy zasady: zasadę I-N-S – czyli udzielenie odpowiedzi na pytanie: co jest natychmiastowe (ang. immediate), konieczne (ang. necessary) i wystarczające (ang. sufficient) do zajścia zdarzenia X? zasadę SS-SC – czyli rozróżnienie, czy zdarzenie jest stanem systemu (ang. State-of-the-System), czy też stanem komponentu (ang. State-of-theComponent). W przypadku SC odpowiadającą bramką będzie OR.
W
przypadku
SS
zdarzenie
będzie
podlegać
dalszej
analizie
z wykorzystaniem zasady I-N-S, aby określić wejścia oraz typ bramki. zasadę P-S-C – czyli udzielenie odpowiedzi na pytanie: jakie są podstawowe (ang. primary), zewnętrzne (ang. secondary) oraz tzw. polecone (ang. command) powody zdarzenia? Pytanie to zmusza analityka do skupienia się na określonych czynnikach sprawczych. Każdy komponent może ulec awarii tylko na te trzy sposoby: podstawowy tryb awarii, zewnętrzny tryb awarii lub przez tzw. ścieżkę polecenia (ang. command path failure). Jeśli wystąpią co najmniej dwa spośród trzech typów P, S, C, to automatycznie stosuje się bramkę OR. Oznaczenia podstawowego oraz zewnętrznego trybu awarii pokazane są w dolnej części rysunku 19.21. Podstawowa awaria to wewnętrzna, inherentna awaria danego komponentu lub modułu (np. wykonanie błędnej instrukcji). Awarie podstawowe są od siebie niezależne, ze względu na swą inherentność. Zewnętrzna awaria jest spowodowana zewnętrznymi czynnikami (np. atak hakerski na oprogramowanie). Te awarie są od siebie zależne, bo np. atak hakerski może spowodować awarię więcej niż jednego komponentu. Awarie polecone są spowodowane przez zamierzone działania, które odbyły się w niewłaściwym czasie, np. za wcześnie lub za późno. Jeśli np. proces okresowego
tworzenia
kopii
zapasowej
uruchomi
się
przed
wylogowaniem użytkownika zamiast po, to użytkownik może po stworzeniu kopii wykonać jeszcze jakieś działania w systemie, które zostaną utracone przez wylogowanie go. Drzewa awarii pozwalają na określenie specyficznych zbiorów zdarzeń, zwanych zbiorami przecinającymi, pomocnych w analizie ryzyka. Zbiór przecinający to zbiór zdarzeń, których jednoczesne zajście powoduje wystąpienie głównej awarii. Z kolei minimalny zbiór przecinający to najmniejszy możliwy (w sensie liczby elementów) zbiór przecinający. Obliczanie minimalnych zbiorów przecinających wymaga znajomości algebry Boola i zagadnień związanych z minimalizacją wyrażeń logicznych. Więcej informacji na ten temat Czytelnik znajdzie w Dodatku C. Rozważmy drzewo awarii z rysunku 19.22. Modeluje ono zajście awarii głównej B1.
Rysunek 19.22. Przykładowe drzewo awarii Chcemy obliczyć zbiory przecinające i minimalne zbiory przecinające dla tego drzewa. Awaria B1 zajdzie przy jednoczesnym zajściu B2 i B3. Z kolei B2 zajdzie przy zajściu przynajmniej jednego spośród zdarzeń: A i B4. Analizując całe drzewo w ten sposób, możemy obliczyć algebraiczny warunek na zajście B1: B1 = B2 ⋅ B3 = (A + B4) ⋅ (C + B5) = (A + B + C) ⋅ (C + A⋅B) = AC + AAB + BC + BAB + CC + CAB = AC + AB + BC + AB + C + ABC
= AC + AB + BC + C + ABC Każdy ze zbiorów {A, C}, {A, B}, {B, C}, {C}, {A, B, C} jest zbiorem przecinającym. Na przykład wystąpienie samego zdarzenia C spowoduje – niezależnie od zajścia A oraz B – zajście B4 i B3. Zajście B4 spowoduje zajście B2, a jednoczesne zajście B2 i B3 powoduje zajście awarii głównej B1. Aby obliczyć minimalny zbiór
przecinający, musimy
zminimalizować
obliczone wyrażenie: AC + AB + BC + C + ABC = AB(1 + C) + C(A + B + 1) = AB + C Minimalnymi zbiorami przecinającymi są więc zbiory {A, B} oraz {C} Jeśli zdarzeniom przypiszemy prawdopodobieństwa (współczynniki niezawodności), z jakimi zachodzą, to możemy obliczyć prawdopodobieństwo awarii głównej, używając podobnego aparatu, jak przy wyliczaniu zbiorów przecinających, tyle, że tym razem będziemy działać na prawdopodobieństwach. Załóżmy, że niezawodności komponentów A, B i C wynoszą odpowiednio 0,9, 0,8 i 0,6, zatem prawdopodobieństwa ich awarii wynoszą odpowiednio 0,1, 0,2 i 0,4. Wykorzystując wzory na obliczanie prawdopodobieństw zdarzeń dla bramek AND i OR, mamy PB1 = PB2 ⋅ PB3 = (PA + PB4 – PA PB4)(PC + PB5 – PC PB5) = (PA + (PB + PC – PB PC) – PA (PB + PC – PB PC)) ⋅ (PC + (PA + PB – PA PB) – PC (PA + PB – PA PB)) = (0,1 + (0,2 + 0,4 – 0,08) – 0,1(0,2 + 0,4 – 0,08)) ⋅ (0,4 + (0,1 + 0,2 – 0,02) – 0,4(0,1 + 0,2 – 0,02)) = (0,62 – 0,052)(0,68 – 0,112) = 0,568 ⋅ 0,568 ≈ 0,322 W przypadku dużych drzew awarii, w których bramki mają wiele wejść, dokładne obliczenie prawdopodobieństwa staje się bardzo trudne lub wręcz niemożliwe. W takich przypadkach stosuje się podejście Monte Carlo, polegające na wielokrotnym losowaniu awarii poszczególnych komponentów, zgodnie z zadanymi prawdopodobieństwami, badając, czy w danym przypadku główna awaria nastąpiła, czy nie. Jeśli wśród N takich prób M zakończyło się
wystąpieniem awarii głównej, to szacuje się prawdopodobieństwo zajścia awarii głównej jako P = M/N.
19.9.13. Przykład: zastosowanie FTA do systemu ELROJ Załóżmy, że modelujemy awarię główną „niepoprawne działanie ekranu” w systemie ELROJ opisanym w Dodatku A. Zidentyfikowano następujące 4 bezpośrednie, wystarczające przyczyny niepoprawnego działania ekranu: awaria mechaniczna ekranu, brak zasilania, błąd transferu danych, błąd programu. Są to przyczyny wystarczające, dlatego łączymy je bramką OR. Brak zasilania oraz błąd transferu są awariami podstawowymi, w związku z czym stają się liśćmi drzewa awarii. Aby nastąpiła awaria mechaniczna, konieczny jest skok napięcia oraz awaria bezpiecznika. Konieczność tych warunków skutkuje połączeniem ich bramką AND, a fakt, że jedna jest awarią podstawową a druga zewnętrzną sprawia, że stają się one liśćmi drzewa. Błąd programu może być spowodowany błędnym działaniem funkcji getDisplayString lub błędem interfejsu program-ekran. Obie awarie są podstawowe, zatem stają się liśćmi drzewa. Ostateczna postać drzewa jest przedstawiona na rysunku 19.23.
Rysunek 19.23. Drzewo awarii dla systemu ELROJ Z danych historycznych wiemy, że poszczególne awarie występują w określonym przedziale czasu z następującymi prawdopodobieństwami: brak zasilania: 0,001;
błąd transferu danych: 0,005; skok napięcia: 0,02; awaria bezpiecznika: 0,003; błędne działanie funkcji getDisplayInfo4: 0,08; błąd interfejsu program-ekran: 0,05. Przyjmując
za
P,
Q,
R,
S odpowiednio
prawdopodobieństwa: awarii
mechanicznej ekranu, braku zasilania, błędu transferu danych oraz błędu programu, możemy obliczyć prawdopodobieństwo awarii głównej, korzystając ze wzoru włączeń-wyłączeń jako: Pr = P + Q + R + S – PQ – PR – PS – QR – QS – RS + PQR + PQS + PRS + QRS – PQRS. Łatwo policzyć, że P = 0,00006, S = 0,126, więc otrzymujemy Pr ≈ 0,131. Zamiast dokładnych rachunków moglibyśmy przeprowadzić estymację tego prawdopodobieństwa
metodą
Monte Carlo. Uruchamiając prosty
skrypt5
w języku R Statistical Package, przedstawiony na listingu 19.1, przeprowadziliśmy sto tysięcy prób Monte Carlo i otrzymaliśmy wynik 0,1332, który – jak widać – jest bliski wynikowi prawdziwemu.
liczbaAwarii = 0 N = 100000 for (i in 1:N) { if ((runif(1) Zarządzanie -> Raporty). Raporty s ą dos tępne tylko dla kadry zarządzającej. 21. Personel. Zes pół tes towy w projekcie ELROJ będzie s ię s kładać z: kierownika tes tów (podleg a Kierownikowi Projektu); analityka tes tów (podleg a kierownikowi tes tów; zadania: analiza wymag ań, uczes tniczenie w ins pekcji dokumentów i kodu, projektowanie tes tów, ws półpraca z kierownikiem tes tów w zakres ie raportowania i monitorowania pos tępów); 2 tes terów (podleg ają kierownikowi tes tów; zadania: automatyzacja wykonania tes tów przez tworzenie s kryptów tes towych, tworzenie uprzęży tes towych, przeprowadzanie tes tów niefunkcjonalnych, uczes tnictwo w ins pekcji dokumentów i kodu, ś cis ła ws półpraca z analitykiem tes tów); inżyniera ds . zarządzania konfig uracją (zas ób dzielony – 20% czas u pracy – formalnie przypis any do Działu Obs ług i Technicznej Projektów). S kład os obowy zes połu pozwoli na bezproblemowe przeprowadzenie proces u tes toweg o. Ws zys tkie zas oby s ą dos tępne i odpowiednio przes zkolone. 22. Harmonogram (wykres Gantta)
21.2.3. Jednopoziomowy plan testów (level test plan) Jednopoziomowy plan testów obejmuje swoim zakresem w zasadzie te same czynności, co główny plan testów, przy czym dotyczy konkretnego poziomu testowania, np. fazy testów integracyjnych. Jeśli zachodzi konieczność tworzenia jednopoziomowego planu testów, to zwykle zawiera on jedynie uszczegółowienia czy rozszerzenia głównego planu testów o zagadnienia związane z pojedynczym poziomem testowania. plan testów jednego poziomu, jednopoziomowy plan testów (ang. level test plan) – plan testu odnoszący się do jednego poziomu testowania plan testów dla fazy (ang. phase test plan) – plan testów odnoszący się do jednej fazy testowania Jednopoziomowy plan testów może bardziej szczegółowo opisywać np. podejście do testowania, np. techniki projektowania testów. Zazwyczaj jednak
dokument ten jest bardzo rzadko spotykany. W większości przypadków jedynym dokumentem dotyczącym zarządzania testowaniem jest główny plan testów. Norma ISO 29119-3 nie wyróżnia osobnego planu dla poziomu testów, z kolei norma IEEE 829 to czyni, określając następującą strukturę tego dokumentu: 1. Informacja o dokumencie a) unikatowy identyfikator; b) zakres (jaki obszar obejmuje swym zasięgiem niniejszy plan, co jest w nim uwzględnione, a co zostało wyłączone); c) odnośniki (określają listę dokumentów związanych z niniejszym planem, do których mogą następować odwołania w dalszych częściach dokumentu); d) umiejscowienie poziomu testów w całym procesie testowym; e) klasy testów i ogólne warunki testowe (opisują unikalną naturę danego poziomu testów; przykładami klasy testów mogą być: analiza wartości brzegowych, testowanie niepoprawnych wartości).
eksploracyjne,
wykorzystywanie
2. Szczegółowy plan dla poziomu testów a) elementy testowe i ich identyfikatory (opisują, co będzie podlegało testowaniu); b) macierz identyfikowalności testów; c) cechy, które mają być przetestowane; d) cechy, które mają nie być testowane; e) podejście (np.: techniki czarnoskrzynkowe, białoskrzynkowe, analiza, symulacja, inspekcja); f) kryteria zaliczenia testów (ang. pass/fail criteria); g) kryteria zawieszenia i wznowienia wykonywania testów; h) produkty fazy testowej (ang. test deliverables), np. właściwe dla danej fazy: plany, projekty, przypadki i procedury testowe, logi, raporty o incydentach, raporty o stanie testów. 3. Zarządzanie testowaniem (określenie infrastruktury, odpowiedzialności, zasobów, szkoleń, harmonogramu i ryzyk, jeśli nie zostało to uczynione w dokumencie wyższego poziomu, np. w planie testów)
a) planowane czynności i zadania; b) środowisko/infrastruktura (sprzęt, wykorzystywane oprogramowanie, środowisko testowe, narzędzia wspierające, bazy danych itp.); c) odpowiedzialność i władza (ang. authority) (określenie osób lub grup odpowiedzialnych za zarządzanie, projektowanie, przygotowanie, wykonanie, raportowanie oraz weryfikację wyników testów na danym poziomie, a także za usuwanie defektów i dostarczanie elementów testowych); d) powiązania między zaangażowanymi podmiotami (opisuje środki oraz rodzaj komunikacji między zaangażowanymi podmiotami; powiązania mogą być przedstawione w formie graficznej, jako diagram przepływu informacji); e) zasoby i ich alokacja; f) szkolenia; g) harmonogramy, szacunki, koszty; h) ryzyka i sposoby ich łagodzenia. 4. Ogólne a) procedury zapewniania jakości
(jeśli
nie zostały
zdefiniowane
w dokumencie wyższego poziomu lub jeśli wymagane jest ich uszczegółowienie dla danego poziomu testów); b) metryki; c) pokrycie testowe (opisuje wymagane pokrycie, np. kodu dla testów jednostkowych, wymagań dla testów systemowych bądź innych miar pokrycia testami elementów testowych); d) słownik pojęć; e) historia zmian w dokumencie. Cechy testowane, nietestowane oraz podejście (punkty 2c, 2d i 2e powyższego planu) często występują wspólnie w postaci tzw. macierzy testów (ang. test matrix).
21.2.4. Przykład jednopoziomowego planu testów Qualium IT PLAN T EST ÓW JEDNOST KOW YCH
dla s ys temu ELROJ 0.9 Id: JPT-ELROJ_0.9-1.0 W ersja: 1.0 Data: 2014-08-05 Autor: S ławoj S yty (S tars zy Analityk Tes tów), Tomas z Nowak (Prog ramis ta Java) Zaakceptowane przez: Jan Nowakows ki (Kierownik Tes tów) 1 Zakres planu. Plan dotyczy tes towania jednos tkoweg o na poziomie 13 zdefiniowanych w projekcie s ys temu ELROJ metod. 2 Odnośniki. [DC] – „ S tandardowe tes ty dla funkcji daty i czas u” , wers ja 1.6. [S WN] – S pecyfikacja Wymag ań Niefunkcjonalnych dla s ys temu ELROJ, wers ja 1.0. 3 Umiejscowienie poziomu testów. Tes ty jednos tkowe nas tępują po zdefiniowaniu i zaakceptowaniu projektu nis kieg o poziomu i s ą wykonywane przez deweloperów przy ws parciu zes połu tes toweg o. 4 Elementy testowe i ich identyfikatory. Element tes towy
Identyfikator
setInfo(String info)
ET1_S etInf
getInfo()
ET2_GetInf
removeInfo()
ET3_RemInf
addLine(int line)
ET4_AddLin
removeLine(int line)
ET5_RemLin
addBus(int line, int mm)
ET6_AddBus
getAllBusLines()
ET7_AllBus
getAllBusesTimes(int line)
ET8_AllBTim
getNextBusTime(int line, int mm)
ET9_NexBus
addBusInterval(int line, int firstTimemm, int interval, int count)
ET10_Bus Int
removeBus(int line, int mm)
ET11_RemBus
getDisplayString(int actTimemm, int displayLines)
ET12_Dis S tr
setActualDate(int day, int month, int year)
ET13_S etDat
5 Macierz identyfikowalności testów. Macierz ta umożliwia obus tronne ś ledzenie między wymag aniami a elementami tes towymi. Element tes towy
R01
R02
R03
R04
R05
R06
ET1_S etInf
X
X
ET2_GetInf
X
X
ET3_RemInf
X
X
ET4_AddLin
X
ET5_RemLin
X
ET6_AddBus ET7_AllBus
X X
R07
ET8_AllBTim
X
ET9_NexBus
X
ET10_Bus Int
X
ET11_RemBus
X
ET12_Dis S tr
X
ET13_S etDat
X
6 Cechy, które mają być przetestowane. Tes towaniu podleg a ws zys tkie 13 metod. 7 Cechy, które mają nie być testowane. Tes towaniu nie podleg ają metody s łużące do obs ług i komunikacji s ieciowej, bazodanowe oraz do komunikacji oprog ramowania z ekranem. 8 Macierz testów i podejście testowe Element tes towy
Podejś cie
setInfo(String info)
Tes towanie czarnos krzynkowe Tes towanie wartoś ci błędnych (znaki s pecjalne, wartoś ci eks tremalne)
getInfo() removeInfo() addLine(int line) removeLine(int line)
addBus(int line, int mm) getAllBusLines()
Tes towanie obciążeniowe Tes towanie czarnos krzynkowe Analiza wartoś ci brzeg owych Tes towanie obciążeniowe Analiza wartoś ci brzeg owych
getAllBusesTimes(int line) getNextBusTime(int line, int mm) addBusInterval(int line, int firstTimemm, int interval, int count)
Tes towanie czarnos krzynkowe Tes towanie eks ploracyjne Metoda Categ ory-Partition
Analiza wartoś ci brzeg owych Tes towanie CRUD
removeBus(int line, int mm)
getDisplayString(int actTimemm, int displayLines)
Tes towanie wydajnoś ci Categ ory-Partition lub drzewa klas yfikacji + tes towanie pairwis e Tes towanie CRUD
setActualDate(int day, int month, int year)
S tandardowy zes taw tes tów dla weryfikacji funkcji czas u i daty [DC]
9 Kryteria zaliczenia testów. Tes ty funkcjonalne s ą uznane za zaliczone, jeś li ich wynik jes t zg odny z wynikiem oczekiwanym. Tes ty wydajnoś ci s ą uznane za zaliczone, jeś li wynik mieś ci s ię w dopus zczalnych g ranicach okreś lonych w dokumencie [S WN]. 10 Pokrycie testowe. Dla każdeg o z modułów wymag ane jes t co najmniej 90% pokrycie ins trukcji oraz 80% pokrycie decyzji, przy czym dla modułu getDisplayString pokrycie decyzji ma być co najmniej 90%.
21.2.5. Raport o stanie testów (test status report) Raport o stanie testów dostarcza informacji o aktualnym statusie testowania przeprowadzonego w ustalonym czasie. ISO 29119-3 [4] słusznie podkreśla, że
raport ten nie zawsze musi mieć formę pisemną. Na przykład w projektach prowadzonych według metodologii zwinnych stan testów może być omawiany na spotkaniach przed lub po przeprowadzonej iteracji. Podczas takich dyskusji można wspomagać się informacjami zawartymi na tablicach aktywności lub tzw. wykresie spalania. raport o stanie testów, raport o postępie testów (ang. test status report, test progress report) – dokument zawierający podsumowanie aktywności testowych i osiągniętych wyników, tworzony regularnie, aby raportować postęp prac testowych w stosunku do założeń i przedstawiający ryzyka oraz alternatywy wymagające podjęcia decyzji zarządczych Oto przykładowa struktura raportu o stanie testów: a) okres raportowania (za jaki okres są prezentowane dane); b) postęp odniesiony do planu testów (wszelkie poważniejsze odchylenia powinny być opisane, wraz z powodami tych odchyleń, akcjami naprawczymi oraz efektami tych akcji); c) czynniki blokujące postęp; d) miary; e) nowe i zmienione ryzyka; f) zaplanowane testowanie (opis planowanych czynności testowych na następny okres raportowania).
21.2.6. Przykład raportu o stanie testów Qualium IT Raport za okres: od 2014-08-01 do 2014-08-31 RAPORT O ST ANIE T EST ÓW Osoba odpowiedzialna: Rados ław S ikorka dla s ys temu ELROJ 0.9 # tes tów Moduł
pokrycie
zaproj. wykon. zdanych niezd. ws trz. ins tr. dec.
ET10_Bus Int
0
0
0
0
0
–
–
ET11_RemBus 4
3
3
0
0
78%
66%
ET12_Dis S tr
26
21
11
10
0
62%
58%
ET13_S etDat
5
5
1
4
0
100% 90%
RAZEM
35
29
15
14
0
77%
68%
21.2.7. Raport końcowy z testowania (test completion report) Raport końcowy z testowania podsumowuje wszystkie zadania wykonane w ramach procesu testowego. Może obejmować cały projekt lub dotyczyć wybranego podprocesu testowego.
raport końcowy z testowania, raport z testów, sumaryczny raport z testów, raport oceny testów (ang. test completion report, test report, test summary report, test evaluation report) – sumaryczny dokument przedstawiający działania testowe i ich rezultaty; zawiera także ocenę testowanych elementów pod względem zgodności z kryteriami wyjścia [5] Oto przykładowa struktura raportu z zakończonego testowania: a) podsumowanie (opisuje, co było przetestowane, a także ograniczenia, w ramach których proces testowy był prowadzony); b) odchylenia od planu (w szczególności może podawać również informację o ryzyku rezydualnym); c) ocena komplentości testów (w jakim stopniu testowanie spełniło założone kryteria, np. w terminach pokrycia strukturalnego, wymagań bądź ryzyk); d) czynniki opóźniające postęp (wraz ze sposobami radzenia sobie z nimi); e) miary; f) ryzyko rezydualne (ryzyka pozostałe w programie, niezłagodzone, a także ryzyka odkryte po ukończeniu testowania); g) produkty procesu testowego (plan testów, przypadki testowe itp.); h) reużywalne produkty procesu testowego (np. skrypty lub dane testowe); i) wyciągnięte wnioski (ang. lessons learned) (opis wniosków będących efektem spotkania poprojektowego).
21.2.8. Przykład raportu końcowego z testowania
S zacunkowy poziom ryzyka rezydualneg o: wpływ/prawdopodobieńs two b. małe małe ś rednie duże b. duże b. mały
3
mały
1
ś redni duży b. duży
1
1 1
S umaryczne pokrycie/cel: ins trukcji: 94%/90% decyzji: 91%/90% 3 Szacowana liczba błędów polowych: 7 (model Rayleig ha), 10 (analiza mutacyjna) 4 W nioski na przyszłość. S zkolenie z zakres u technik projektowania tes tów s kutkowało wyraźnym zwięks zeniem jakoś ci tes tów. Warto org anizować je jako obowiązkowe dla nowo przyjmowanych tes terów.
Projekt s tworzenia włas neg o narzędzia do obs ług i incydentów nie zos tał ukończony i s tracono z teg o powodu 3 os obomies iące czas u. Powodem były rozbieżne oczekiwania interes arius zy co do funkcjonalnoś ci produktu i brak motywacji deweloperów do teg o odg órnie narzuconeg o projektu. S ug es tia: używać opens ource’owych narzędzi. Mają niżs zą funkcjonalnoś ć niż ta żądana przez kierownictwo IT, ale za to s ą darmowe i można je s zybko wdrożyć.
21.3. Dokumenty dynamicznych procesów testowych 21.3.1. Specyfikacja testów (test design specification) Specyfikacja testów wskazuje cechy, które mają podlegać testowaniu oraz warunki testowe wyprowadzone z podstawy testów. Jest to więc dokument fazy projektowania testów. specyfikacja testów (ang. test specification) – dokument zawierający specyfikację projektu testów, specyfikację przypadków testowych i/lub specyfikację procedury testowej
specyfikacja projektu testów (ang. test design specification) – dokument specyfikujący warunki testowe (elementy pokrycia) dla elementu testowego, szczegółowe podejście do testów oraz identyfikujący powiązane przypadki testowe wysokiego poziomu [5] Przykładowa struktura specyfikacji testów wygląda następująco: 1. Zbiory cech (ang. feature sets) a) ogólny opis (jakie zbiory cech występują w systemie); b) unikatowy identyfikator dla każdego zbioru cech; c) cel (co chcemy osiągnąć, wykorzystując ten zbiór cech); d) priorytet dla testowania (jeśli konieczny); e) szczegółowa strategia testowania dla każdego zbioru cech; f) śledzenie między zbiorami cech a elementami podstawy testów (ta idenyfikowalność może być zawarta w macierzy testów). 2. Warunki testowe a) ogólny opis (jakie warunki testowe będą podlegać testom); b) unikatowy identyfikator dla każdego warunku testowego; c) opis poszczególnych warunków testowych (w języku naturalnym lub za pomocą modelu); d) priorytet dla testowania (jeśli konieczny); e) śledzenie między warunkami testowymi a zbiorami cech lub elementami podstawy testów (ta identyfikowalność może być zawarta w macierzy testów).
21.3.2. Przykład specyfikacji testów Qualium IT Data: 2014-08-01 SPECYFIKACJA T EST ÓW Osoba odpowiedzialna: Julius z Mickiewicz dla s ys temu ELROJ 0.9 Id: S T-ELROJ_0.9-1.0 1 Zbiory cech i warunki testowe.
ELROJ 1. Oprog ramowanie 1.1. Komunikat (WT: setInfo, getInfo, removeInfo) 1.2. Data (WT: setActualDate) 1.3. Tablica kurs ów (WT: getDisplayString, getNextBusTime) 2. GUI oraz interfejs GUI-oprog ramowanie 2.1. Opcja „ zarządzaj liniami” 2.1.1. funkcja dodawania linii (WT: formularz dodawania linii, addLine) 2.1.2. okno wyś wietlania linii (WT: okno wyś wietlania linii, getAllBusLines) 2.1.3. funkcja us uwania linii (WT: formatka us uwania linii, removeLine) 2.1.4. funkcja zmiany numeru linii (WT: formularz zmiany numeru linii) 2.2. Opcja „ zarządzaj kurs ami” 2.2.1. funkcja dodawania kurs u (WT: formularz dodawania kurs u, addBus) 2.2.2. funkcja dodawania wielu kurs ów (WT: formularz dodawania wielu kurs ów, addBusInterval) 2.2.3. funkcja us uwania kurs u (WT: formatka us uwania kurs u, removeBus) 2.2.4. funkcja wyś wietlania kurs ów (WT: okno wyś wietlania kurs ów, getAllBusesTimes) 2.2.5. funkcja zmiany kurs u (WT: okno zmiany kurs u) 2.3. Opcja „ komunikat”
2.3.1. funkcja us tawiania komunikatu (WT: formularz us tawiania komunikatu) 2.3.2. przycis k us uwania komunikatu (WT: przycis k „ us uń komunikat” ) 2.4. Opcja „ zapis /odczyt” 2.4.1. opcja zapis u rozkładu na dys k (WT: funkcje transform2xml oraz save) 2.4.2. opcja odczytu rozkładu z dys ku (WT: funkcje load oraz transform2timetable) 2.5. Opcja „ komunikacja z ekranem” 2.5.1. funkcja aktualizacji rozkładu jazdy (WT: okno aktualizacji, funkcja updateTimetable) 2.5.2. funkcja wyłączenia wyś wietlania rozkładu (WT: przycis k „ wyłącz rozkład” ) 2.5.3. funkcja wyś wietlania w trybie s pecjalnym (tylko komunikaty) (WT: setInfo w połączeniu z formatką wyś wietlania w trybie s pecjalnym) 3. Ekran 3.1. Interfejs komunikacyjny ekran-oprog ramowanie (WT: API ScreenCom) 3.2. Oprog ramowanie panelu konfig uracyjneg o ekranu (WT: prog ram SConfig, panel s terujący w ekranie (GUI))
21.3.3. Specyfikacja przypadku testowego (test case specification) Specyfikacja przypadku testowego opisuje jeden lub kilka przypadków testowych dla określonych elementów testowych. specyfikacja przypadków testowych (ang. test case specification) – dokument specyfikujący zbiór przypadków testowych (cel, wejścia, czynności testowe,
oczekiwane rezultaty i wstępne warunki wykonania) dla elementu testowego [5] Przykładowa następująco:
struktura
specyfikacji
przypadku
testowego
wygląda
1. Elementy pokrycia testowego a) ogólny opis (jakie elementy pokrycia testowego są definiowane dla jakich warunków testowych; elementy pokrycia testowego są wyprowadzane przez zastosowanie odpowiedniej techniki projektowania testów do warunków testowych; np. technika podziału na klasy równoważności wygeneruje elementy pokrycia w postaci klas równoważności); b) unikatowy identyfikator dla każdego elementu pokrycia testowego; c) opis elementów pokrycia (co ma być pokryte, np. dla metody podziału na klasy równoważności można opisać, czy pokrywane są poprawne, czy niepoprawne klasy równoważności); d) priorytet (jeśli konieczny); e) śledzenie (identyfikuje związek elementu pokrycia z warunkiem testowym lub zbiorem cech; identyfikacja ta może być opisana w macierzy testów). 2. Przypadki testowe a) ogólny opis (definiuje przypadki wyprowadzone z elementów pokrycia; liczba przypadków testowych będzie zależna od przyjętego kryterium pokrycia zdefiniowanego np. w planie testów); b) unikalny identyfikator dla każdego przypadku testowego; c) cel (jaki jest cel danego przypadku testowego? często cel ujęty jest w nazwie przypadku); d) priorytet (jeśli konieczny); e) śledzenie (wiąże przypadek z elementem pokrycia, wymaganiem lub innym elementem podstawy testów); f) warunki wstępne (co musi zachodzić przed wykonaniem testu); g) wejście; h) oczekiwane wyjście; i) rzeczywiste wyniki i rezultat testu (mogą być opisane w specyfikacji procedury testowej, w dokumencie „otrzymane wyniki” lub w dokumencie wyniku testu).
21.3.4. Przykład specyfikacji przypadku testowego Przykładowa specyfikacja przypadku testowego dla systemu ELROJ jest pokazana na rysunku 3.5.
21.3.5. Specyfikacja procedury testowej (test procedure specification) Specyfikacja procedury testowej opisuje kolejność wykonywania przypadków testowych. Może również zawierać informacje dotyczące specjalnych wymagań (np. dla środowiska testowego), które muszą być spełnione przed wykonaniem danej suity testów. Schemat wykonywania procedur testowych jest określany mianem harmonogramu wykonania testu. harmonogram wykonania testu (ang. test execution schedule) – schemat wykonania procedur testowych; procedury testowe są zawarte w harmonogramie wykonywania testów, w ich kontekście i kolejności, w jakiej mają być wykonane Przykładowa organizacja specyfikacji procedury testowej może wyglądać następująco: 1. Zbiory testów a) ogólny opis (jak poszczególne przypadki testowe są budowane w zbiory testów o wspólnym celu testowania); b) unikatowy identyfikator dla każdego zbioru testów; c) cel (jaki jest cel poszczególnych zbiorów testów); d) priorytet (jeśli konieczny); e) zawartość/śledzenie (lista identyfikatorów przypadków testowych wchodzących w skład poszczególnych zbiorów testów). 2. Procedury testowe a) ogólny opis procedur wyprowadzonych ze zbiorów testów; b) unikatowy identyfikator dla każdej procedury testowej; c) cel (jaki jest cel poszczególnych procedur testowych); d) priorytet (jeśli konieczny); e) konfiguracja początkowa (opisuje niezbędne do wykonania czynności przed uruchomieniem danej procedury testowej; zwykle zawiera opis
warunków wstępnych dla pierwszego przypadku testowego); f) uruchamiane przypadki testowe (lista przypadków w kolejności ich uruchomienia); g) związek z innymi procedurami (opisuje zależności między tą a innymi procedurami testowymi, które np. wymuszają wykonanie całych procedur testowych w określonej kolejności); h) konfiguracja końcowa (opisuje niezbędne do wykonania czynności, jakie należy wykonać po zakończeniu procedury testowej, np. rozmontowanie środowiska testowego, wyczyszczenie bazy danych).
21.3.6. Przykład specyfikacji procedury testowej
21.3.7. Wymagania co do danych testowych (test data requirements)
Dokument wymagań co do danych testowych określa warunki, jakie muszą spełniać dane wykorzystywane w procedurach bądź przypadkach testowych. Przykładowa struktura takich wymagań wygląda następująco: a) ogólny opis (opisuje dane wymagane do uruchomienia procedur testowych; może również zawierać wymagania dotyczące jakości tych danych); b) unikatowy identyfikator; c) opis (określa nazwę, możliwy/dopuszczalny zakres wartości, format i strukturę każdej danej; może zawierać wymagania co do anonimowości lub zaciemniania (ang. obfuscation) danych); d) odpowiedzialność (kto odpowiada za udostępnianie danych i kontrolę ich poprawności); e) czas (kiedy i na jak długo dane są potrzebne); f) resetowanie (określa, czy i w jakich okolicznościach dane mają być resetowane); g) archiwizacja/rozporządzanie danymi (opisuje, kiedy i w jaki sposób dane mają być archiwizowane lub przetwarzane po zakończeniu testowania).
21.3.8. Przykład wymagania co do danych testowych
21.3.9. Wymagania co do środowiska testowego (test environment requirements) Wymagania co do środowiska testowego to dokument analogiczny do wymagań dotyczących danych testowych. Jak sama nazwa wskazuje, dotyczy on środowiska testowego, czyli całej infrastruktury niezbędnej do przeprowadzenia testów. Dokument ten może być bardzo istotny w przypadku projektów silnie opartych na sprzęcie, np. związanych z oprogramowaniem telekomunikacyjnym współpracującym ze stacjami przekaźnikowymi. Zwykle jednak nie ma potrzeby generowania wymagań na środowisko jako odrębnego dokumentu. Kwestie związane z elementami środowiska testowego omawiane są najczęściej w planie testów lub w wymaganiach dla poszczególnych przypadków bądź procedur testowych. Można wyróżnić następujące elementy środowiska: sprzęt (ang. hardware); oprogramowanie pośredniczące (ang. middleware), np. aplikacyjne, silniki bazodanowe czy systemy transakcyjne; oprogramowanie (ang. software); urządzenia peryferyjne, np. drukarki, skanery, plotery;
serwery
środki komunikacji, np. dostęp przez www, usługi sieciowe; narzędzia; bezpieczeństwo; środowisko fizyczne (np. wymagania dotyczące wielkości pomieszczeń, dopuszczalnego poziomu hałasu, promieniowania, natężenia ruchu); akcesoria (np. specjalne dokumenty, takie jak karty kredytowe, karty do głosowania, karty odpowiedzi do testu wielokrotnego wyboru, celowo uszkodzone płyty DVD). Przykładowa struktura wymagań co do środowiska testowego wygląda następująco: a) ogólny opis (opisuje elementy środowiska wymagane do wykonania procedur testowych; swym zakresem obejmuje zarówno fazę przygotowawczą, fazę wykonania procedur, jak i fazę po wykonaniu procedur); b) unikalny identyfikator dla każdego elementu środowiska; c) dokładny opis każdego elementu środowiska wraz z wymaganiami co do tych elementów; d) osoba odpowiedzialna za dostarczenie i nadzór nad elementami środowiska; e) czas, na który potrzebne będą poszczególne elementy środowiska.
21.3.10. Przykład wymagań co do środowiska testowego Qualium IT W YMAGANIA CO DO ŚRODOW ISKA T EST OW EGO dla s ys temu ELROJ 0.9 Id: WS T-ELROJ_0.9, wers ja 1.0 Data: 2014-07-02 Osoba odpowiedzialna: Janina Nowakows ka (s enior config uration manag ement eng ineer)
Lp. Id
1
2
3
Element ś rodowis ka
Wymag ania
Czas
S ymulator S oftware’owy S creenS imulator Tes ty s ymulator ES 001 w wers ji zg odnej jednos tkowe elektroniczneg o z fizycznym modelem i integ racyjne ekranu wykorzys taneg o ekranu
ES 002 Ekran
Model CityS creen S -779 wraz z oprog ramowaniem dla modułu komunikacji
Tes ty s ys temowe i akceptacyjne
ES 003 Baza MyS QL
S krypty umożliwiające tworzenie konfig uracji bazy dla danych tes towych opis anych w Wymag aniach na dane tes towe
Cały czas trwania projektu
21.3.11. Raport o gotowości danych testowych (test data readiness report) Raport ten opisuje status gotowości/dostępności poszczególnych danych testowych. Dla każdej danej testowej zawiera jej unikatowy identyfikator (opisany w wymaganiach na dane testowe) oraz status tej danej. Status może informować tylko, czy dana jest gotowa do użycia, czy nie, ale w szczególności może również opisywać odchylenia danych od wymagań, np. w terminach wartości, struktury czy objętości.
21.3.12. Raport o gotowości środowiska testowego (test environment readiness report)
Raport ten opisuje status gotowości/dostępności poszczególnych elementów środowiska. Dla każdego elementu środowiska zawiera jego unikatowy identyfikator (opisany w wymaganiach na środowisko testowe) oraz status tego elementu. Status może informować tylko, czy dany element jest gotowy do użycia czy nie, ale w szczególności może również opisywać odchylenia od wymagań, np. w terminach wersji elementu lub opóźnienia w jego dostarczeniu. Tego typu wymagania często spotyka się w systemach krytycznych, o wysokich wymaganiach co do niezawodności czy też o wysokiej integracji danych.
21.3.13. Otrzymane wyniki (actual results) Dokument ten opisuje otrzymane, rzeczywiste wyniki testu. Wyniki te mogą być jedną liczbą lub łańcuchem znaków, ale mogą mieć również bardziej skomplikowaną strukturę. Ponadto, niektóre projekty wymagają bardziej restrykcyjnego podejścia do logowania wyników, np. logowania wszystkich akcji wykonanych w systemie lub wręcz nagrywania całego działania programu, w celu umożliwienia późniejszego dokładnego odtworzenia tego wykonania. Niektóre przypadki testowe podczas wykonania mogą produkować dane będące pośrednimi wynikami. Takie dane mogą być zapisywane osobno np. w dzienniku wykonania testów. Jeśli są zapisywane razem z wynikami końcowymi, to należy jasno rozróżnić, które wyniki są pośrednie, a które końcowe. Zwykle dokument opisujący otrzymane wyniki nie stanowi osobnego artefaktu procesu testowego. Najczęściej jego zawartość jest po prostu logowana w dzienniku wykonania testów.
21.3.14. Wynik testu (test result) Wynik testu to dokument opisujący to, w jaki sposób zakończyło się wykonanie każdego z testów – czy test został zdany, czy też nie. Innymi słowy, dokument ten opisuje porównanie wyników oczekiwanych z rzeczywistymi dla każdego przypadku testowego. Wynik testu jest zapisywany zwykle w dzienniku wykonania lub w innym miejscu i nie jest traktowany jako osobny dokument.
21.3.15. Dziennik wykonania testów (test execution log)
Dziennik wykonania testów, zwany często po prostu logiem testów to zapis wszystkich czynności występujących podczas uruchamiania i wykonywania testów w ramach jednej lub wielu procedur testowych. Dziennik często jest plikiem tekstowym zawierającym mniej lub bardziej ustrukturalizowaną informację na temat uruchamiania czy przebiegu testów. Często wykorzystuje się specjalne skrypty przetwarzające taki plik, które usuwają mniej istotne dane i przekształcają pozostałe informacje do czytelnej postaci. Przykładowa struktura dziennika wykonania testów wygląda następująco: a)
ogólne informacje (zawierają spis najistotniejszych zdarzeń zaobserwowanych podczas wykonywania testów, np. nagły spadek dostępnych zasobów lub wydajności, błędy wykonania skryptów testowych, zbyt długi czas trwania testu);
b) unikalny identyfikator dla każdego zdarzenia (zwykle jest to kolejna liczba naturalna); c) czas (ang. timestamp), w którym nastąpiło zdarzenie; d) opis zdarzenia; e) wpływ zdarzenia na wykonanie testu lub na otrzymany wynik. Na rysunku 21.2 jest pokazany fragment przykładowego dziennika dla testów (źródło: http://blogs.msdn.com/b/billbar/archive/2009/06/09/vsts-
obciążeniowych
2010-load-test-feature-saving-test-logs.aspx).
Rysunek 21.2. Przykładowy dziennik wykonania testów
21.3.16. Raport o incydencie (test incident report) Incydent to każde zdarzenie wymagające dalszego zbadania. Zdarzenia takie są zapisywane w raporcie o incydencie. Więcej informacji o zarządzaniu incydentami Czytelnik znajdzie w rozdziale 27. W szczególności, w podrozdziale 27.4 jest opisana zawartość raportu o defekcie.
21.3.17. Raport z sesji testowania eksploracyjnego (exploratory testing session report) Testowanie eksploracyjne jest nieustrukturalizowaną formą testowania opartą na doświadczeniu i intuicji. Tester ma dużą swobodę w wyborze formy oraz technik testowania. Mimo to testowanie to można również dokumentować. Raport z sesji testowania eksploracyjnego powinien zawierać następujące elementy: identyfikator dokumentu; nazwisko i imię testera/testerów przeprowadzającego/ych sesję; kartę testu (ang. test charter); podział zadań w ramach sesji; opis przebiegu sesji i uwagi;
znalezione problemy; znalezione defekty.
21.3.18. Przykład raportu z sesji testowania eksploracyjnego Qualium IT RAPORT Z SESJI T EST OW ANIA EKSPLORACYJNEGO S ys tem ELROJ 0.9 ID: ET-Elroj-006 T ester: Karol Nowakows ki Karta testu: Przetes tować zarządzanie liniami i kurs ami Podział zadań w ramach sesji: Czas trwania: 1h (s es ja krótka) Przyg otowanie do s es ji: 0% Projektowanie i wykonanie tes tów: 80% Badanie problemów i błędów: 20% Opis sesji: Przetes towałem ws zys tkie pods tawowe operacje związane z dodawaniem i us uwaniem linii oraz kurs ów, s tos ując różne s cenarius ze dotyczące liczby linii i kurs ów, kolejnoś ci dodawania oraz różnych s ekwencji wykonywanych akcji. Znalezione problemy: Podczas dodawania is tniejąceg o już kurs u znikał on z rozkładu jazdy (!). Działo s ię tak w każdym przypadku, niezależnie od liczby
is tniejących linii czy kurs ów, dlateg o problem ten jes t łatwy do zreprodukowania. Z ws tępneg o dochodzenia, przeprowadzoneg o ws pólnie z prog ramis tą Janem Malinows kim w ramach tej s es ji wynika, że prawdopodobną przyczyną błędu może być fakt, że dodawanie kurs u zmienia jeg o flag ę (jes t/nie ma w rozkładzie), zamias t us tawiać ją na „ jes t” . Dlateg o powtórne dodanie kurs u mająceg o flag ę „ jes t” może ją zmienić na flag ę „ nie ma” .
1 CTO to angielski skrót od Chief Technical Officer, czyli Dyrektor ds. Technologii. 2 Zarządzanie incydentami jest opisane w rozdziale 27. 3 Podobnie jak niesławny współczynnik „testers to developers ratio” [416] czy określanie, jaka część budżetu projektu ma być przeznaczona na testowanie.
22. Szacowanie testów
Szacowanie jest klasyczną czynnością w zarządzaniu każdym projektem. W przypadku projektu testowego jest wykorzystywane do: określenia pracochłonności projektu (ile czasu, zasobów, wysiłku będzie potrzebne do wykonania danych czynności?); określenia kosztochłonności projektu (ile będzie kosztować wykonanie zaplanowanych zadań?). Szacowanie pozwala na efektywną alokację zasobów oraz lepszą organizację projektu. Jest również podstawą do podjęcia decyzji o ograniczeniu zakresu projektu, jeśli estymowany koszt lub czas jest zbyt duży. Szacunki są oczywiście tylko przybliżeniem rzeczywistych wartości (bo tych jeszcze nie znamy), dlatego istotne jest, aby były tak dokładne, jak to tylko możliwe. Aby estymacja była dobra, w trakcie tworzenia musi mieć następujące charakterystyki [173]: jest oparta na wiedzy przedmiotu szacowania;
i
doświadczeniu ekspertów w zakresie
jest wspierana przez osoby, które będą wykonywały szacowaną pracę; jest możliwie dokładna w określaniu kosztów, zasobów, zadań i ludzi; bazuje na najbardziej prawdopodobnych kosztach, wysiłkach i czasach trwania poszczególnych zadań. Szacowanie, zwłaszcza w inżynierii oprogramowania, nie jest zadaniem łatwym. Problemy w estymowaniu wynikają nie tylko z natury tej dziedziny, lecz także z tzw. przyczyn politycznych. Niemniej istnieją dobre praktyki w naukach o zarządzaniu, które pozwalają uzyskiwać możliwie dobre szacunki.
szacowanie testów (ang. test estimation) – proces aproksymacji kosztu, czasu, wysiłku lub nakładu pracy, jaki będzie związany z testowaniem Jeśli organizacja w ogóle wykorzystuje szacowanie testów jest ważne, aby w trakcie projektu mierzyć poziom wszystkich czynników mogących mieć wpływ na estymowaną wartość oraz rzeczywistą wartość po zakończeniu danego projektu czy zadania. Tak zebrane dane historyczne są niesłychanie przydatne w tworzeniu lepszych modeli predykcyjnych. Stanowią również cenną informację, którą można wykorzystać w korekcie estymacji w przyszłych projektach. Rozważmy następujący prosty przykład: organizacja mierzyła koszt testów w czterech projektach, A, B, C i D. Wartości estymacji, parametry projektów oraz rzeczywisty koszt są ujęte w tabeli 22.1. Tabela 22.1. Przykładowe szacowanie kosztów testów z korektą
Szacunkowy Szacunkowa T echnologia koszt Projekt liczba wykorzystywana testów testów w projekcie (USD)
Rzeczywisty koszt testów (USD)
A
450
Java
450 000
480 000
B
600
Java
600 000
575 000
C
400
C++
400 000
580 000
D
500
C++
500 000
785 000
E
600
C++
900 000
930 000
Przy projekcie A oszacowano, że koszt jednego testu wyniesie około 1000 USD. Szacunki okazały się w miarę dokładne, dlatego takie same estymacje poczyniono w kolejnych projektach B, C i D. O ile w przypadku projektu B przewidywania znów w miarę dobrze się sprawdziły, o tyle przy projektach C i D wystąpił problem niedoszacowania – rzeczywisty koszt testowania był około półtora razy większy niż w projektach A i B. Liczba testów we wszystkich projektach jest tego samego rzędu wielkości (od 400 do 600), więc wielkość projektu może nie mieć wpływu na
te różnice. Z tabeli wiemy, że projekty C i D wykorzystywały inną technologię niż A i B, co jest prawdopodobną przyczyną tych rozbieżności. Dlatego, w przypadku projektu E, pisanego w tej samej technologii, co C i D wprowadzono korektę – koszt jednego testu podniesiono do kwoty 1500 USD. Tym razem oszacowania były bliższe rzeczywistości.
22.1. Czynniki wpływające na szacowanie Aby nasze szacunki były jak najbliższe rzeczywistości, musimy wziąć pod uwagę możliwie dużo czynników wpływających na szacowaną wielkość. Black [173] wymienia cztery podstawowe grupy takich czynników: czynniki procesowe; czynniki materiałowe; czynniki ludzkie; czynniki opóźniające. Przykłady czynników związanych z procesem, w którym działa zespół testowy: stopień, w jakim aktywności testowe przenikają projekt; wyraźne oddzielenie zespołu testowego od reszty organizacji; dobre zarządzanie zmianą; wybrany cykl życia projektowania lub utrzymywania oprogramowania, z uwzględnieniem poziomu dojrzałości procesu testowego; skuteczne i przeprowadzane na czas naprawy błędów; realistyczne harmonogramy i budżety; dostarczane na czas, wysokiej jakości artefakty testowe (skrypty, raporty, przypadki testowe, środowisko testowe itp.) właściwe przeprowadzenie testowania we wczesnych fazach testowych. Czynniki materiałowe pochodzą z samej natury projektu, wykorzystywanych narzędzi, dostępnych zasobów i tym podobnych:
istniejące, wykorzystywane automatyzacji testowania;
i
o
wysokiej
jakości
narzędzia
jakość środowiska, procesu, przypadków i narzędzi testowych; odpowiednie, dedykowane i bezpieczne środowisko testowe; oddzielone od produkcyjnego środowisko testowe; dostępność wyroczni testowych; dostępna i wysokiej jakości dokumentacja testowa i projektowa (wymagania, projekty, plany itp.); reużywalne systemy testowe oraz dokumentacja podobnych projektów;
z poprzednich,
podobieństwo projektu (w tym projektu testowego) do projektów wcześniejszych. Czynniki związane z ludźmi i zespołem: wysokiej klasy kierownictwo wyższego szczebla, menedżerowie oraz liderzy zespołów; zaangażowanie kierownictwa w jakość, w szczególności w odniesieniu do procesu testowego; realistyczne oczekiwania interesariuszy; umiejętności, doświadczenie i postawa członków zespołu; stabilność zespołu (w tym brak rotacji pracowników); zdrowe relacje międzyludzkie w zespole; kompetentni pracownicy stanowiący wsparcie środowiska testowego; docenienie w całej organizacji testowania, zarządzania wydaniami, zarządzania konfiguracją, administracji systemowej i innych mniej zauważalnych, ale kluczowych dla powodzenia przedsięwzięcia obszarów; wykorzystanie doświadczonych kontraktorów i konsultantów; uczciwość, transparentność, zaangażowanie, otwartość, dzielenie się wiedzą; dostępność szkoleń. Przykłady czynników opóźniających: wysoka złożoność procesów, projektu, organizacyjnej lub środowiska testowego;
technologii,
struktury
duża liczba interesariuszy procesu testowego lub całego projektu; wiele zespołów, zwłaszcza rozproszonych geograficznie; potrzeba formowania testowego;
i
szkolenia
nowo
powstającego
zespołu
dostępność określonego sprzętu i oprogramowania; jakość dostawców; decyzja o automatyzacji części czynności testowych (może dać zwrot z inwestycji w długim okresie, ale krótkookresowo paradoksalnie prawie zawsze jest powodem opóźnień); wymóg tworzenia szczegółowych, przypadków testowych;
dokładnie udokumentowanych
duży nacisk na tworzenie dokumentacji, zwłaszcza w formacie, z którym zespół testowy jest niezaznajomiony; zależności między czasem dostarczenia poszczególnych komponentów (szczególnie ważne w testach integracyjnych); „kruche” dane, np. wrażliwe na czas; wymaganie na wysoką jakość systemu (np. w przypadku projektów o znaczeniu krytycznym); duży i skomplikowany testowany system; brak danych historycznych lub przemysłowych w szczególności projektów wysoce innowacyjnych).
(dotyczy
Bardzo ważne jest, aby dokonując szacowania brać pod uwagę wymienione czynniki. Prawie na pewno większość z nich wystąpi w naszym kolejnym projekcie. Fakt wystąpienia każdego z czynników należy zapisywać tak, aby w danych historycznych pozostał jakiś ślad, że nastąpiło takie wydarzenie. Te informacje pozwolą na dokładniejsze zbadanie, jak ilościowo poszczególne czynniki wpływają na poszczególne parametry projektu, takie jak: czas, budżet, zasoby itp.
22.2. Techniki szacowania Istnieje wiele metod szacowania. Dalej prezentujemy kilka z nich, opisując jednocześnie okoliczności, w jakich dana technika powinna się sprawdzić. Część z tych metod opiszemy dokładnie w dalszej części książki.
22.2.1. Intuicja, zgadywanie, doświadczenie Jest to metoda najprostsza do przeprowadzenia i jednocześnie najbardziej podatna na błędy. Szacowanie polega na zgadywaniu, przy czym należy wspierać się swoją intuicją oraz doświadczeniem. Metoda polecana zwykle jako „ostatnia deska ratunku”, gdy nie możemy wykorzystać żadnego innego podejścia.
22.2.2. Estymacja przez analogię W tej metodzie szacunki oparte są na wartościach z innych, podobnych projektów. Używając analogii należy pamiętać o czynnikach, które mogą zawyżać lub zaniżać nasze szacunki, tak jak w przykładzie z tabeli 22.1.
22.2.3. Struktura podziału prac (Work Breakdown Structure, WBS) Metoda polega na dokonaniu podziału projektu na mniejsze części. Podział ten przeprowadza się tak długo, aż otrzymamy podzadania na tyle niewielkie, że możemy przeprowadzić wobec nich w miarę dokładne szacowanie. Przykład zastosowania struktury podziału prac do oszacowania liczby przypadków testowych dla systemu ELROJ z Dodatku A jest pokazany na rysunku 22.1.
Rysunek 22.1. Struktura podziału prac dla liczby testów systemu ELROJ
Białe elementy drzewa oznaczają części systemu, procesu testowego lub funkcjonalności, których nie dzielimy już dalej. Dla każdego elementu estymujemy liczbę testów jego dotyczącą, np. szacujemy, że przetestowanie funkcjonalności związanej z zarządzaniem kursami (dodawanie, usuwanie itp.) będzie wymagać stworzenia ośmiu przypadków testowych. Elementy oznaczone na szaro to części, które podlegają dalszemu podziałowi. Liczby na szarych polach odpowiadające tym wierzchołkom drzewa to suma wyników wszystkich jego dzieci. Na przykład, szacunkowa liczba testów modułowych oprogramowania wynosi 5 + 5 + 8 + 23 = 41. Sumując, przechodzimy w górę drzewa, aż znajdziemy się w jego korzeniu, którego liczba odpowiada szacunkowej estymacie. W naszym przypadku przewidujemy, że cały system ELROJ będzie wymagał stworzenia 95 przypadków testowych. Szacowanie dotyczyło tylko i wyłącznie liczby przypadków testowych. Aby estymować koszt lub czas tego testowania, należy wziąć pod uwagę różne typy tych testów. Na przykład projektowanie, implementacja i wykonanie testu systemowego będzie zwykle zajmowało więcej czasu niż w przypadku testu jednostkowego. struktura podziału prac, WBS (ang. Work Breakdown Structure, WBS) – układ elementów pracy i ich wzajemnych związków oraz związków z produktem końcowym [197]
22.2.4. Estymacja grupowa Estymacja grupowa wykorzystuje szacunki wielu osób, które następnie są uśredniane1. Istnieje wiele metod estymacji grupowej, np. metoda delficka, rozszerzona metoda delficka, poker planistyczny2. Dokładne omówienie różnych metod estymacji znajduje się w rozdziale 29. Załóżmy, że zebraliśmy 5 ekspertów z zakresu testowania i zapytaliśmy niezależnie każdego z nich, ile testów powinno być zaplanowanych dla systemu ELROJ. Otrzymaliśmy następujące odpowiedzi: Ekspert 1: 102 Ekspert 2: 230 Ekspert 3: 91 Ekspert 4: 90 Ekspert 5: 100
Po uśrednieniu tych wyników otrzymujemy liczbę 1/5 ⋅ (102 + 230 + 91 + 90 + 100) = 122,6. Zauważmy jednak, że ocena eksperta 2 znacznie odbiega od pozostałych. Różne techniki estymacji grupowej w odmienny sposób radzą sobie z takimi sytuacjami. Można – w przypadku zbyt dużych rozbieżności wyników – przeprowadzić dyskusję, w której ekspert udzielający skrajnej odpowiedzi uzasadnia swoją odpowiedź. Następnie można przeprowadzić kolejną turę „głosowania”, do momentu uzyskania akceptowalnego poziomu różnic między poszczególnymi wynikami. Inna metoda mogłaby polegać np. na wzięciu mediany, czyli wartości dzielącej posortowany zbiór otrzymanych wartości na dwie połowy. W naszym przypadku posortowane odpowiedzi wyglądają następująco: 90, 91, 100, 102, 230. Wartością środkową jest 100 i taki jest końcowy wynik. Jak widać, jest on bardziej zbliżony do tego uzyskanego za pomocą struktury prac niż rezultat polegający po prostu na uśrednieniu wszystkich wyników. Wynika to stąd, że średnia jest dużo bardziej niż mediana wrażliwa na wartości odstające. Inną odmianą techniki grupowej jest tzw. estymacja trzypunktowa. Polega ona na tym, że każdy ekspert podaje trzy oceny: pesymistyczną, najbardziej prawdopodobną oraz optymistyczną. Następnie uśrednia się wyniki (lub bierze medianę) w obrębie tych trzech kategorii, otrzymując trzy średnie dla ocen: pesymistycznej p, najbardziej prawdopodobnej n i optymistycznej o. Na koniec uśrednia się te trzy wartości, najczęściej według wzoru:
gdzie E jest końcowym, ostatecznym szacunkiem. Na przykład, gdyby naszych pięciu ekspertów podało następujące oceny (gdzie przez wartość optymistyczną rozumiemy najmniejszą możliwą liczbę przypadków do zaprojektowania):
Eks pert
Wartoś ć optymis tyczna (min)
Wartoś ć najbardziej prawdopodobna
Wartoś ć pes ymis tyczna (max)
1
70
102
145
2
150
230
300
3
45
91
110
4
40
90
150
5
30
100
150
mediany 45
100
150
ostateczna estymacja wyniosła by
Zespół ekspertów szacuje zatem
całkowitą liczbę testów na około 99. estymacja trzypunktowa (ang. three point estimation) – metoda estymacji testów wykorzystująca oszacowane wartości dla „najlepszego”, „najgorszego” i „najbardziej prawdopodobnego” przypadku w sprawie, którą oceniamy, aby zdefiniować poziom pewności otrzymanego wyniku estymacji
22.2.5. Dane przemysłowe Przemysłowe dane testowe są bardzo przydatne w szacowaniu testów. Są to wartości parametrów związanych z testowaniem, obliczone na podstawie rzeczywistych projektów. Jeśli zupełnie nie mamy pojęcia, jak oszacować dany parametr, to dane przemysłowe mogą być dobrym punktem wyjścia. Najbardziej znane dane przemysłowe pochodzą od Jonesa. Na przykład w książce [198] podaje on wartości parametrów przemysłowych przedstawionych w tabeli 22.2. Tabela 22.2. Średnie przemysłowe parametrów dotyczących testowania
Czynność, zadanie, parametr
W artość
Czas trwania projektu (w os obomies iącach)
(FP/150) ⋅ FP 0.4
Wys iłek dla s tworzenia 1 przypadku tes toweg o
1 os obog odzina
Liczba potrzebnych przypadków tes towych
FP 1.2
Oczekiwana liczba defektów
FP 1.25
Ś rednia liczba defektów na punkt funkcyjny (US A)
5,00
Źródło defektów (wymag ania:projekt:kod:dokumentacja:reg res ja)
3:4:5:2:1
Efektywnoś ć us uwania defektów dla ins pekcji formalnej projektu
65%
Efektywnoś ć us uwania defektów dla formalnej ins pekcji kodu
60%
Efektywnoś ć us uwania defektów dla dobrze zaprojektowanej s uity tes towej
30%
Ś rednia efektywnoś ć us uwania defektów w projekcie (US A)
85%
Tego typu dane można wykorzystać do porównania naszego projektu ze średnią wartością przemysłową i zobaczyć, jak nasza organizacja wypada na tle innych. Można również użyć ich do szacowania. Na przykład, jeśli w wymaganiach odkryliśmy 30 defektów, to możemy spodziewać się, że w fazie projektowej będzie ich około 40, uwzględniając stosunek 3:4 tych wartości. Jones w swoich badaniach zamiast linii kodu używa punktów funkcyjnych, które są niezależne od użytego języka programowania. Aby przeliczyć punkt funkcyjny na liczbę linii kodu, należy skorzystać ze specjalnej tabeli przeliczników PF/LOC dla określonego języka. Wiele takich tabel jest dostępnych w internecie. Więcej o punktach funkcyjnych Czytelnik znajdzie w punkcie 41.2.1.
22.2.6. Analiza punktów testowych (Test Point Analysis) Technika APT pochodzi z metodologii TMap [28]. Jest ona bardzo ciekawą, choć nietrywialną i niełatwą do przeprowadzenia metodą estymacji wysiłku testowego. Nawet, jeśli Czytelnik nie zamierza bezpośrednio jej stosować, to warto przestudiować APT, aby zobaczyć, w jaki sposób można budować modele predykcyjne, używając danych historycznych. Analiza tej metody pozwala również zobaczyć, jakie czynniki według niej wpływają na wielkość wysiłku testowego.
analiza punktów testowych, APT (ang. Test Point Analysis, TPA) – formuła umożliwiająca szacowanie czasu i wysiłku testowania oparta na punktach funkcyjnych [28] Szacowanie testu czarnoskrzynkowego zależy od trzech elementów: rozmiaru testowanego systemu, strategii testowej oraz poziomu produktywności. Pierwsze dwa elementy wyznaczają wielkość pracy testera w terminach punktów testowych. Mnożąc ją przez poziom produktywności, otrzymamy czas potrzebny do wykonania tej pracy. Przy obliczaniu tych trzech komponentów należy uwzględnić różne czynniki wpływające na zmianę wyniku pomiaru. Ogólny schemat procedury przeprowadzania APT jest pokazany na rysunku 22.2.
Rysunek 22.2. Schemat APT Aby przeprowadzić Analizę punktów testowych, należy mieć dostęp do projektu systemu, dzięki któremu będziemy w stanie podzielić system na funkcje czy też funkcjonalne obszary. Dynamiczne punkty testowe określają liczbę testów dynamicznych, związanych z uruchamianiem programu. Na liczbę tę mają wpływ dwa rodzaje czynników: jakościowe oraz zależne od funkcji. W podejściu APT wykorzystuje się 4 charakterystyki jakościowe z normy ISO 9126 [3]:
funkcjonalność Qf
bezpieczeństwo Qb użyteczność Qu wydajność Qw
Charakterystyki te muszą być mierzalne, tzn. musi istnieć odpowiednia technika ich testowania. Każdą z nich ocenia się na pięciopunktowej skali: 0, 3, 4, 5, 6, przy czym wartością domyślną jest 4. Wartość ta określa wysiłek, jaki należy włożyć w testowanie tej właśnie charakterystyki podczas testów dynamicznych. Na podstawie ocen oblicza się czynnik zależny od jakości, Qd :
Czynniki zależne od funkcji to: ważność dla użytkownika W, czyli jak bardzo dana funkcja jest istotna z punktu widzenia użytkownika; stosowana tu reguła inżynierska mówi, że około 25% funkcji powinno mieć stopień ważności „wysoki” (W = 12), tyle samo – kategorię „niski” (W = 3), a 50% – kategorię „średni” (W = 6). Domyślna wartość to 6; intensywność użycia I, czyli jak często dana funkcja jest używana. I = 12, jeśli funkcja jest używana w sposób nieprzerwany w ciągu dnia, I = 4, jeśli jest używana wielokrotnie w ciągu dnia, I = 2, jeśli jest używana tylko kilka razy w ciągu dnia lub tygodnia. Domyślna wartość to 4; stopień powiązania S (ang. interfacing), czyli wartość określająca, w jakim stopniu zmiana w danej funkcji wpłynie na cały system. Bardzo pomocna w definiowaniu tego stopnia jest macierz CRUD. Możliwe wartości to: S = 2 jeśli stopień powiązania jest niski, S = 4 jeśli średni oraz S = 8 jeśli wysoki. Domyślna wartość do 4; złożoność Z, czyli jak skomplikowana jest struktura algorytmu. Możliwe wartości to: Z = 3 jeśli funkcja zawiera nie więcej niż 5 warunków3, Z = 6 jeśli zawiera od 6 do 11 warunków, Z = 12 jeśli zawiera więcej niż 11 warunków. Wartość domyślna to 6; jednolitość J, która wynosi 1 (wartość domyślna) lub 0,6. Tę drugą wartość stosuje się, gdy do testowania funkcji można wykorzystać już istniejącą specyfikację (np. gdy funkcja jest kopią innej funkcji lub jest do
niej bardzo podobna). Wtedy zakłada się, że funkcja ta wnosi jedynie 60% swoich punktów funkcyjnych do całkowitej sumy. Na podstawie ocen powyższych charakterystych jest wyliczany czynnik zależny od funkcji f, Df : Na podstawie wartości Qd , Df oraz FPf – liczby punktów funkcyjnych dla funkcji f – oblicza się liczbę dynamicznych punktów testowych TPf według wzoru TPf = Qd ⋅ Df ⋅ FPf Statyczne punkty testowe Qs zlicza się na podstawie ocen charakterystyk
statycznych, czyli testowanych statycznie. Na przykład bezpieczeństwo mogłoby być testowane przy użyciu listy kontrolnej. Każda charakterystyka statyczna Qi wnosi do sumy Qs wartość 16, przy czym masymalna możliwa wartość Qs nie może przekroczyć 96: Qs = min{96, ∑ iQi} Całkowita liczba punktów testowych TP wyliczana jest ze wzoru: gdzie FP jest całkowitą liczbą punktów funkcyjnych systemu (minimum 500). Liczbę podstawowych godzin testowych PG oblicza się, mnożąc całkowitą liczbę punktów testowych przez dwa czynniki: produktywności P oraz środowiska E: PG = TP ⋅ P ⋅ E Czynnik produktywności P określa liczbę godzin testowych wymaganych dla jednego punktu testowego. Im jest on wyższy, tym więcej czasu potrzeba na przetestowanie jednego punktu testowego. Współczynnik P jest określany na podstawie doświadczenia, wiedzy i umiejętności zespołu testerskiego. W praktyce powinien on mieścić się między 0,7 a 2. Liczba godzin testowych zależy również od czynników środowiskowych: E = En + Etd + Ept + Esd + Est + Et gdzie: En oznacza stopień, w jakim testowanie jest zautomatyzowane za pomocą narzędzi; możliwe oceny: 1 (testowanie wykorzystuje język zapytań, np. SQL oraz narzędzie typu capture-replay), 2 (testowanie
wykorzystuje język zapytań, ale nie narzędzie typu capture-replay), 4 (brak narzędzi). Domyślna wartość to 4; Etd oznacza jakość testowania we wczesnych fazach. Jakość i zakres tego testowania może wpłynąć na zmniejszenie ilości funkcjonalności, którą należy przetestować lub na osłabienie kryteriów wymaganego pokrycia; możliwe oceny: 2 (plan testów dostępny i zespół testowy jest z nim zaznajomiony), 4 (plan testów dostępny), 8 (brak planu testów). Domyślna wartość: 8; Ept oznacza jakość podstawy testów (ang. test basis), na której będą oparte testy. Jakość ta wpływa na czas faz przygotowania i projektu testów; możliwe oceny: 3 (wykorzystuje się standardy dokumentowania i wzory dokumentów oraz przeprowadza inspekcje), 6 (wykorzystuje się standardy dokumentowania i wzory dokumentów), 12 (dokumentacja nie została stworzona przy użyciu określonych standardów i wzorów). Domyślna wartość: 12; Esd opisuje środowisko deweloperskie, w którym system jest realizowany. Od środowiska zależy stopień zapobiegania różnym błędom oraz niewłaściwym metodom pracy; możliwe oceny: 2 (system tworzony przy użyciu języka 4 generacji ze zintegrowaną bazą danych zawierającą wiele więzów), 4 (system tworzony przy użyciu języka 4 generacji, w możliwej kombinacji z językiem 3 generacji), 8 (system tworzony wyłącznie przy użyciu języka 3 generacji, takiego jak np. COBOL, Pascal itp.). Domyślna wartość: 4; Est opisuje stopień uprzedniego wykorzystania środowiska testowego, w którym aktualnie pracujemy. W sprawdzonym środowisku pojawia się mniej problemów opóźniających prace testerskie; możliwe oceny: 1 (środowisko było wcześniej wielokrotnie wykorzystywane), 2 (testy wykonywane będą w nowym środowisku, podobnym do wcześniej używanego), 4 (testy będą wykonywane w zupełnie nowym środowisku). Domyślna wartość: 1; Et opisuje stopień, w jakim istniejące testalia umożliwiają przeprowadzenie testu. Istnienie reużywalnych testaliów przyspiesza proces projektowania i wykonywania testów; możliwe oceny: 1 (dostępny do użycia ogólny zestaw danych, np. w postaci tabel oraz przypadki testowe dla danego testu), 2 (dostępny do użycia ogólny
zestaw danych), 4 (brak możliwych do użycia testaliów). Domyślna wartość: 4. Na całkowitą liczbę godzin testowych mają wpływ czynności związane z zarządzaniem, monitorowaniem i kontrolą procesu testowego. Narzut na zarządzanie NZ jest opisany wzorem
gdzie: Trz odzwierciedla rozmiar zespołu testowego i związany z tym narzut komunikacyjny zabierający pewną ilość czasu; możliwe wartości: 3 (zespół liczy nie więcej niż 4 osoby), 6 (zespół liczy od 5 do 10 osób), 12 (zespół liczy ponad 10 osób). Domyślna wartość: 6; Tnz odzwierciedla wykorzystanie narzędzi zarządzania w zakresie planowania i kontroli. Narzędzia takie pomagają w automatyzacji części pracy menedżera i tym samym ułatwiają przebieg całego procesu; możliwe wartości: 2 (są dostępne zarówno system rejestrowan