Testowanie II

Transkrypt

Testowanie II
Inżynieria Oprogramowania 1
Testowanie II
Testowanie II
Cel zajęć
Celem zajęć jest zapoznanie studentów z uzupełniającymi zagadnieniami dotyczącymi testowania
wytwarzanego oprogramowania. W pierwszej części zajęć przedstawiona zostanie metoda oceny
kompletności i jakości testów przy użyciu metryk pokrycia kodu (ang. code coverage), w drugiej
natomiast koncepcja atrap obiektów (ang. mock objects) pomocnych przy testowaniu jednostkowym klas
trudnych do odseparowania od reszty systemu.
Pokrycie kodu
Jak już była mowa na poprzednich zajęciach testy nie są w stanie zagwarantować, że w oprogramowaniu
nie ma błędów. Trzeba jednak się zastanowić, czy można zrobić coś, aby nasze testy znajdowały jak
największą liczbę błędów? Czy można w jakiś sposób sprawdzić jakość i kompletność naszych testów
(przez jakość rozumiemy tutaj znalezienie jak największej ilości błędów)? Jednym ze sposobów
mierzenia jakości testów jest sprawdzanie pokrycia kodu.
Możemy wyróżnić między innymi:
• pokrycie wyrażeń/linii (ang. statement/line coverage) – sprawdza czy sterowanie przeszło przez
każde wyrażenie dostępne w kodzie
Kod:
public int addItems(List items) {
if (items != null) {
list.addAll(items);
}
return list.size();
}
Test, który gwarantuje 100% pokrycia kodu:
@Test
public void testAddItems() {
assertEquals(3, someObject.addItems(new ArrayList()));
}
• pokrycie decyzji (ang. decision coverage) – sprawdza, czy każde wyrażenie logiczne przyjęło
wartość true i false. Całe wyrażenie logiczne jest brane w tym przypadku pod uwagę, bez względu
na podwyrażenia logiczne połączone operatorami AND (&&) lub OR (||).
Kod:
public int addItem(Object item) {
if (item != null) {
list.add(item);
}
return list.size();
}
Następujący test gwarantuje 50% pokrycia decyzji, gdyż warunek w instrukcji warunkowej
przyjmuje jedynie wartość true:
strona 1 / 7
Inżynieria Oprogramowania 1
Testowanie II
@Test
public void testAddItem() {
assertEquals(1, someObject.addItem(new Object()));
}
• pokrycie warunków (ang. condition coverage) – podobne do pokrycia decyzji, bierze jednak pod
uwagę wartości jakie przyjmują podwyrażenia logiczne
• pokrycie ścieżek (ang. path coverage) – weryfikuje, czy każda możliwa ścieżka została sprawdzona
Kod:
public int calculate() {
int result = 0;
if (a > 0) {
result += a;
}
if (b > 0) {
result += b;
}
return result;
}
Następujący test gwarantuje 25% pokrycia ścieżek, gdyż tylko jedna z czterech możliwych ścieżek
została sprawdzona:
@Test
public void testCalculate() {
SomeObject object = new SomeObject();
Object.a = 2;
object.b = 0;
assertEquals(2, object.calculate());
}
Dopiero następujący test gwarantuje 100% pokrycia ścieżek
@Test
public void testCalculate() {
SomeObject object = new SomeObject();
object.a = 2;
object.b = 0;
assertEquals(2, object.calculate());
object.a = 0;
object.b = 0;
assertEquals(0, object.calculate());
object.a = 3;
object.b = 1;
assertEquals(4, object.calculate());
object.a = 0;
object.b = 1;
assertEquals(1, object.calculate());
}
• pokrycie metod (ang. method coverage) – sprawdza, czy wszystkie dostępne metody zostały
wywołane podczas testowania
• pokrycie klas (ang. class coverage) – sprawdza, czy wszystkie klasy zostały chociaż raz
zainicjalizowane
strona 2 / 7
Inżynieria Oprogramowania 1
Testowanie II
Wybrane problemy
Sprawdzanie pokrycia kodu testami daje nam pewną informację o jakości naszych testów, nie możemy
jednak polegać na nim całkowicie. Poniżej przedstawione zostały sytuacje, które pokazują, że takie
podejście może być czasem mylące:
1. Pełne pokrycie kodu testami nie gwarantuje jakości!
Kod:
public List addObject(List list, Object o) {
if (list != null) {
list.add(o);
}
return list;
}
Następujący test gwarantuje 100% pokrycia wyrażeń, decyzji, warunków, metod, klas, ale nic tak
naprawdę nie testuje, bo nie ma w nim żadnej asercji:
@Test
public void testAddObject() {
SomeObject object = new SomeObject();
object.addObject(new ArrayList(), new Object());
object.addObject(null, null);
}
Kod:
public int multiply(int x, int y) {
return x + y;
}
Następujący test gwarantuje 100% pokrycia wyrażeń, decyzji, warunków, metod, klas, ale źle
dobrane dane wejściowe powodują, że test nie wykrywa błędu:
@Test
public void testAdd() {
SomeObject object = new SomeObject();
assertEquals(4, object.multiply(2, 2));
}
2. Czasami nie jest możliwe osiągnięcie pełnego pokrycia kodu.
3. Problem z testowaniem pętli. Czy aby na pewno wystarczy, jeśli kod pętli zostanie wykonany tylko
raz?
Zadania
1. Napisz na kartce test, który zapewnia 100% pokrycie decyzji w poniższym kodzie:
public int countUniqueElements(List<String> elements) {
if (elements == null) {
return 0;
}
HashMap<String, Object> uniqueElements =
new HashMap<String, Object>();
for (String element : elements) {
strona 3 / 7
Inżynieria Oprogramowania 1
Testowanie II
if (!uniqueElements.containsKey(element)) {
uniqueElements.put(element, null);
}
}
return unigueElements.size();
}
2. Podaj przykład kodu, dla którego nie jest możliwe napisanie testu dającego 100% pokrycia ścieżek
programu.
Mock objects
Testowanie jednostkowe opiera się na tworzeniu przypadków testowych dla pojedynczych klas. Dla
każdej testowanej klasy tworzona jest klasa testująca, której metody wywołują poszczególne metody z
obiektu klasy testowanej sprawdzając np. czy dla wskazanych parametrów zwracana przez metodę
wartość jest poprawna. Podejście to jest wystarczające w przypadku prostych klas. Problemy zaczynają
się gdy istnieje wiele powiązań między klasą testowaną a innymi klasami z systemu:
1. Jeżeli klasa testowana wymaga współpracy z dużą liczbą innych klas może się okazać, iż w metodzie
konfigurującej dany test (oznaczonej adnotacją @Before) musimy uruchamiać sporą część systemu
co może znacząco wydłużyć proces testowania.
2. Ciężko jest też testować kod, który korzysta z zasobów zewnętrznych, np. baz danych, połączenia z
serwerem itp. Wiąże się to nie tylko z wydłużeniem trwania takich testów, lecz również może dojść do
zakłamania testów – np. dane w bazie mogą się różnić od oczekiwanych lub mogą występować
problemy z połączeniem internetowym, przez co testy mogą się nie powieść.
3. Testowanie klasy w oderwaniu od reszty systemu może być trudne lub wręcz niemożliwe.
Z pomocą przychodzą nam atrapy obiektów, tzw. mocki. Służą do zastępowania nimi rzeczywistych
obiektów, tak by inne klasy nie zauważyły różnicy między prawdziwym obiektem a atrapą. Istnieją
biblioteki pozwalające na łatwe tworzenie mocków. Jedną z nich jest EasyMock.
EasyMock
EasyMock jest to biblioteka, która daje możliwość tworzenia obiektów mock dla podanych interfejsów
bądź klas. Obiekty mock potrafią symulować działanie rzeczywistych obiektów, tzn. dla zadanych
parametrów metody zwracają odpowiednią wartość, można na przykład określić dozwoloną liczbę
wywołań danej metody lub wymusić kolejność wywołań itp. Poniżej znajduje się wykaz często
używanych funkcji biblioteki EasyMock.
• Aby korzystać z EasyMock należy zaimportować statycznie klasę EasyMock z pakietu
org.easymock:
import static org.easymock.EasyMock.*;
• Tworzenie atrapy obiektu:
mock = createMock(<Klasa/Interfejs>.class);
• Obiekt mock posiada dwa stany: nagrywania i odgrywania. W stanie nagrywania należy dodać
oczekiwane wywołania metod na obiekcie mock, wraz z dozwolonymi parametrami. Po przejściu w
stan odgrywania obiekt mock może być używany tak samo jak rzeczywisty obiekt przez niego
strona 4 / 7
Inżynieria Oprogramowania 1
Testowanie II
zastępowany. W momencie, gdy interakcja z obiektem będzie odbiegać od oczekiwanej (ustawionej w
fazie nagrywania), obiekt mock rzuci wyjątek/błąd typu AssertionError.
• Ustawianie oczekiwanych wywołań
Dla metody nie zwracającej wartości:
mock.metodaOczekiwana(<Wartość parametru oczekiwana>);
expectLastCall(). … //operacje dodatkowe
Dla metody zwracającej wartość:
expect(mock.metodaOczekiwana(<Wartość parametru oczekiwana>). …
• Przejście ze stanu nagrywania do odtwarzania
replay(mock);
• Weryfikacja interakcji z obiektem
verify(mock);
• Określenie liczby wywołań
expect(mock.metoda()).times(3);
//dokładnie 3 wywołania
expect(mock.metoda()).times(1, 3); //od 1 do 3 wywołań
expect(mock.metoda()).anyTimes();
//dowolna liczba wywołań
expect(mock.metoda()).atLeastOnce();//przynajmniej jedno wywołanie
• Określenie wartości zwracanej
expect(mock.metoda()).andReturn(<Wartość>);
• Rzucanie wyjątków
expect(mock.metoda()).andThrow(new MyException());
• Ściśle określona kolejność wywołań
Mock = createStrictMock(<Klasa/Interfejs>.class);
Więcej funkcji można znaleźć w dokumentacji EasyMock:
http://easymock.org/EasyMock3_0_Documentation.html
Zadania
Poniżej znajduje się lista zadań do wykonania. Do każdego zadania przyporządkowany jest pakiet o
nazwie io.testing.task<numer_zadania> w projekcie Mocks umieszczonym w archiwum
Mock.zip.
Zadanie 1
1. Uzupełnij kod w klasie Task1Starter
strona 5 / 7
Inżynieria Oprogramowania 1
Testowanie II
◦ Utwórz obiekt mock w metodzie configureMock dla interfejsu IDataService
◦ Dodaj oczekiwanie na wywołanie metody connect, saveData z parametrem „Moje dane”,
disconnect
◦ Przełącz obiekt mock w stan odgrywania
◦ Dodaj weryfikację interakcje z obiektem mock w metodzie playInteractions
◦ Uruchom program. Na konsoli nie powinny się pojawić żadne błędy – interakcja z obiektem
przebiegała zgodnie z oczekiwaniami.
2. Zmień przebieg interakcji z obiektem mock w metodzie playInteractions poprzez wywołanie
metody saveData z innym parametrem, np. „Nowe dane”. Uruchom aplikację. Przejrzyj
wygenerowany stack trace. Co jest przyczyną rzucenia błędu AssertionError?
3. Przywróć początkowy przebieg interakcji. Usuń metodę disconnect i uruchom program. Z jakiego
powodu aplikacja przerwała swoje działanie?
4. Przywróć początkowy przebieg interakcji. Dodaj metodę connect po wywołaniu metody
disconnect. Uruchom program. Jaki wystąpił błąd?
5. Obecnie nie jest weryfikowana kolejność wywołań metod obiektu mock.
◦ Przywróć stan początkowy i sprawdź czy przestawienie kolejności wywołania metod, np. poprzez
zamianę connect z disconnect, ma wpływ na poprawność interakcji
◦ Zmień
sposób
tworzenia
obiektu
mock
poprzez
zamianę
metody
createMock
na
createStrictMock
◦ Uruchom aplikację. Jak zadziałała?
Zadanie 2
W tej chwili jeżeli chcemy by metoda saveData była wywołana dokładnie dwa razy musimy dokładnie
tyle wywołań tej metody wykonać podczas fazy nagrywania. Problemy z powyższym podejściem pojawią
się w momencie, gdy będziemy chcieli np. by metoda ta została uruchomiona minimum 2 razy, ale nie
więcej niż 5. W tym celu użyjemy specjalnych metod określających oczekiwaną ilość wywołań.
• Otwórz klasę Task2Starter
• Uruchom program. Dlaczego pojawia się błąd?
• Dodaj oczekiwanie by metoda connect była wywołana przynajmniej raz. Użyj konstrukcji
expectLastCall().__.
• Dodaj oczekiwanie by metoda saveData była wywołana 2 lub 3 razy bez względu na podaną wartość
parametru. Wskazówka: użyj metody statycznej anyObject podając jako parametr klasę String.
• Dodaj oczekiwanie by metoda disconnect była wywołana przynajmniej raz.
• Uruchom program. Czy występują błędy?
Zadanie 3
Do tej pory symulowane interakcje opierały się na wywoływaniu metod nie zwracających wartości.
Przyszła kolej na dodanie wartości, które powinna zwracać dana funkcja.
1. Przejdź do klasy Task3Starter
◦ Uruchom program. Czy aplikacja uruchomiła się poprawnie? Jeśli nie to dlaczego?
strona 6 / 7
Inżynieria Oprogramowania 1
Testowanie II
◦ Dodaj oczekiwanie by metoda getData w pierwszym wywołaniu zwróciła pusty string
◦ Dodaj oczekiwanie by metoda getData w drugim wywołaniu zwróciła string „Moje dane”
◦ Dodaj oczekiwanie by metoda getData w trzecim wywołaniu zwróciła string „Moje dane. Nowe
dane”
◦ Uruchom program i sprawdź co pojawia się na konsoli?
2. W interfejsie IDataService pojawiła się metoda countLength obliczająca liczbę znaków
podanego parametru
◦ Odkomentuj interakcje w metodzie playInteractions
◦ Uruchom program. Co się dzieje?
◦ Dodaj oczekiwania dla metody countLength dla parametrów „wyraz” i „dwa słowa” ustawiając
wartości zwracane na 5 i 9
◦ Uruchom aplikacje. Co pojawiło się na konsoli?
3. Może się okazać, że podczas połączenia z prawdziwym obiektem DataService wystąpi błąd
połączenia. W tym zadaniu zasymulujemy pojawienie się wyjątku ConnectException w obiekcie
mock.
◦ Odkomentuj słowo kluczowe throws w interfejsie IDataService
◦ Dodaj oczekiwanie rzucania throws wyjątku przez metodę connect. Pamiętaj by zachować
odpowiednią kolejność wywołania metod andThrows i atLeastOnce. Wywołanie connect
powinno się znaleźć w klauzuli try/catch, mimo iż tak naprawdę w fazie nagrywania wyjątek
ConnectException nie zostanie nigdy wygenerowany. W przypadku metody zwracającej wartość
nie byłoby konieczności dodawania tej klauzuli.
◦ Dodaj klauzulę try/catch w metodzie playInteractions
◦ Uruchom aplikację. Czy wyjątek został wygenerowany?
Zadanie 4
Obiekty typu mock najczęściej używa się w kontekście testowania jednostkowego. Spróbujemy teraz
zastosować obiekt mock przy pisaniu testów dla klasy ServiceFacade, która wykorzystuje pewien
serwis implementujący interfejs IDataService. Zamiast dostarczać obiektowi klasy ServiceFacade
prawdziwy obiekt komunikujący się z istniejącym serwerem, dostanie on obiekt mock.
• Otwórz klasę ServiceFacade. W konstruktorze wymaga ona podania obiektu typu IDataService.
Przeanalizuj kod dwóch metod.
• Otwórz klasę ServiceFacadeTest. Jest to klasa testowa dla klasy ServiceFacade. Posiada 3
metody testujące (do zaimplementowania) i jedną metodę setUpBefore wywoływaną za każdym
razem przed wykonaniem danego testu.
• Napisz metodę testową testReadData. Najpierw skonfiguruj obiekt mock, następnie przełącz go w
stan odtwarzania. Dodaj asercję JUnit sprawdzającą poprawność pobranych danych. Zweryfikuj
obiekt mock.
• Napisz metodę testową testStoreData, podobnie jak poprzednią
• Doimplementuj metodę testReadDataWithException, tak by zasymulować problem z
połączeniem do serwera
strona 7 / 7