Cześć,
tym razem coś o testowaniu oprogramowania, a dokładniej testowaniu funkcjonalnym, a jeszcze dokładniej o framework’u Selenium.
Testowanie jest ogromnie ważną częścią dostarczania oprogramowania, gdyż jak wiadomo, programiści, nawet Ci najlepsi, nie są nieomylni i często, a nawet bardzo często zdarza się czegoś nie przewidzieć, szczególnie jeśli system jest skomplikowany / słabo napisany / rzadko rozwijany / szybko rozwijany – no ogólnie powodów są setki – dlatego zawsze warto patrzeć na dział QA (quality assurance – dział zapewnienia jakości) jak na mur który nie pozwala by Twoje błędy spowodowały kuku na produkcji :) A poprawiania na szybko nikt nie lubi, szczególnie jak stoi za Tobą jakiś PM :D Więc dzięki im za każdego reopena :)
Dział QA nie ma łatwego zadania – zazwyczaj muszą ogarniać cały projekt, jego założenia biznesowe jak i część techniczną – co wymaga bycia na bieżąco z wszystkim rzeczami które są dodawane / będą dodawane do głównej gałęzi. Poza zapewnieniem że nowe funkcjonalności nie zawierają błędów, należy sprawdzać jak nowy kod oddziałuje ze starym, czyli czy dodając funkcję A nie zepsuliśmy czegoś w miejscu B, C i Z. Takie błędy gdzie nowa funkcja działa, ale psuje poprzednie, nazywamy regresją.
Aby sprawnie wykrywać błędy regresyjne należało by przeklikiwać cały projekt po każdym kliknięciu merge – co jest nie wykonalne nawet jakby dział QA posiadał po 2-3 testerów na jednego programistę. No może zależy to jeszcze od tempa rozrostu projektu – ale nadal – niemożliwe.
No i przychodzą tutaj nam z pomocą testy automatyczne :)
Frameworkiem który może nam pomóc pisać testy automatyczne jest Selenium – pozwala on pisać testy które są odpowiednio odpalane automatycznie na przeglądarkach takich jak Chrome, Firefox itp Dokładniej to wygląda tak że Selenium odpala przeglądarkę i klika za Was, sprawdzając po drodze zostawione asserty i tak, wizualnie to wygląda świetnie jak np. sam formularz się wypełnia!
Testy Selenium można pisać w różnych językach, ja pokaże jak je uruchomić w Pythonie. Pierw należy pobrać webdriver’a – aplikację która pozwala uruchamiać przeglądarkę pod kontrolą Selenium – każda przeglądarka ma swojego webdriver’a. My przejdziemy przez konfigurację dla Chroma.
Pobrać driver’a należy ze strony – link. I zapisać w miejscu który macie w swoim PATHie, np. /usr/local/bin dla Linuxa. Gdy ten krok jest za nami, należy zainstalować paczkę selenium programem pip:
1 | pip install selenium |
i koniec! Można pisać testy.
W celu pokazania jak używać Selenium przetestujemy mojego bloga ;)
Stwórzmy klasę bazową dla każdego naszego testu:
1 2 3 4 5 6 7 8 9 | import unittest from selenium import webdriver class TestCase(unittest.TestCase): def setUp(self): self.driver = webdriver.Chrome() def tearDown(self): self.driver.close() |
metody setUp oraz tearDown wykonują się odpowiednio przed i po wykonaniu testów i również odpowiednio – inicjalizują odpowiedniego driver’a oraz go zamykają.
Poza klasą bazową napiszemy plik uruchamiający testy:
1 2 3 4 | import unittest if __name__ == "__main__": unittest.main() |
Teraz każdą klasę dziedziczącą po TestCase dodamy tutaj importem a biblioteka unittest odpali nam je automatycznie.
Zacznijmy od pierwszego testu – sprawdźmy czy na stronie „O mnie” jest moje zdjęcie ;)
Stwórzmy plik has_new_photo.py z zawartością:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from test_case import TestCase class HasGotNewPhoto(TestCase): BLOG_URL = "https://mmazur.eu.org" LINK_TEXT = "KIM JESTEM?" PHOTO_CLASS_NAME = "wp-image-2215" def test_has_got_new_photo(self): self.driver.get(self.BLOG_URL) try: elem = self.driver.find_element_by_partial_link_text(self.LINK_TEXT) except Exception: self.fail("Link not found!") elem.click() try: image = self.driver.find_element_by_class_name(self.PHOTO_CLASS_NAME) except Exception: self.fail("Image not found!") |
W klasie tej mamy jeden test – klasę traktujemy jako worek dla testów z tej samej kategorii a każda metoda zaczynająca się od przedrostka test_ jest traktowana jako test i będzie odpala przez bibliotekę unittest. Pierwsza linia tej metody to przejście na tego bloga, następnie szukamy linku który wygląda jak „KIM JESTEM?” – szukanie to obwarowujemy blokiem try-except, gdyż nieznalezienie elementu powoduje wyrzucenie wyjątku – i jeśli taki zostanie wyrzucony – oznaczamy test jako FAILED. Jeśli natomiast znajdziemy element „KIM JESTEM?” to klikamy w niego metodą click(). To spowoduje przejście na ową podstronę. Następnie szukamy elementu który ma klasę „wp-image-2215” (taką mojemu zdjęciu nadał WordPress) i również oblewamy ten kawałek blokiem try-except. Jeśli test nie spowoduje wykonania się metody fail(msg) lub nie przejdzie asercji (o czym za chwilkę) lub rzuci inny wyjątek – jest uznawany za zakończony sukcesem.
Uzupełnijmy nasz plik tests.py:
1 2 3 4 5 | import unittest from has_new_photo import HasGotNewPhoto if __name__ == "__main__": unittest.main() |
i odpalmy
1 | python3.5 tests.py |
w konsoli.
Spowoduje to uruchomienie przeglądarki Chrome i wykonanie testu oraz jej zamknięcie. Rezultat będzie w konsoli:
1 2 3 4 5 6 | mmazurek@mmazurek ~/selenium> python3.5 tests.py . ---------------------------------------------------------------------- Ran 1 test in 4.570s OK |
Napiszmy coś więcej, sprawdźmy czy tekst witający pojawia się na stronach statycznych tego bloga jak i na stronach wpisów, stwórzmy plik welcome_message.py z zawartością:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | from test_case import TestCase import re class WelcomeMessage(TestCase): BLOG_URL = "https://mmazur.eu.org" MESSAGE_CLASS_NAME = "textwidget" POST_TITLE_CLASS_NAME = "entry-title" STATIC_PAGE_ANCHOR = "DEKLARACJA MISJI ŻYCIOWEJ" TEXT = """ Cześć, cieszę się że zajrzałeś na mojego bloga. Mam nadzieję że znalazłeś tutaj to czego szukałeś. Informatyką pasjonuję się już od kilkunastu lat, przez ten czas było mi dane nauczyć się wielu technologii, tych powszechnie używanych jak i tych niszowych. Jeśli chciałbyś skorzystac z tych umiejętności, proszę skontaktuj się ze mną. Jeśli natomiast chciałbyś cytować treść z tego bloga - nie wahaj się, ale daj mi o tym znać mailem. Mateusz Mazurek """ def test_welcome_message_on_post_page(self): self.driver.get(self.BLOG_URL) try: elem = self.driver.find_element_by_class_name(self.POST_TITLE_CLASS_NAME) anchor = elem.text elem = self.driver.find_element_by_partial_link_text(anchor) except Exception: self.fail("Post not found") elem.send_keys("\n") try: welcome_msg = self.driver.find_element_by_class_name(self.MESSAGE_CLASS_NAME) except Exception: self.fail("Welcome text not found!") self.assertTrue( self._convert_into_comparing_format(welcome_msg.text) == self._convert_into_comparing_format(self.TEXT) ) def test_welcome_message_on_static_page(self): self.driver.get(self.BLOG_URL) try: elem = self.driver.find_element_by_partial_link_text(self.STATIC_PAGE_ANCHOR) except Exception: self.fail("Post not found") elem.click() try: welcome_msg = self.driver.find_element_by_class_name(self.MESSAGE_CLASS_NAME) except Exception: self.fail("Welcome text not found!") self.assertTrue( self._convert_into_comparing_format(welcome_msg.text) == self._convert_into_comparing_format(self.TEXT) ) def _convert_into_comparing_format(self, txt): return re.sub(r'\s+', '', txt) |
Tutaj mamy dwa testy – i te same myki z blokami try-except. Doszło nowe wyrażenie assertTrue() która sprawdza czy argument przekazany do niego jest wartości True. Mamy tu też metodę prywatną
_convert_into_comparing_format która wycina białe znaki z tekstów – tak zdecydowanie łatwiej się porównuje teksty.
Kolejny plik jaki stworzymy to będzie main_page.py z zawartością:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from test_case import TestCase class MainPage(TestCase): BLOG_URL = "https://mmazur.eu.org" POSTS_ON_MAIN = 9 POST_TITLE_CLASS_NAME = "entry-title" MENU_ELEMENTS = 4 def test_valid_count_of_posts_on_main(self): self.driver.get(self.BLOG_URL) elements = self.driver.find_elements_by_class_name(self.POST_TITLE_CLASS_NAME) self.assertEqual(self.POSTS_ON_MAIN, len(elements)) def test_all_menu_elements(self): self.driver.get(self.BLOG_URL) navigation = self.driver.find_element_by_id("site-navigation") menu_elements = navigation.find_elements_by_tag_name("li") self.assertEqual(self.MENU_ELEMENTS, len(menu_elements)) |
Te dwa testy sprawdzą czy liczba elementów menu = 4 oraz czy liczba wpisów na stronie głównej jest równa 9. Użyliśmy tutaj find_elements_by_class_name / find_elements_by_tag_name – nie było ich wcześniej a od swoich kolegów (find_element_by_class_name / find_element_by_tag_name) różnią się tym że zwracają listę wszystkich znalezionych elementów a nie pierwszy, szukając od głowy. Natomiast metoda assertEqual sprawdza czy elementy przekazane jako argumenty są sobie równe. Co idealnie pasuje do tej sytuacji. W obu testach porównujemy po prostu długość tablicy zwróconej jako rezultat szukania elementów na stronie z zadaną z góry wartością.
Mała dygresja – asercje w kodzie sprawdzają czy metoda ma sens być wywołana z zadanymi parametrami. W testach natomiast sprawdzają poszczególne kroki testów. Biblioteka unittest dostarcza nam zestaw metod typu właśnie assertEqual.
Kolejny plik jaki stworzymy to będzie contact_form.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | from test_case import TestCase class ContactForm(TestCase): BLOG_URL = "https://mmazur.eu.org" CONTACT_PAGE_ANCHOR="KONTAKT" EMAIL_ID = "email_1" EXPECTED_ERROR_CLASS = "cpefb_error" FORM_ID = "cpp_form" INVALID_EMAIL = "jestemwadliwymemailem!" def test_validation_of_contact_form_email_field(self): self.driver.get(self.BLOG_URL) try: contact_link = self.driver.find_element_by_partial_link_text(self.CONTACT_PAGE_ANCHOR) except Exception: self.fail("Menu element not found!") contact_link.click() try: email_field = self.driver.find_element_by_id(self.EMAIL_ID) except Exception: self.fail("Email field not found") email_field.send_keys(self.INVALID_EMAIL) try: send_button = self.driver.find_element_by_class_name(self.FORM_ID) except Exception: self.fail("Send button not found on contact page!") send_button.submit() email_field = self.driver.find_element_by_id(self.EMAIL_ID) self.assertTrue(self.EXPECTED_ERROR_CLASS in email_field.get_attribute("class")) |
Test ten sprawdza czy na stronie „KONTAKT” formularz kontakty wyświetli błąd przy próbie podania wadliwego adresu email. Co My mamy tutaj ciekawego… Ah, tak, jeśli chcemy wysłać formularz to mamy do tego metodę submit. Asercję sprawdzamy poprzez sprawdzenie czy do pola gdzie wpisujemy adres email, dodawana jest klasa określająca element jako podany błędnie – takie informacje można wywnioskować analizując zmieniający się kod strony – thanks to google chrome debugger :)
Nasz plik tests.py rozrósł się:
1 2 3 4 5 6 7 8 | import unittest from has_new_photo import HasGotNewPhoto from welcome_message import WelcomeMessage from main_page import MainPage from contact_form import ContactForm if __name__ == "__main__": unittest.main() |
ale nasz wynik się nie zmienił
1 2 3 4 5 6 | mmazurek@mmazurek ~/selenium> python3.5 tests.py ...... ---------------------------------------------------------------------- Ran 6 tests in 25.478s OK |
Jak widać napisanie własnych testów wysoko poziomowych nie musi być zadaniem dla seniora :) w Pythonie można to fajnie i łatwo osiągnąć. Selenium ma ogromne możliwości – po więcej informacji odsyłam do dokumentacji. Pobaw się tym, warto :)
A już całkowicie odchodząc od tematyki wpisu – zapraszam na bloga mojego serdecznego przyjaciela – link. Blog ten jest pisany w języku angielskim, ale przystępnym, nie skomplikowanym językiem, sam target natomiast, troszkę inny, ale odwiedzić na pewno warto :)
Mateusz Mazurek
Nie za bardzo rozumiem po co dajesz każdy find_element w bloku try/except. Zaciemnia to IMHO kod a niewiele przynosi. Jak nie znajdzie elementu to test i tak się wywali, w stacktrace zobaczysz na czym. A jak poleci jakiś inny wyjątek, to w twoim przypadku, się o tym nie dowiesz.
Wszystko zależy od tego jakiego output’u oczekujesz:)