Jak pisa¢ testy?
Transkrypt
Jak pisa¢ testy?
Jak pisać testy? Wersja 2 Jakub Stolarski 2011-11-28 Spis treści 1 Wymagania 1.1 Instalacja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i i 2 Testy akceptacyjne 2.1 Przygotowanie danych do testów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Przygotowanie testów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii ii iii 3 Testy funkcjonalne v 3.1 Testowanie javascript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viii 4 Test driven development 1 Wymagania Do testów oprócz Django potrzebne sa˛ biblioteki: • Django-WebTest 1 , • WebTest 2 , • AllPairs 3 • perf_utils, • selenium 4 . 1.1 Instalacja Instalacja zewn˛etrznych bibliotek za pomoca˛ pip odbywa si˛e poprzez wpisanie: 1 2 3 4 http://pypi.python.org/pypi/django-webtest http://webtest.pythonpaste.org/en/latest/index.html http://sourceforge.net/apps/trac/allpairs/ http://pypi.python.org/pypi/selenium xi pip pip pip pip install install install install webtest django-webtest AllPairs selenium Zamiast pip install można użyć easy_install. Perf_utils znajduje si˛e w naszym svn w django_project_templates (jak tak jak cms_project) ściagamy ˛ z svn: svn co svn://80.52.177.82/django_project_templates I uzupełniamy jeden z plików .pth w site-packages ścieżkami wskazujacymi ˛ na katalogi: django_project_templates/src django_project_templates/src/cms_project Aplikacj˛e perf_utils należy dodać do INSTALED_APPS w pliku local_settings.py. 2 Testy akceptacyjne W MowimyJakV1 w pliku cms_local/acceptance_tests.py znajduje si˛e przykładowy test akceptacyjny opisujacy ˛ podstawowy przebieg tworzenia i publikowania artykułu w serwisie www.mowimyjak.pl. Opis przypadku testowego: • admin lub kierownik definiuja˛ nowe zadanie (artykuł z kategoria˛ i tytułem) • redaktor widzi list˛e nowych zadań i pobiera interesujacy ˛ go temat • redaktor pisze tekst i przesyła go do sprawdzenia do admina (długość tekstu musi wynosić minimum 1000 znaków) • kierownik otrzymuje podglad ˛ artykułu • przy sprawdzeniu admin może: opulikować artykuł lub zwrócić go do redaktora W tym przypadku testowym mamy 3 role admin, kierownik oraz redaktor. 2.1 Przygotowanie danych do testów Na poczatek ˛ wypełnijmy testowa˛ baz˛e. Uruchamiamy aplikacj˛e na testowej bazie: python manage.py testserver Wchodzimy do panelu administracyjnego i tworzymy użytkowników dla odpowiednich ról. Nazwijmy ich superredaktor1 (admin), kierownik1 (kierownik), redaktor1 (redaktor). Oprócz ról tworzymy potrzebne kategorie, grupy użytkowników, uprawnienia itp. Jeżeli mamy już działajac ˛ a˛ baz˛e można ja˛ skopiować. Utworzona baza ma prefix test_ (możemy dodać w konfiguracji baz danych taka˛ baze z aliasem test_mowimyjak, aby wygodniej operować na bazie danych). Możemy zgrać jej zawartość do plików json, które wykorzystamy jako fixture w naszych testach: python manage.py dumpdata --database test_mowimyjak > cms_local/fixtures/auth.json python manage.py dumpdata --database test_mowimyjak > cms_local/fixtures/categories.json python manage.py dumpdata --database test_mowimyjak python manage.py dumpdata --database test_mowimyjak python manage.py dumpdata --database test_mowimyjak auth articles.ArticleBlockType \ categories \ authors > cms_local/fixtures/authors.json agency > cms_local/fixtures/agency.json editorprofiles.Editorial \ > editorprofiles/fixtures/editorial.json python manage.py dumpdata --database test_mowimyjak editorprofiles.EditorProfile \ > editorprofiles/fixtures/editorprofiles.json 2.2 Przygotowanie testów Nast˛epnie tworzymy szablon testów: python manage.py gen_tests --template acceptance > acceptance_tests.py W szablonie uzupełniamy fixtures o ścieżki do wcześniej wygenerowanych fixtures i tworzymy klas˛e dla naszego testu: class AbstractMuratorTest(AcceptanceTestCase): fixtures = [ ’cms_local/fixtures/auth.json’, ’cms_local/fixtures/categories.json’, ’cms_loca/fixtures/authors.json’, ’cms_loca/fixtures/agency.json’, ’editorprofiles/fixtures/editorial.json’, ’editorprofiles/fixtures/editorprofiles.json’, ] class BasicWorkflowTest(AbstractMuratorTest): pass Teraz zróbmy test sprawdzajacy ˛ poprawność dodawania i publikacji artykułu. Metdy testów musza˛ zaczynać si˛e od prefiksu test_: class BasicWorkflowTest(AbstractMuratorTest): def test_add_article(self): task_num = 1 self.task_title = ’test_zadanie%s’ % task_num # dodanie zadania przez kierownika self._supervisor_add_task() # dodawanie artykulu przez redaktora self._editor_add_article() # kierownik ma wglad ˛ do artykulu self._supervisor_view_article() # zatwierdzenie artykulu przez administratora self._admin_publish_article() Przygotowaliśmy sobie pomocnicza˛ zmienna˛ task_title, która b˛edzie tytułem zadania, a nast˛epnie artykułu. Test podzieliliśmy dla wygody na cz˛eści wykonywane poprzez poszczególne role. Uzupełnijmy teraz cz˛eść testu przypisana˛ do roli kierownika: def _supervisor_add_task(self): ## kierownik loguje sie do panelu kierownika response = self.app.get(’/panel-kierownika/’) response.form[’username’] = ’kierownik1’ response.form[’password’] = ’test’ response = response.form.submit().follow() ## klika dodaj zadanie response = response.click(href=’articles/task/add/’) ## wypelnia formularz response.form[’title’] = self.task_title response.form[’category’] = 22 ## klika na zapisz response = response.form.submit().follow() self.assertEquals(response.status_code, 200) try: self.task = Task.objects.get(title=self.task_title) except Task.DoesNotExist: self.task = None self.assertFalse(self.task is None) ## i wylogowuje sie response.click(href=’/panel-kierownika/logout’) Pod zmienna˛ self.app znajduje si˛e testowana aplikacja (wi˛ecej informacji w dokumentacji WebTest 2 ). Otwieramy stron˛e poprzez get, pobieramy jedyny znajdujacy ˛ si˛e tam formularz, uzupełniamy go i logujemy si˛e. Ponieważ po poprawnym zalogowaniu nast˛epuje przekierowanie, to dorzucamy follow. Nast˛epnie klikamy na przycisk dodawania zadania, wypełniamy formularz i tworzymy nowe zadanie. Nowo utworzone zadanie zapisujemy, aby móc odwołać si˛e do tego samego zadania w dalszej cz˛eści testu. Na koniec wylogowujemy si˛e. Analogicznie post˛epujemy dla pozostałych ról. Nast˛epnie wyciagamy ˛ testy z naszej klasy (aktualnie tylko jeden test) i tworzymy z nich zestaw testów, który podpinamy do głownego zestawu testów: basic_suite = test_loader.loadTestsFromTestCase(test_case) def suite(): suite = unittest.TestSuite() test_suites = [ basic_suite ] test_loader = unittest.TestLoader() for test_suite in test_suites: suite.addTest(test_suite) return suite W tej samej klasie testów możemy i powinniśmy zdefiniować inne testy sprawdzajace ˛ alternatywne ścieżki przebiegu (np. odrzucenie artykułu przez kierownika). Django automatycznie uruchamia jedynie te testy, które podpi˛ete sa˛ w pliku tests.py w katalogu aplikacji. Dlatego musimy zaimportować tam nasze testy: import unittest def suite(): from cms_local.acceptance_tests import suite as acceptance_suite suite = unittest.TestSuite() test_suites = [ acceptance_suite() ] for test_suite in test_suites: suite.addTest(test_suite) return suite Testy uruchamiamy poleceniem: python manage.py test cms_local 3 Testy funkcjonalne W MowimyJakV1 w pliku cms_local/functional_tests.py znajduje si˛e przykładowy test funkcjonalny sprawdzajacy ˛ działanie formularza tworzenia artykułu dla redaktora serwisu www.mowimyjak.pl. Opis przypadku testowego: • jako zalogowany redaktor pobieramy zadanie, na podstawie którego tworzymy artykuł • wypelniamy pola zajawka, tagi, treść, nadtytul, zdj˛ecie główne, galeria • klikamy “Zapisz i wyślij do sprawdzenia” • w wyniku otrzymujemy potwierdzenie, że artykuł został wysłany i trafiamy na strone główna˛ panelu redaktora Alternatywne przypadki do przetestowania: • uzupełniona zbyt długa zajawka • brak zajawki • brak tagów • brak treści • za długi nadtytuł Na poczatek ˛ generujemy szablon testów funkcjonalnych: python manage.py gen_tests --template functional > functional_tests.py Uzupełniamy szablon przez wcześniej wygenerowane fixtures oraz nasza˛ klasa˛ testowa˛ AddArticleTest. W każdym teście bedziemy potrzebować zadania, na podstawie którego powstanie artykuł. Dlatego w metodzie setUp tworzymy takie zadanie i przypisujemy je pod self.task, aby łatwiej odwołać si˛e do niego w testach. W cz˛esci tearDown powinniśmy wyczyścić wszystko to, co utworzyliśmy w setUp (czyli nasze zadanie): class AbstractMuratorTest(AcceptanceTestCase): fixtures = [ ’cms_local/fixtures/auth.json’, ’cms_local/fixtures/categories.json’, ’cms_loca/fixtures/authors.json’, ’cms_loca/fixtures/agency.json’, ’editorprofiles/fixtures/editorial.json’, ’editorprofiles/fixtures/editorprofiles.json’, ] class AddArticleTest(AbstractMuratorTest): def setUp(self): super(AddArticleTest, self).setUp() task_num = 1 self.task_title = ’test_zadanie%s’ % task_num # tworzymy zadanie supervisor = User.objects.get(username=’superredaktor1’) category = Category.objects.get(pk=22) self.proposal = TaskProposal.objects.create(title=self.task_title, status=TaskProposal.STATUS self.task = Task(title=self.task_title) self.task.category = category self.task.user = User.objects.get_or_create(username=settings.NOT_SET_USERNAME)[0] self.task.status = Task.STATUS_NEW self.task.save() def tearDown(self): # usuwamy artykul self.task.delete() self.proposal.delete() super(AddArticleTest, self).tearDown() Nast˛epnie tworzymy pomocnicza˛ metod˛e, która zasymuluje pobranie zadania przez zalogowanego redaktora, wypełni formularz, zapisze i wyśle do sprawdzenia: def _add_article(self, lead, tags, text, overtitle, lead_photo, gallery): editor = User.objects.get(username=’redaktor1’) response = self.app.get(’/panel-redaktora/articles/task/take/%s/’ % self.task.id, user=editor) response = response.follow() response.form[’lead’] = lead response.form[’tags’] = tags response.form[’text’] = text response.form[’overtitle’] = overtitle response.form[’lead_photo’] = lead_photo response.form[’gallery’] = gallery return response.form.submit(’_save_and_sent_to_check’, 0) Pod self.app mamy utworzony obiekt aplikacji WebTest, przez który możemy wykonywać zapytania na naszej aplikacji. W tym teście wywołujemy metod˛e get, która pobiera odpowiednie zadanie. Opis pozostałych metod (m.in. post) znajduje si˛e w dokumentacji WebTest 2 . Jako parametr user podaliśmy obiekt redaktora. Takie użycie powoduje, że zapytanie jest traktowane, jakbyśmy byli zalogowani jako ten użytkownik. W wyniku zapytania otrzymujemy obiekt response. Ponieważ pobranie zadania powoduje przekierowanie, to poda˛żamy za przekierowaniem przy użyciu metody follow. W wyniku otrzymujemy kolejny obiekt response. W obiekcie response pod zmienna˛ form znajduje si˛e pierwszy formularz znaleziony na stronie. Jeżeli formularzy byłoby wi˛ecej, to wszystkie znajduja si˛e w słowniku forms. Formularz zachowuje si˛e jak słownik i możemy zupełnić poszczególne pola podpisujac ˛ je pod kluczami, które odpowiadaja˛ artybutom name w formularzu. Na koniec wysyłamy formularz za pomoca˛ przycisk o nazwie “_save_and_sent_to_check”. Jeżeli takich przycisków byłoby wi˛ecej, to drugi parametr mówi, który z nich użyć. Dopisujemy jeszcze dwie kolejne metody pomocnicze. Jedna˛ wykorzystamy do testów, które zakładaja,˛ że wszystko zadziałało prawidłowo. Druga˛ do testów, w których oczekujemy, że system poinformuje nas o źle wypełnionym formularzu: def _add_article_ok(self, lead, tags, text, overtitle, lead_photo, gallery): response = self._add_article(lead, tags, text, overtitle, lead_photo, gallery) try: errors = response.context[’errors’] except (TypeError, KeyError): errors = [] self.assertEquals(errors, [], msg=errors) self.assertEquals(response.status_code, 302) response = response.follow() self.assertEquals(response.status_code, 200) def _add_article_error(self, lead, tags, text, overtitle, lead_photo, gallery): response = self._add_article(lead, tags, text, overtitle, lead_photo, gallery) try: errors = response.context[’errors’] except (TypeError, KeyError): errors = [] self.assertNotEquals(errors, []) self.assertEquals(response.status_code, 200) Obiekt response posiada atrybut context, w którym znajduje si˛e kontekst użyty do renderowania szablonów. Jeżeli wystapił ˛ bład ˛ w formularzu, to znajdzie si˛e tam lista errors wskazujaca, ˛ jakie bł˛edy wystapiły. ˛ Jeżeli poprawnie został dodany artykuł to oczekujemy, że errors b˛edzie puste, a kod zwrócony przez aplikacj˛e b˛edzie przekierowywał nas na inna˛ stron˛e. Jeżeli źle wypełniliśmy formularz oczekujemy niepustej listy bł˛edów oraz braku przekierowania. Metody self.assert sprawdzaja˛ te warunki w testach. Wi˛ecej metod jest opisanych w dokumentacji Django na temat testów 5 . Jeżeli funkcja przyjmuje różne parametry, to należałoby przetestować wszystkie możliwe kombinacje tych parametrów. Niestety jest to czasochłonne, a czasami wr˛ecz niemożliwe. Dlatego metoda˛ przynoszac ˛ a˛ dobre efekty jest sprawdzanie każdej pary takich parametrów. Aby to zrobić wykorzystamy dekorator generator, który na podstawie szablonu metody i przekazanych parametrów, automatycznie wygeneruje testy: @generator([’zajawka1’], [’tag1,tag2’], [’Tresc artykulu’], [’’, ’Nadtytul’], [’’], [’’]) def _add_article(self, lead, tags, text, overtitle, lead_photo, gallery): # poprawne dodanie artykulu self._add_article_ok(lead, tags, text, overtitle, lead_photo, gallery) @generator([’’, ’za_dluga_zajawka’ * 100], [’tag1,tag2’], [’Tresc artykulu’], [’’], [’’], [’’]) def _add_article_with_wrong_lead(self, lead, tags, text, overtitle, lead_photo, gallery): # blad dodawania artykulu - niepoprawna zajawka self._add_article_error(lead, tags, text, overtitle, lead_photo, gallery) @generator([’zajawka’], [’’], [’Tresc artykulu’], [’’], [’’], [’’]) def _add_article_with_wrong_tags(self, lead, tags, text, overtitle, lead_photo, gallery): # blad dodawania artykulu - niepoprawne tagi self._add_article_error(lead, tags, text, overtitle, lead_photo, gallery) @generator([’zajawka’], [’tag1,tag2’], [’’], [’’], [’’], [’’]) def _add_article_with_wrong_text(self, lead, tags, text, overtitle, lead_photo, gallery): # blad dodawania artykulu - niepoprawny tekst self._add_article_error(lead, tags, text, overtitle, lead_photo, gallery) @generator([’zajawka’], [’tag1,tag2’], [’tresc’], [’za dlugi nadtytul’ * 100], [’’], [’’]) def _add_article_with_wrong_overtitle(self, lead, tags, text, overtitle, lead_photo, gallery): # blad dodawania artykulu - niepoprawny nadtylul self._add_article_error(lead, tags, text, overtitle, lead_photo, gallery) Kolejnymi parametrami generatora sa˛ listy zawierajace ˛ możliwe wartości dla kolejnych argumentów dekorowanej funkcji. W przypadku wartości liczbowych, warto wprowadzić przynajmniej wartości ze skraju zakresu, w którym wartość jest poprawna oraz jakaś ˛ typowa˛ wartość ze środka zakresu. W przypadku łańcuchów znaków, warto sprawdzać, jak zachowuje si˛e test przy pustych łańcuchach, bardzo długich, zawierajacych ˛ spacje i znaki diakrytyczne. Na koniec wyciagamy ˛ testy z naszej klasy i tworzymy z nich zestaw testów, który podpinamy do głownego zestawu testów: add_article_suite = test_loader.loadTestsFromTestCase(AddArticleTest) def suite(): suite = unittest.TestSuite() test_suites = [ add_article_suite ] for test_suite in test_suites: suite.addTest(test_suite) return suite 5 https://docs.djangoproject.com/en/1.2/topics/testing/ Analogicznie jak w testach akcpetacyjnych należy podpiać ˛ testy do głownego tests.py: import unittest def suite(): from cms_local.functional_tests import suite as functional_suite suite = unittest.TestSuite() test_suites = [ functional_suite() ] for test_suite in test_suites: suite.addTest(test_suite) return suite Testy uruchamiamy poleceniem: python manage.py test cms_local 3.1 Testowanie javascript W MowimyJakV1 w pliku cms_local/selenium_tests.py znajduje si˛e przykładowy test funkcjonalny sprawdzajacy ˛ działanie autouzupełniania tagów w formularzu tworzenia artykułu dla redaktora serwisu www.mowimyjak.pl. Opis przypadku testowego: • redaktor loguje si˛e do panelu redaktora • klika na link “Dodaj nowy artykuł” • uzupełnia pole title • zaczyna uzupełnianie pola tags wybranym tagiem i czeka na podpowiedzi systemu • widzac ˛ interesujacy ˛ go tag potwierdza naciśni˛eciem klawisza “ENTER” • nast˛epnie uzupełnia zajawk˛e oraz treść w edytorze TinyMCE • zapisuje artykuł • wylogowuje si˛e Na poczatek ˛ generujemy szablon testów selenium: python manage.py gen_tests --template selenium > selenium_tests.py Szablon uzupełniamy o nowy test AddArticleTest: class AddArticleTest(AbstractMuratorTest): def setUp(self): super(AddArticleTest, self).setUp() def tearDown(self): super(AddArticleTest, self).tearDown() def test_should_show_autocomplete(self): pass Pod zmienna˛ self.driver znajduje si˛e webdriver selenium, który steruje zachowaniem przegladarki. ˛ Uzupełniamy dwie pomocniczne metody do logowania i wylogowania: def _login(self): self.driver.get(self.host_name + ’/panel-redaktora’) login_form = self.driver.find_element_by_id("login-form") login_form.find_element_by_name(’username’).send_keys("redaktor1") login_form.find_element_by_name(’password’).send_keys("test") login_form.submit() def _logout(self): self.driver.find_element_by_xpath("//a[contains(@href, ’/panel-redaktora/logout’)]").click() Polecenie metoda self.driver.get powoduje uruchomienie w przegladarce ˛ strony podanej w parametrze metoda˛ GET. Za pomoca˛ self.drive.find_element_by_id możemy pobrać interesujacy ˛ nas element na stronie. W naszym przypadku jest to formularz. Inne dost˛epne metody to: • find_element_by_xpath - pobiera element na stronie za pomoca˛ wyrażenia xpath • find_element_by_link_text - pobiera element na stronie na podstawie treści linku • find_element_by_partial_link_text - pobiera element na stronie na podstawie fragmentu treści linku • find_element_by_name - pobiera element na stronie na podstawie atrybutu name • find_element_by_tag_name - pobiera element na stronie na podstawie nazwy taga • find_element_by_class_name - pobiera element na stronie na podstawie nazwy klasy • find_element_by_css_selector - pobiera element na stronie w sposób analogiczny do selektorów css Oprócz tego wyst˛epuja˛ wszystkie wyżej wymienione metody w wersji find_elements, która powoduje pobranie listy wszystkich takich elementów, a nie tylko jednego. Dokumentacja do selenium webdriver jest dost˛epna w postaci pythonowych docstringów: import selenium.webdriver.remote.webdriver help(selenium.webdriver.remote.webdriver) Elementy pobrane za pomoca˛ metod find_element same również posiadaja˛ wiele z wcześniej wymienionych metod oraz swoje unikalne. Wewnatrz ˛ pobranego formularza logowania wyszukujemy po name pola username oraz password i uzupełniamy je symulujac ˛ wpisywanie tekstu za pomoca˛ metody send_keys. Nast˛epnie na formularzu wywołujemy metod˛e submit, która wywoła domyślna˛ akcj˛e dla formularza. Jeżeli chcielibyśmy wywołać jedna˛ z alternatywnych akcji, należałoby uruchomić submit na konkretnym elemencie np. przycisku OK lub Anuluj. Wypełniamy nasza˛ metod˛e testowa: ˛ def test_should_show_autocomplete(self): self._login() # potwierdz, zapytanie "Czy jestes pewien?" # czekamy, na zaladowanie sie strony # wypelniamy formularz ## wypelniamy tagi i czekamy na podpowiedz ## sprawdzamy, czy podpowiedziano tam wlasciwy tag # wypelniamy TinyMCE # zapisujemy formularz self._logout() Klikni˛ecie na link “Dodaj nowy artykuł” powoduje wyświetlenie alertu z informacja˛ typu “Czy jesteś pewien, że chcesz odstrzelić sobie palca?”. Metoda˛ self.driver.switch_to_alert przechodzimy do okienka alertu, a nast˛epnie potwierdzamy poprzez alert: self.driver.find_element_by_link_text(u"Dodaj nowy artykuł").click() self.driver.switch_to_alert().accept() Strona może ładować si˛e dłużej lub krócej i powinniśmy zaczekać na załadowanie si˛e jej. Załóżmy, że jak si˛e wyświetli już właściwy tytuł, to strona jest załadowana. Do oczekiwania na pewne zdarzenie służy obiekt WebDriverWait: from selenium.webdriver.support.ui import WebDriverWait ... try: # czekamy, na zaladowanie sie strony WebDriverWait(self.driver, 20).until(lambda driver : driver.title.lower().startswith(u"dodaj")) finally: pass Jako parametry przekazujemy webdriver oraz czas, po który zostanie wyrzucony wyjatek ˛ informujacy, ˛ że nie doczekaliśmy si˛e żadanego ˛ efektu. Nast˛epnie na naszym obiekcie, wywołujemy metod˛e until, której parametrem jest funkcja sprawdzajaca, ˛ czy zaszło oczekiwane zdarzenie. Prototyp tej funkcji to: def func(driver): pass Przekazywany parametr driver to przekazany webdriver do obiektu WebDriverWait. W chwili, gdy funkcja zwróci True, kończymy oczekiwanie. W dalszej cz˛eści testu wyszukujemy poszczególne pola formularza i uzupełniamy je. W przypadku tagów chcemy sprawdzić podpowiedzi. Dlatego wypełniamy tylko fragment nazwy taga i nast˛epnie oczekujemy, aż pojawi si˛e okieno z podpowiedziami. W chwili, gdy widoczne jest okno z podpowiedziami wybieramy pierwsza˛ automatycznie zaznaczona˛ odpowiedź. Aby zasymulować naciśni˛ecie klawisza ENTER musimy wykorzystać moduł z lista˛ kodów klawiszy: from selenium.webdriver.common import keys W tym module znajduje si˛e klasa Key, w której zdefiniowane sa˛ różne kody klawiszy. My wybieramy Keys.RETURN: self.driver.find_element_by_name(’title’).send_keys(’Artykul testowy’) self.driver.find_element_by_name(’tags’).send_keys(’infor’) try: def has_response(driver): ac_results = driver.find_element_by_class_name(’ac_results’) return ac_results.value_of_css_property(’display’) != ’none’ # czekamy, az przyjdzie odpowiedz z lista tagow WebDriverWait(self.driver, 20).until(has_response) finally: pass self.driver.find_element_by_name(’tags’).send_keys(keys.Keys.RETURN) Na koniec sprawdzamy, czy oby na pewno uzupełniło nam o tag, który wcześniej przygotowaliśmy: value = self.driver.find_element_by_name(’tags’).get_attribute(’value’).strip().strip(’,’) self.assertEquals(value,’informacja’) Zajawk˛e uzupełniamy w sposób analogiczny do title: self.driver.find_element_by_name(’lead’).send_keys(’zajawka’) Na koniec uzupełniania formularza, trzeba si˛e troch˛e pogimnastykować z TinyMCE. Używajac ˛ w zwykły sposób tego edytora, tak na prawd˛e nie uzupełniamy pola textarea, a jedynie wypełniamy iframe umieszczany na jego miejscu. Aby przejść do odpowiedniego iframe należy wykorzystać metod˛e switch_to_frame i jako parametr podać nazw˛e iframe: # TinyMCE dziala w iframe self.driver.switch_to_frame(’id_text_ifr’) Dalej już pobieramy element body wewnatrze ˛ tego iframe, które w przypadku TinyMCE jest zasze tinymce i klikamy na paragraf, czyli symulujemy dokładnie to co byśmy robili kursorem: tinymce_frame = self.driver.find_element_by_id(’tinymce’) textarea = tinymce_frame.find_element_by_xpath(’p’) textarea.click() Dalej pozostaje już tylko zasymulować wpisywanie tekstu i przełaczyć ˛ si˛e z iframe do głownego okna: textarea.send_keys(’tresc’) self.driver.switch_to_default_content() Na koniec zapisujemy i wylogowujemy si˛e: self.driver.find_element_by_name(’_save’).submit() self._logout() Teraz należy uruchomić testy. Testy selenium działaja˛ poprzez instruowanie rzeczywistej przegladarki ˛ do wykonywania poleceń, jakie wykonywałby człowiek. Z tego powodu, musimy mieć uruchomiony serwer z działajac ˛ a˛ aplikacja˛ oraz odpalić oddzielnie testy. Inaczej, niż to było w przypadku testów akceptacyjnych, które w całości odbywały si˛e po stronie pythona. Do uruchomienia serwera z aplikacja˛ wykorzystamy serwer testowy django: python manage.py testserver cms_local/fixtures/all.json W pliku cms_local/fixtures/all.json znajduja˛ si˛e fixtures wykorzystywane w tym teście. W przypadku MowimyJakV1 ten plik jest generowany poprzez uruchomienie merge_fixtures.py. Można by te wszystkie fixtures podać po kolei jako parametry do testserver, ale z lenistwa wol˛e generować jeden plik... Nast˛epnie należy uruchomić nasze testy. Testy selenium uruchamiamy za pomoca˛ polecenia: python manage.py testselenium cms_local To polecenie zakłada, że testy selenium umieszczone sa˛ w pliku selenium_tests.py w katalogu podanej aplikacji. Domyślnie uruchamiana jest przegladarka ˛ Firefox. Jeżeli chcecie wykorzystać inna˛ ustawcie w pliku local_settings.py zmienna˛ SELENIUM_WEBDRIVER na wartość Firefox, Ie lub Chrome. Jeżeli testserver działa pod adresem innym niż http://127.0.0.1:8000, to należy adres tego serwera przekazać do testselenium za pomoca˛ parametry testserver. 4 Test driven development W podejściu TDD w pierwszej kolejności pisane sa˛ testy, nast˛epnie kod. Takie podejście powoduje, że wi˛ecej pracy jest na poczatku ˛ tworzenia projektu, ale nie ma lawiny rzeczy do poprawy bliżej końca projektu. Tworzenie według TDD działa według schematu: 1. Weź przypadek użycia. 2. Utwórz przypadek testowy na podstawie przypadku użycia. 3. Utwórz test na podstawie przypadku testowego. 4. Uruchom test. Test wyszedł niepomyślnie? To dobrze. 5. Dopisz fragment testowanej funkcjonalności. 6. Uruchom test. • jeżeli test przeszedł pomyślnie, funkcjonalność jest skończona; przejdź do punktu 1 • jeżeli test nie powiódł si˛e, przejdź do punktu 5 Dzi˛eki TDD czas nie jest tracony na przygotowanie funkcjonalności, która “może” si˛e nam przydać. Robione jest tylko to, co niezb˛edne. W przypadku późniejszych zmian, testy zapewniaja,˛ że nie wprowadzimy bł˛edów w istniejacej ˛ funkcjonalności. Kluczem do sprawnego stosowania TDD jest dobry opis przypadków użycia. Przypadek użycia musi zawierać przynajmniej: • aktorów bioracych ˛ udział • warunki poczatkowe ˛ • warunki końcowe • przebieg zdarzeń Przykładowy przypadek użycia “rejestracja użytkownika”: aktorzy • niezalogowany użytkownik warunki poczatkowe ˛ • brak konta w serwisie warunki końcowe: • utworzone aktywne konto w serwisie • konto jest przypisane do grupy “użytkownicy” • użytkownik jest zalogowany przebieg zdarzeń: • niezarejestrowany użytkownik wchodzi na stron˛e rejestracji • użytkownik wypełnia obowiazkowe ˛ pola: – “nazwa użytkownika” – “hasło” – “powtórz hasło” – “adres email” – “akceptuj˛e regulamin” • użytkownik klika “zarejestruj” • system przekierowuje użytkownika na SG i wyświetla komunikat z informacja,˛ aby użytkownik aktywował link w mailu wysłanym na jego skrzynk˛e • użytkownik wchodzi na skrzynk˛e, otwiera maila i klika link aktywacyjny • system aktywuje konto użytkownika i wyświetla informacj˛e o poprawnej aktywacji alternatywny przebieg zdarzeń: • użytkownik wypełnia również niektóre pola nieobowiazkowe ˛ Przypadek testowy to realizacja przypadku użycia widziana już z perspektywy wyłacznie strony testujacej. ˛ Pomijamy elementy, które musi wykonać system, ale sprawdzamy, czy interesujace ˛ nas efekty zaszły. Na tym etapie wiemy już lub przewidujemy jak b˛eda˛ nazywać si˛e poszczególne pola w formularzach, wi˛ec wykorzystujemy t˛e wiedz˛e przy opisywaniu przebiegu zdarzeń. Przykładowy przypadek testowy: aktorzy • niezalogowany użytkownik warunki poczatkowe ˛ • czysty system bez użytkowników, ale z danymi umożliwiajacymi ˛ uruchomienie go, np. predefiniowane grupy użytkowników warunki końcowe • utworzone aktywne konto użytkownika • utworzony profil użytkownika • konto przypisane do grupy “użytkownicy” • wysłany 1 mail aktywacyjny • użytkownik jest zalogowany przebieg zdarzeń • niezarejestrowany użytkownik wchodzi na stron˛e rejestracji pod adresem ‘/rejestracja/’ • użytkownik wypełnia pola obowiazkowe ˛ ‘username’, ‘password’, ‘repassword’, ‘email’, ‘reg_accept’ • użytkownik klika przycisk “zarejestruj” • użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacja˛ o konieczności aktywacji z maila • użytkownik klika na link z maila • użytkownik otrzymuje informacj˛e, że jego konto jest zarejestrowane alternatywne przebiegi zdarzeń: • użytkownik bł˛ednie wypełnia pola obowiazkowe ˛ - otrzymuje komunikat o bł˛edzie • użytkownik wypełnia pola nieobowiazkowe ˛ - na zakończenie powinny być ustawione te pola Nast˛epnie przechodzimy do tworzenia kodu samego testu. Tworzymy szablon testów akceptacyjnych: python manage.py gen_tests --template acceptance > acceptance_tests.py Ponieważ jeszcze nie mamy danych, nie wypełniamy fixtures. Jak już b˛eda˛ istniały zr˛eby projektu z wypełnionymi podstawowymi kategoriami itp. b˛edzie można wykorzystać te dane do inicjalizacji testów: class AbstractMuratorTest(AcceptanceTestCase): fixtures = [ ] Dla każdej funkcjonalności tworzymy oddzielna˛ klase testów: class RegistrationTest(AbstractMuratorTest): def setUp(self): super(RegistrationTest, self).setUp() def tearDown(self): super(RegistrationTest, self).tearDown() def test_register_basic(self): pass Warunki poczatkowe ˛ przypadku testowego definiuja˛ nam środowisko, w jakim uruchamiany musi być test. To środowisko przygotowujemy w metodzie setUp. W przykładowym teście wymagane jest, aby istniała już grupa “użytkownicy”: def setUp(self): super(RegistrationTest, self).setUp() self.group = Group.objects.create(name=u’użytkownicy’) self.user_name = ’testuser’ self.user_password = ’testpassword’ self.user_email = ’[email protected]’ W cz˛eści tearDown sprzatamy ˛ wszystko po sobie. Testy w django uruchamiane sa˛ wewnatrz ˛ jednej transakcji, która jest wycofywana na zakończenie testów i bazwa wraca do stanu pierwotnego. Dlatego można pominać ˛ czyszczenie utworzonych obiektów w bazie. Jednak jeżeli w trakcie testu wykorzystywane były jakieś inne moduły aplikacji (np. zapis plików na dysku, cache), to trzeba je przywrócić do pierwotnego statu własnie w metodzie tearDown. W klasie testu powinien znaleźć si˛e przynajmniej jeden test, odpowiadajacy ˛ głownemu przebiegowi zdarzeń oraz testy odpowiadajace ˛ alternatywnym przebiegom. Zacznijmy od wypełnienia testu opisem przebiegu zdarzeń.: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept # użytkownik klika przycisk "zarejestruj" # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj # użytkownik klika na link z maila # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane # sprawdzenie warunków końcowych pass Teraz zamieniamy poszczególne kroki przebiegu zdarzeń na symulowane czynności wykonywane przez użytkownika. W pierwszej kolejności musimy wejść na stron˛e rejestracji.: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ response = self.app.get(’/rejestracja/’) # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept # użytkownik klika przycisk "zarejestruj" # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj # użytkownik klika na link z maila # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane # sprawdzenie warunków końcowych pass Pod atrybutem app znajduje si˛e testowana aplikacja. Wywołujac ˛ na niej metod˛e “get(‘/rejestracja/’)” symulujemy zapytanie, jakie wykonałaby przegladarka ˛ użytkownika. W wyniku otrzymujemy obiekt response. Jeżeli na stronie rejestracyjnej znajduje si˛e jeden formularz to znajdzie si˛e on pod atrybutem form obiektu response. Wszystkie formularze dost˛epne sa˛ w słowniku forms. Formularz zachowuje si˛e podobnie do słownika. Możemy wypełnić interesujace ˛ nas pola podpisujac ˛ odpowiednie wartości w formularzu. Zgodnie z opisem musimy wypełnić pola “username”, “password”, “repassword” (czyli ponownie wpisane hasło), “email” i zaakceptować regulamin (“reg_accept”).: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ response = self.app.get(’/rejestracja/’) # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept form = response.form form[’username’] = self.user_name form[’password’] = self.user_password form[’repassword’] = self.user_password form[’email’] = self.user_email form[’reg_accept’] = True # użytkownik klika przycisk "zarejestruj" # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj # użytkownik klika na link z maila # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane # sprawdzenie warunków końcowych pass Użytkownik klika przycisk “zarejestruj”. Wysłanie formularza symulujemy metoda˛ submit formularz. Metoda wywołana bez parametrów wykona domyślna˛ akcj˛e formularza. Jeżeli podamy parametry, to pierwszy z nich jest wartościa˛ name przycisku, a drugi jest indeksem takich przycisków (w przypadku, gdyby było wi˛ecej przycisków z takim samym name).: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ response = self.app.get(’/rejestracja/’) # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept form = response.form form[’username’] = self.user_name form[’password’] = self.user_password form[’repassword’] = self.user_password form[’email’] = self.user_email form[’reg_accept’] = True # użytkownik klika przycisk "zarejestruj" response = form.submit(’zarejestruj’, 0) # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj # użytkownik klika na link z maila # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane # sprawdzenie warunków końcowych pass Po poprawnej rejestracji powinniśmy być przekierowani na SG i otrzymać komunikat z informacja˛ “Na adres XYZ został wysłany link aktywacyjny.”. Aby poda˛żać za przekierowaniem należy wywołać metod˛e follow na obiekcie response. Do sprawdzenia, czy na stronie znajduje si˛e oczekiwany przez nas tekst możemy użyć atrybutu content.: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ response = self.app.get(’/rejestracja/’) # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept form = response.form form[’username’] = self.user_name form[’password’] = self.user_password form[’repassword’] = self.user_password form[’email’] = self.user_email form[’reg_accept’] = True # użytkownik klika przycisk "zarejestruj" response = form.submit(’zarejestruj’, 0) # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj response = response.follow() self.assertEquals(response.status_code, 200) self.assertTrue((u’Na adres %s został wysłany link aktywacyjny’ % self.user_email) in response.co # użytkownik klika na link z maila # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane # sprawdzenie warunków końcowych pass W trakcie działania testów Django zamiast wysyłać maile umieszcza je w testowej skrzynce django.core.mail.outbox. Testowa skrzyna jest zwykła lista˛ wysłanych maili zawierajac ˛ a˛ obiekty EmailMessage. W naszym teście sprawdzamy, czy został wysłany jeden mail, na podany przez nas adres i czy zawiera link rejestracyjny.: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ response = self.app.get(’/rejestracja/’) # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept form = response.form form[’username’] = self.user_name form[’password’] = self.user_password form[’repassword’] = self.user_password form[’email’] = self.user_email form[’reg_accept’] = True # użytkownik klika przycisk "zarejestruj" response = form.submit(’zarejestruj’, 0) # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj response = response.follow() self.assertEquals(response.status_code, 200) self.assertTrue((u’Na adres %s został wysłany link aktywacyjny’ % self.user_email) in response.co # użytkownik klika na link z maila from django.core import mail self.assertEquals(len(mail.outbox), 1) activation_mail = mail.outbox[0] self.assertTrue(activation_mail.subject.startswith(u’Aktywuj konto’)) self.assertTrue(self.user_email in activation_mail.to) import re match = re.search(’’’(?P<link>/aktywacja/\w+/)’’’, activation_mail.body) self.assertTrue(match is not None) link = match.groupdict()[’link’] response = self.app.get(link) # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane self.assertTrue((u’Twoje konto zostało zarejestrowane’) in response.content) # sprawdzenie warunków końcowych pass Na sam koniec sprawdzamy rzecz najważniejsza˛ - czy mamy utworzone konto i jest ono poprawne.: def test_register_basic(self): # niezarejestrowany użytkownik wchodzi na stron˛ e rejestracji pod adresem ’/rejestracja/’ response = self.app.get(’/rejestracja/’) # użytkownik wypełnia pola obowiazkowe ˛ ’username’, ’password’, ’repassword’, ’email’, ’reg_accept form = response.form form[’username’] = self.user_name form[’password’] = self.user_password form[’repassword’] = self.user_password form[’email’] = self.user_email form[’reg_accept’] = True # użytkownik klika przycisk "zarejestruj" response = form.submit(’zarejestruj’, 0) # użytkownik trafia na SG i otrzymuje komunikat, z informacja˛ o poprawnej rejestracji i informacj response = response.follow() self.assertEquals(response.status_code, 200) self.assertTrue((u’Na adres %s został wysłany link aktywacyjny’ % self.user_email) in response.co # użytkownik klika na link z maila from django.core import mail self.assertEquals(len(mail.outbox), 1) activation_mail = mail.outbox[0] self.assertTrue(activation_mail.subject.startswith(u’Aktywuj konto’)) self.assertTrue(self.user_email in activation_mail.to) import re match = re.search(’’’(?P<link>/aktywacja/\w+/)’’’, activation_mail.body) self.assertTrue(match is not None) link = match.groupdict()[’link’] response = self.app.get(link) # użytkownik otrzymuje informacj˛ e, że jego konto jest zarejestrowane self.assertTrue((u’Twoje konto zostało zarejestrowane’) in response.content) # sprawdzenie warunków końcowych # czy konto istnieje i jest poprawne? try: user = User.objects.get(username=self.user_name) except User.DoesNotExist: user = None self.assertTrue(user is not None) self.assertEquals(user.username, self.user_name) self.assertEquals(user.email, self.user_email) # czy użytkownik jest przypisany do grupy użytkowników? self.assertTrue(self.group in user.groups.all()) # czy profil istnieje? try: profile = user.profile except Profile.DoesNotExist: profile = None self.assertTrue(profile is not None) # czy użytkownik jest zalogowany? self.assertTrue((u’/logout’) in response.content) Po zakończeniu pisania testu podłaczamy ˛ go do testów aplikacji w pliku tests.py.: import unittest def suite(): from cms_local.acceptance_tests import suite as acceptance_suite suite = unittest.TestSuite() test_suites = [ acceptance_suite() ] for test_suite in test_suites: suite.addTest(test_suite) return suite Nast˛epnie uruchamiamy testy.: python manage.py test -v 2 <aplikacja> Testy powinny dać w rezultacie “fail” lub “error”. Jeżeli któryś test przeszedł poprawnie, a nie mamy napisanego dla niego kodu, który miałby go wykonywać, to znaczy, że ten test nic nie sprawdza. W nast˛epnej kolejności tworzymy kod, który wykonuje opisana˛ funkcjonalność. W chwili, gdy test przechodzi poprawnie, funkcjonalność jest zakończona (albo test jest niekompletny).