Niniejszy post będzie wprowadzeniem do Testów Jednostkowych (ang. Unit Tests) dla platformy .Net. Po jego lekturze powinniśmy mieć ogólne m.in. o następujących zagadnieniach:
- Generowanie testów jednostkowych w VS 2010;
- Ładowanie danych ze źródeł zewnętrznych;
- Podstawowe klasy natywnych testów w VS i ich główne metody (przestrzeń nazw: Microsoft.VisualStudio.TestTools.UnitTesting);
- Konfiguracja testów przy wykorzystaniu pliku ustawień;
- Analiza wyników testowania
I. Testy jednostkowe - wstęp
Narzędziem, którym się posłużę będzie Visual Studio 2010 Ultimate (VS). Z tej wersji będą pochodziły też printscreeny wykorzystane w prezentacji. Skorzystam też z wbudowanych testów jednostkowych VS. Należy dodać, że platforma testów jednostkowych nie jest wbudowanym elementem technologii .Net. Jest to część środowiska VS. Natomiast nie wszystkie wersje VS posiadają wbudowane narzędzia testowe. By skorzystać z tej opcji musimy posiadać VS Professional, Premium lub Ultimate.
1. Czym są testy jednostkowe
Choć byśmy się zapierali rękoma i nogami, wstępne przetestowanie aplikacji to zadanie nie tylko testera ale niestety również programisty. Narzędziem, które rzecz znacząco ułatwia są testy jednostkowe.
Zwięzła - i na nasze potrzeby wystarczająca - definicja mówi, że test jednostkowy to kod, który wywołuje samodzielny element programu i weryfikuje jego poprawność.
Kluczowa jest tu cecha samodzielności, czy też atomowości testowanego kodu. Dobry test jednostkowy powinien sprawdzać mały fragment kodu, odpowiedzialny za jedną funkcjonalność.
Tylko wtedy testy skutecznie wspomagają wyszukiwanie błędnego kodu. Niezdany test jednoznacznie wskazuje na metodę, która wymaga poprawek.
2. Zalety testów jednostkowych
Testowanie w ogólności ma zapewnić lepszą jakość oprogramowania. Testy jednostkowe umożliwiają zautomatyzowanie procesu testowania, a w konsekwencji jego przyspieszenie i zoptymalizowanie.
Gotowy test służy nam wiele razy. Poza tym projekt testów może być regularnie odpalany, by monitorować jakość aplikacji. Dodatkowo środowisko VS i Team Foundation Server pozwalają narzucić ograniczenia podczas check-inów aplikacji. Na przykład można zablokować dodawanie do ostatecznej wersji aplikacji tego kodu, który nie przeszedł obowiązkowej serii testów.
Takie regularne testy chronią programistę również przed błędami wstecznymi, kiedy po naprawieniu jednej funkcji programu przestaje działać kod, który wcześniej był sprawny. Pewność, że nasze zmiany nic nie popsuły jest szczególnie cenna, kiedy poprawiamy coś “na ostatnią chwilę”, refaktoryzujemy kod, przygotowujemy kolejną wersję aplikacji itp.
Wreszcie, poprawnie napisany test może pomóc w zrozumieniu kodu, kiedy np. analizujemy fragment programu napisany 3 miesiące wcześniej i zachodzimy w głowę “co autor miał na myśli”. Przekazywane w testach argumenty, sprawdzany stan i bądź jego obsługa to dodatkowe informacje, które, przy niejasnym kodzie, mogą okazać się cenne.
II. Generowanie testów jednostkowych
Dobrą praktyką jest tworzenie jednego projektu testów na jeden projekt aplikacji. Kod jest przejrzysty a struktura aplikacji odpowiada strukturze testów. Kiedy ustalimy, które projekty testujemy do samego tworzenia testów możemy się zabrać w dwojaki sposób. VS oferuje nam gotowy szablon testów, który wykorzystamy w projekcie testów. Możemy też automatycznie wygenerować testy do istniejącego kodu.
Nim przejdziemy do genereowania samych testów potrzebujemy przykładowych klas i metod. Posłużymy się klasami: Card, Deck i Game. Metody będą odpowiadały m. in. za tasowanie talii, wyciąganie pierwszej karty z wierzchu talii, porównywanie kart w celu wskazania starszej itp. Z założeń dodatkowych przyjmiemy, że talia ma 52 karty i żadna się nie powtarza. Poza tym starszeństwo kart będziemy oceniali wedle notacji brydżowej (piki, kiery, kara, trefle). Porównując karty istotna będzie najpierw wartość kart (2, 3,…, Dama, Król, As) a dopiero jeśli karty okażą się równe – ich kolor.
1. Nowy projekt testów
By utworzyć nowy Projekt Testów klikamy File/New/Project i z gotowych szablonów w lewym panelu wybieramy Test.
Projekt automatycznie otrzymuje referencję do platformy testów VS: using Microsoft.VisualStudio.TestTools.UnitTesting. Do projektu załączone są też dwa pliki z rozszerzeniem .testsettings. Co oczywiste pozwalają one na manipulację ustawieniami testów
Klikając Test/New Test w menu VS otwieramy okno, z którego możemy wybrać siedem szablonów testów:- Basic Unit Test;
- Coded UI Test;
- Database Unit Test;
- Generic Test;
- Ordered Test;
- Unit Test;
- Unit Test Wizard;
Wstępne pisanie testów a dopiero później pisanie kodu, którego zadaniem jest pomyślne przejście przez testy jest istotnym elementem metodyki Test Driven Development.
2. Automatyczne generowanie testów na podstawie istniejącego kodu
By automatycznie utworzyć test dla klasy public class Deck klikamy prawym przyciskiem myszy (PPM) napis Deck i z menu kontekstowego wybieramy Create Unit Tests.
VS analizuje kod klasy i na jego podstawie tworzy testy jednostkowe. Dokładniej – tworzy nową solucję testów, wraz z testami. Nazwy testów domyślnie mają nazwy takie same jak nazwy testowanych metod z dodanym na końcu Test. Np. test metody CreateDeck będzie się nazywał CreateDeckTest. Metody testów zaopatrzone są w dodatkowo w atrybut TestMethod(), a klasa testów w TestClass(). Wygenerowane testy są gotowe do uruchomienia. Nie dostarczą nam jednak żadnych sensownych informacji:
[TestMethod()]
public void ShuffleDeckTest()
{
Deck target = new Deck(); // TODO: Initialize to an appropriate value
target.ShuffleDeck();
Assert.Inconclusive("A method that does not return a value cannot be verified.");
}
Naszym zadaniem jest teraz utworzenie danych testowych i właściwe wypełnienie metod asercji. Testowana metoda jest typu void, i nie zwraca żadnych parametrów. Domyślnie VS ostrzega nas, że taka metoda nie może zostać przetestowana.
Możemy jednak spróbować sprawdzić naszą metodę pośrednio. Przetestujmy na przykład czy tasowanie faktycznie działa na jednym obiekcie czy też w międzyczasie podmienia referencje:
[TestMethod()]
public void ShuffleDeckTest()
{
Deck deck = new Deck();
Deck deckToShuffle = deck;
deckToShuffle.ShuffleDeck();
Assert.AreSame(deck, deckToShuffle);
}
Możemy też skorzystać z klasy CollectionAssert. Służy ona do sprawdzania całych kolekcji danych. Oczywiście by z niej skorzystać klasa Deck musi implementować interfejs ICollection.
By sprawdzić metodę sortowania wykorzystamy metody AreEqual/AreNotEqual (sprawdza czy dwie kolekcje są takie same), AreEquivalent/AreNotEquivalent (sprawdza czy elementy kolekcji są takie same). By sprawdzić czy talia została przetasowana musi ona zawierać te same elementy przed i po tasowaniu (AreEquivalent), ale nie mogą być one w takiej samej kolejności, ponieważ kolejność po tasowaniu powinna się zmienić (AreEqual). Przykładowy test może wyglądać np. tak:
[TestMethod()]
public void ShuffleDeckTest()
{
Deck beforeShuffle = new Deck();
Deck afterShuffle = new Deck();
for (int i = 0; i < beforeShuffle.Count(); i++)
afterShuffle[i] = beforeShuffle[i];
afterShuffle.ShuffleDeck();
CollectionAssert.AreEquivalent((ICollection)beforeShuffle, (ICollection)afterShuffle);
CollectionAssert.AreNotEqual((ICollection)beforeShuffle, (ICollection)afterShuffle);
}
Jeśli nasza metoda tasowania pomyślnie przejdzie test to będziemy wiedzieli, że w talii mamy ciągle takie same obiekty ale w innej kolejności. W kodzie testu tworzone są dwie talie i do drugiej przekazywane są karty z pierwszej. Robimy tak ponieważ nie chcemy porównywać referencji obiektów Deck, a ich zawartość.
Przetestujmy jeszcze jedną metodę. Porównywanie kart będzie wykonywane przez klasę Game. Zakładamy, że karty porównywane są zgodnie z opisanymi wyżej zasadami (najpierw wartość, później kolor). Metoda ma zwracać wyższą kartę, więc jeśli otrzyma dwie o takiej samej wartości zwróci jedną z nich, ale nie poinformuje, że były równe.
[TestMethod()]
public void GetHigherCardTest()
{
Game target = new Game();
Card cardA = new Card(CardValue.Five, CardSuit.Clubs);
Card cardB = new Card(CardValue.Seven, CardSuit.Diamonds);
Card expected = cardB;
Card actual;
actual = target.GetHigherCard(cardA, cardB);
Assert.AreEqual(expected, actual);
}
Przekazujemy dwie karty do porównania: 7 Karo i 5 Trefl. Oczekujemy, że metoda zwróci jako wyższą kartę 7 Karo. I rzeczywiście kiedy klikniemy na GetHigherCardTest() prawym przyciskiem myszy i wybierzemy Run Test w oknie VS wyświetli się komunikat o sukcesie.
Obecnie sprawdziliśmy tylko jeden zestaw danych. Dobrze by testy obejmowały swym zasięgiem znacznie większy przedział danych.
III. Przekazanie danych testowych i analiza wyników
Nasz test sprawdził tylko jeden przypadek użycia metody porównującej karty. Powinniśmy sprawdzić jak zachowa się metoda porównująca np. kiedy przekażemy jej karty o tej samej wartości ale różnych kolorach, o różnych wartościach i tego samego koloru, takie same karty itp.
1. Charakterystyka danych testowych
Testując różne przypadki użycia warto stosować się do następujących wskazówek:
- Najpierw najczęściej występujący przypadek – prawidłowe wykonanie metody. Dane powinny być prawidłowe
- Różnorodne dane – testujemy prawidłowe wykonanie różnymi, poprawnymi zestawami danych
- Wartości graniczne – sprawdzamy metodę dla danych skrajnych
- Wartości null – sprawdzamy jak zachowa się metoda kiedy dostanie referencję null. Upewniamy się czy została zabezpieczona na taki przypadek. Możemy również sprawdzić czy wyrzucany jest właściwy wyjątek, jeśli takiego zachowania oczekujemy w przypadku dostarczenia danych typu null.
- Wyjątki – dostarczamy błędne dane i sprawdzamy czy test się nie powiedzie lub czy wykaże poprawny wyjątek
2. Przekazywanie zestawów danych testowych
Mając na uwadze powyższe wskazówki przetestujmy metodę, która z talii wydaje określoną liczbę kart. Test może wyglądać następująco:
[TestMethod()]
public void GiveHandTest()
{
Deck target = new Deck();
List<Card> hand = new List<Card>();
int numberOfCardsToGive = 3;
int expected = 3;
int actual;
hand = target.GiveHand(numberOfCardsToGive);
actual = hand.Count;
Assert.AreEqual(expected, actual);
}
Przekazujemy liczbę kart, którą chcemy otrzymać w tym rozdaniu – tutaj są to 3 karty. Dalej sprawdzamy czy nasza metoda rzeczywiście wydała nam 3 karty. (Alternatywnie bądź równolegle można sprawdzić czy z naszej talii ubyły 3 karty.) Teraz powinniśmy przetestować zachowanie naszej metody w różnych warunkach. Zgodnie z zaleceniami przedstawionymi w paragrafie III.1 sprawdzimy co się stanie kiedy wywołamy np. 7 kart, 20, 40, ale też 1, 0, -1 oraz 52, 53 (talia ma tylko 52 karty), 100 i null.
Jednak by to uczynić nie musimy pisać tego samego testu wielokrotnie, wypełniając go tylko innymi danymi. VS pozwala nam na dostarczenie całych zbiorów danych do testu. Możemy w tym celu skorzystać z bazy danych, pliku tekstowego, arkusza kalkulacyjnego, pliku konfiguracyjnego aplikacji itp.
W teście metody wydającej karty wykorzystamy plik arkusza kalkulacyjnego MS Excel. Najpierw tworzymy przykładowe dane.
Następnie dołączamy je do testu:
1) W menu VS wybieramy Test/Window/TestView.
2) W oknie TestView klikamy PPM na nazwę naszego testu.
3) Wybieramy Properties.
4) Wybieramy Data Connection String. Na końcu wiersza pojawi się przycisk z trzema kropkami. Klikamy go.
5) Otworzy się kreator, z którego wybieramy Database. Klikamy Next.
6) Klikamy przycisk New Connection.
7) W oknie Connection Properties wybieramy Change. Data Source ustawiamy na Microsoft ODBC Data Source. Klikamy OK.
8) Ponownie w Connection Properties wybieramy opcję Use connection string. Klikamy Build.
9) Wybieramy New i podajemy ścieżkę do pliku excel z danymi.
10) Po wybraniu pliku cofamy się do Connection Properties i klikamy OK.
11) W oknie Wizarda wybieramy, z którego arkusza w dokumencie excel chcemy skorzystać.
12) VS zapyta nas czy dodać plik do projektu. Potwierdzamy.
VS doda atrybut DataSource().
[DataSource("System.Data.Odbc", "Dsn=cardtest;dbq=C:\\Users\\pwolkowski\\Documents\\CardTest.xls;defaultdir=C:\\Users\\pwolkowski\\Documents;driverid=790;fil=excel 8.0;maxbuffersize=2048;pagetimeout=5", "Arkusz1$", DataAccessMethod.Sequential), TestMethod()]
public void GiveHandTest()
{
Deck target = new Deck();
List<Card> hand = new List<Card>();
int numberOfCardsToGive = Int32.Parse(TestContext.DataRow[0].ToString());
int expected = Int32.Parse(TestContext.DataRow[3].ToString());
int actual;
hand = target.GiveHand(numberOfCardsToGive);
actual = hand.Count;
Assert.AreEqual(expected, actual);
}
By przekazać dane korzystamy z klasy TestContext. W DataRow podaliśmy 0 i 3 – to dlatego, że w tych wierszach umieściliśmy nasze dane.
3. Analiza wyników
Jeśli chociaż jeden pakiet danych nie przejdzie testu VS uzna test za oblany. Wyświetli też informację o liczbie zdanych testów. Dodatkowo PPM na oknie wyników pozwoli wybrać View Test Result Details. Jeśli, któryś z pakietów nie przejdzie testu w oknie szczegółów testu otrzymamy informacje o wartościach oczekiwanych i otrzymanych.
Samo okno Test Result również pozwala na wyświetlenie dodatkowych informacji. Na pasku zadań okienka Test Results możemy wybrać listę [All Columns] i zaznaczyć, jakie jeszcze informacje powinny zostać wyświetlone w wynikach testów (VS udostępnia ponad 20 kolumn z różnymi informacjami).
Zgodnie z dobrymi praktykami testowania co najmniej 80% kodu powinny pokrywać testy jednostkowe. VS pozwala na sprawdzenie procentowego poziomu pokrycia kodu testami. W tym celu wybieramy Test/Edit Test Settings/Local. Z menu po lewej stronie wybieramy Data and Diagnostics i zaznaczamy Code Coverage. Opcję Code Coverage możemy wybrać również z menu głównego VS. Klikamy Test/Winows/Code Coverage. Zalecane
Inne okna dostępne z po kliknięciu Test/Windows to: Test View, Test List Editor, Test Results, Test Runs, Test Impact View.
IV. Przestrzeń nazw testów jednostkowych w VS
W przedstawionych przykładach skorzystaliśmy z dwóch atrybutów DataSource() i TestMethod(). W testach pojawiły się też klasy Assert i CollectionAssert. W tej części przedstawię też inne przydatne klasy, metody i atrybuty dostępne w przestrzeni nazw Microsoft.VisualStudio.TestTools.UnitTesting.
1. Atrybuty
Poniżej opis większości atrybutów:
- AssemblyCleanup – atrybut dla metody porządkującej. Metoda zostanie uruchomiona po wykonaniu wszystkich innych testów.
- AssemblyInitialize – atrybut metody przygotowawczej. Metoda z takim atrybutem zostanie wykonana jako pierwsza. Może posłużyć do przygotowania np. zasobów dla testów.
- DataSource – udostępnia informacje o połączeniu ze źródłem danych.
- DeploymentItem – pozwala wskazać dodatkowe pliki (.dll, .txt i innych), niezbędne do przeprowadzenia testu.
- ExpectedException – wskazuje metodę testową, której wartością oczekiwaną jest zwrócenie wyjątku.
- HostType – atrybut przydatny np. w testach dla ASP.NET kiedy to nie lokalny komputer jest hostem. HostType pozwala wskazać innego hosta.
- Ignore – oznaczoną tak metodę należy pominąć
- TestClass – atrybut do oznaczania klas, które zawierają metody testów.
- TestProperty – pozwala definiować właściwości metod testowych.
- TestMethod – oznacza metodę jako test jednostkowy.
- Timeout – określa limit czasu (w milisekundach) dla danej metody testowej.
2. Klasy Assert, CollectionAssert, StringAssert
Metody klasy Assert:
- AreEqual/AreNotEqual – sprawdza równość/nierówność dwóch wartości.
- AreSame i AreNotSame – sprawdza czy dwa obiekty są/nie są takie same.
- Inconclusive – zwraca informację o braku jednoznacznego wyniku (test nie został zdany, nie został też oblany).
- IsInstanceOfType/IsNotInstanceOfType – sprawdza czy obiekt jest/nie jest określonego typu.
- IsNull/IsNotNull – sprawdza czy obiekt zawiera/nie zawiera referencję null.
- IsTrue/IsFalse – sprawdza czy warunek został/nie został spełniony.
Metody klasy CollectionAssert:
- Contains – pozwala ustalić czy dana kolekcja zawiera element.
- AllItemsAreInstancesOfType – sprawdza czy kolekcja zawiera egzemplarze tego samego
typu.
- AreEqual/AreNotEqual – sprawdza czy kolekcje są takie same.
- AreEquivalent/AreNotEquivalent – sprawdza czy kolekcje mają te same elementy ale nie koniecznie w tej samej kolejności.
Metody klasy StringAssert:
- Contains – sprawdza czy dany string jest częścią innego obiektu typu string.
- StartsWith – umożliwia określenie czym zaczyna się badany string
- EndsWith – określa czy łańcuch kończy się danym podłańcuchem
- Matches/DoesNotMatch – sprawdza czy string pasuje do podanego wyrażenia.
V. Podsumowanie
Powyższe wprowadzenie do testów jednostkowych objęło m.in. ogólny opis testów jednostkowych, ich zastosowanie, tworzenie i konfigurowanie testów, sposoby dostarczania danych, ogólną charakterystyka danych testowych, analizę wyników, opis głównych atrybutów, klas i metod z przestrzeni nazw testów jednostkowych środowiska VS.
Wprowadzenie nie objęło takich tematów jak testy obciążeniowe, czy serie testów, pobieżnie przedstawiłem też złożony temat konfigurowania źródeł danych, pominąłem całkowicie m.in. kwestię zintegrowania testów jednostkowych z Team Foundation Server, a także roli testów jednostkowych w metodyce Test Driven Development.
Zainteresowanych wspomnianymi tematami odsyłam na strony MSDN, do rozdziału “Verifying Code by Using Unit Tests” (http://msdn.microsoft.com/en-us/library/dd264975.aspx).
Natomiast poniżej przedstawiam linki do źródeł, na których bazowałem pisząc powyższy post:
- Walkthrough: Creating and Running Unit Tests
http://msdn.microsoft.com/en-us/library/ms182532.aspx
- Verifying Code by Using Unit Tests:
http://msdn.microsoft.com/en-us/library/dd264975.aspx
- Piotr Zieliński, Testy jednostkowe w VS:
http://msdn.microsoft.com/pl-pl/library/testy-jednostkowe-w-visual-studio.aspx
- Mike Snell, Lars Powers, VS 2010. Księga eksperta, helion.pl 2010. Rozdział o Unit Testach dostępny na stronie wydawnictwa helion.pl:
http://helion.pl/eksiazki/microsoft-visual-studio-2010-ksiega-eksperta,vs21ke.htm
2 komentarze:
Hej
Mógłbyś zmienić podświetlanie składni, strasznie ciężko się to czyta teraz
Fajny tekst. Zwróciłbym jeszcze uwagę na metody z dość przydatnymi atrybutami [ClassInitialize()], [TestInitialize()], [TestCleanup()] i [ClassCleanup()]. Są automatycznie generowane razem z klasą testową, na podstawie nazw można wywnioskować, że służą do inicjowania odpowiednio całej klasy testującej, jak i poszczególnych metod. A następnie do "sprzątania". Ma to spore znaczenie - w praktyce testy odpalamy bardzo często i ważne jest, żeby żaden test nie pozostawiał za sobą żadnych śladów (casus znanej nam bazy, która sypała się po każdorazowym wykonaniu testów;)).
Fajnie jest też zaznaczyć w kodzie komentarzem poszczególne etapy każdego testu jednostkowego (np. set up, exercise, verify wg. http://blog.eweibel.net/?p=200). Znacznie poprawia to czytelność - wystarczy rzut oka i widać np. co jest w teście sprawdzane.
Jak już wspomniałem o czytelności, to trzeba też zwrócić uwagę na same nazwy testów. Ale o tym już w osobnym dokumencie o konwencjach nazewniczych - comming soon, stay tuned;)
Prześlij komentarz