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).

Podobne dokumenty