├── .gitignore ├── README.md ├── core ├── __init__.py ├── conditions.py ├── config.py ├── elements.py └── tools.py └── tests ├── __init__.py ├── base_test.py ├── pages ├── __init__.py └── todomvc.py └── todo_mvc_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | */**/*.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy Selenium tests in Python demo 2 | 3 | This is a notes project for demo on how to write easy selenium tests in Python (of [Selenide](http://selenide.org) style). 4 | Demo was shown in Cogniance, 03.09.2015 5 | 6 | This project was inspired also by [Selene](https://github.com/yashaka/selene/) which is an attempt to implement [Selenide](http://selenide.org) + [htmlelements](https://github.com/yandex-qatools/htmlelements) in Python. 7 | 8 | It differs from Selene 9 | * by using Selenium explicit waits under the hood (Selene implements its own explicit waits) 10 | * by using Selenium style of expected conditions to be used with explicit waits 11 | * only manual webdriver creation (Selene creates and closes driver automatically) 12 | * no Element Widgets support (like in htmlelements), though can be added soon rather easy 13 | * no screenshots in error messages (though can be added very easy) 14 | This simplified the implementation a lot, but have some drawbacks becuase selenium's expected conditions are not handy in use, limiting the possibilities. 15 | 16 | ## To start writing tests 17 | * install python 18 | * install selenium and pytest 19 | * install IDE (e.g. PyCharm CE) 20 | * clone or download the repo 21 | * open in IDE 22 | * write your own tests under the tests folder 23 | * run them from command line via py.test command executed from the project folder 24 | 25 | (google on installation guides for all tools) 26 | 27 | ## TODOs 28 | * add more docs and howtos on insallation and usage, etc. 29 | * add "one test per feature" style tests with before/after hooks for clearing data 30 | * tag smoke tests 31 | * implement conditions in a simpler and smarter way 32 | 33 | ## Extra notes 34 | 35 | ### About PageObjects 36 | 37 | This version of the project contains all "helpers to work with page's elements" in a separate PageObject implemented 38 | as a simple Python module. 39 | On the lesson we used all helpers just inside the python file with test itself: http://pastebin.com/VmeQ6C3B 40 | 41 | Just remember that PageObject as a template is needed: 42 | 43 | * NOT ONLY to gather (encapsulate) things of one context ("elements and action/helper/step-methods to work with a specific page") 44 | 45 | * BUT mainly for code reuse 46 | 47 | i.e. if so far you used all these helpers only in one file, there is no actual need to move them into separate one;) 48 | 49 | Some companies do not use PageObject pattern because structure their tests in the way that these helpers are used only in one file. 50 | Though this is of course not a general recommended principle. Everything should be applied in context. 51 | Sometimes it will work, sometimes not. -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/easy-web-tests-python/319800f12fc012b90336f2816944f1a2a6d75632/core/__init__.py -------------------------------------------------------------------------------- /core/conditions.py: -------------------------------------------------------------------------------- 1 | from operator import contains, eq 2 | from selenium.common.exceptions import StaleElementReferenceException, NoSuchElementException 3 | from core import config 4 | 5 | # todo: refactor conditions to accept element.finder, not element - to make implementation of conditions more secure 6 | 7 | 8 | # this is an example of raw condition implemented from scratch 9 | class example_condition_contain_text(object): 10 | def __init__(self, element, expected_text): 11 | self.element = element 12 | self.expected_text = expected_text 13 | self.actual_text = None 14 | 15 | def __call__(self, driver): 16 | try: 17 | self.actual_text = self.element.finder().text 18 | return self.expected_text in self.actual_text 19 | except (NoSuchElementException, StaleElementReferenceException): 20 | return False 21 | 22 | def __str__(self): 23 | return """ failed while 24 | waiting %s seconds 25 | for element found by: %s 26 | to contain text: %s 27 | while actual text: %s 28 | """ % (config.timeout, 29 | self.element, 30 | self.expected_text, 31 | self.actual_text) 32 | 33 | 34 | class Condition(object): 35 | 36 | def __call__(self, driver): 37 | self.driver = driver 38 | try: 39 | return self.apply() 40 | except (NoSuchElementException, StaleElementReferenceException): 41 | return False 42 | 43 | def __str__(self): 44 | try: 45 | return """ 46 | failed while waiting %s seconds 47 | for %s found by: %s 48 | to assert %s%s%s 49 | """ % (config.timeout, 50 | self.identity(), 51 | self.entity(), 52 | self.__class__.__name__, 53 | """: 54 | expected: """ + str(self.expected()) if self.expected() else "", 55 | """ 56 | actual: """ + str(self.actual()) if self.actual() else "") 57 | except Exception as e: 58 | return "\n type: %s \n msg: %s \n" % (type(e), e) 59 | 60 | def identity(self): 61 | return "element" 62 | 63 | def entity(self): 64 | return self.element 65 | 66 | def expected(self): 67 | return None 68 | 69 | def actual(self): 70 | return None 71 | 72 | def apply(self): 73 | return False 74 | 75 | 76 | class CollectionCondition(Condition): 77 | 78 | def identity(self): 79 | return "elements" 80 | 81 | def entity(self): 82 | return self.elements 83 | 84 | 85 | class text(Condition): 86 | def __init__(self, element, expected_text): 87 | self.element = element 88 | self.expected_text = expected_text 89 | self.actual_text = None 90 | 91 | def compare_fn(self): 92 | return contains 93 | 94 | def apply(self): 95 | self.actual_text = self.element.finder().text 96 | # return self.expected_text in self.actual_text 97 | return self.compare_fn()(self.actual_text, self.expected_text) 98 | 99 | def expected(self): 100 | return self.expected_text 101 | 102 | def actual(self): 103 | return self.actual_text 104 | 105 | 106 | class exact_text(text): 107 | def compare_fn(self): 108 | return eq 109 | 110 | 111 | class visible(Condition): 112 | def __init__(self, element): 113 | self.element = element 114 | 115 | def apply(self): 116 | return self.element.finder().is_displayed() 117 | 118 | 119 | class css_class(Condition): 120 | 121 | def __init__(self, element, class_attribute_value): 122 | self.element = element 123 | self.expected_containable_class = class_attribute_value 124 | self.actual_class = None 125 | 126 | def apply(self): 127 | self.actual_class = self.element.get_attribute("class") 128 | return self.expected_containable_class in self.actual_class 129 | 130 | def expected(self): 131 | return self.expected_containable_class 132 | 133 | def actual(self): 134 | return self.actual() 135 | 136 | 137 | ######################### 138 | # COLLECTION CONDITIONS # 139 | ######################### 140 | 141 | 142 | class texts(CollectionCondition): 143 | 144 | def __init__(self, elements, *expected_texts): 145 | self.elements = elements 146 | self.expected_texts = expected_texts 147 | self.actual_texts = None 148 | 149 | def compare_fn(self): 150 | return contains 151 | 152 | def apply(self): 153 | self.actual_texts = [item.text for item in self.elements] 154 | return len(self.elements) == len(self.expected_texts) and \ 155 | all(map(self.compare_fn(), self.actual_texts, self.expected_texts)) 156 | 157 | def expected(self): 158 | return self.expected_texts 159 | 160 | def actual(self): 161 | return self.actual_texts 162 | 163 | 164 | class exact_texts(texts): 165 | 166 | def compare_fn(self): 167 | return eq 168 | 169 | class size(CollectionCondition): 170 | 171 | def __init__(self, elements, expected_size): 172 | self.elements = elements 173 | self.expected_size = expected_size 174 | self.actual_size = None 175 | 176 | def apply(self): 177 | self.actual_size = len(self.elements) 178 | return self.actual_size == self.expected_size 179 | 180 | def expected(self): 181 | return self.expected_size 182 | 183 | def actual(self): 184 | return self.actual_size 185 | 186 | 187 | def empty(self): 188 | return size(self, 0) 189 | 190 | 191 | class size_at_least(CollectionCondition): 192 | 193 | def __init__(self, elements, minimum_size): 194 | self.elements = elements 195 | self.minimum_size = minimum_size 196 | self.actual_size = None 197 | 198 | def apply(self): 199 | self.actual_size = len(self.elements) 200 | return self.actual_size >= self.minimum_size 201 | 202 | def expected(self): 203 | return self.minimum_size 204 | 205 | def actual(self): 206 | return self.actual_size 207 | -------------------------------------------------------------------------------- /core/config.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | 3 | browser = None 4 | timeout = 4 5 | -------------------------------------------------------------------------------- /core/elements.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver import ActionChains 2 | from selenium.webdriver.common.keys import Keys 3 | 4 | from selenium.webdriver.support.wait import WebDriverWait 5 | 6 | from conditions import * 7 | from core import config 8 | 9 | 10 | def actions(): 11 | return ActionChains(config.browser) 12 | 13 | 14 | class WaitingFinder(object): 15 | 16 | def __init__(self): 17 | self.locator = None 18 | self.default_conditions = {} 19 | 20 | def finder(self): 21 | pass 22 | 23 | def __getattr__(self, item): 24 | for condition_class, condition_args in self.default_conditions.items(): 25 | self.assure(condition_class, *condition_args) 26 | return getattr(self.finder(), item) 27 | 28 | def assure(self, condition_class, *condition_args): 29 | condition = condition_class(self, *condition_args) 30 | WebDriverWait(config.browser, config.timeout).until(condition, condition) 31 | return self 32 | 33 | def __str__(self): 34 | return self.locator 35 | 36 | 37 | class RootSElement(object): 38 | def __getattr__(self, item): 39 | return getattr(config.browser, item) 40 | 41 | 42 | class SmartElement(WaitingFinder): 43 | def __init__(self, css_selector, context=RootSElement()): 44 | self.locator = css_selector 45 | self.context = context 46 | self.default_conditions = {visible: []} 47 | 48 | def finder(self): 49 | return self.context.find_element_by_css_selector(self.locator) 50 | 51 | # def __getattr__(self, item): 52 | # self.assure(visible) 53 | # return getattr(self.finder(), item) 54 | 55 | def within(self, context): 56 | self.context = context 57 | return self 58 | 59 | def s(self, css_locator): 60 | return SmartElement(css_locator, self) 61 | 62 | def ss(self, css_locator): 63 | return SmartElementsCollection(css_locator, self) 64 | 65 | def double_click(self): 66 | actions().double_click(self).perform() 67 | return self 68 | 69 | def set_value(self, new_text_value): 70 | self.clear() 71 | self.send_keys(new_text_value) 72 | return self 73 | 74 | def press_enter(self): 75 | self.send_keys(Keys.ENTER) 76 | return self 77 | 78 | def hover(self): 79 | actions().move_to_element(self).perform() 80 | return self 81 | 82 | 83 | class SmartElementWrapper(SmartElement): 84 | def __init__(self, smart_element, locator=None): 85 | self._wrapped_element = smart_element 86 | super(SmartElementWrapper, self).__init__(locator or smart_element.locator) 87 | 88 | def finder(self): 89 | return self._wrapped_element 90 | 91 | 92 | class SmartElementsCollection(WaitingFinder): 93 | def __init__(self, css_selector, context=RootSElement(), wrapper_class=SmartElementWrapper): 94 | self.locator = css_selector 95 | self.context = context 96 | self._wrapper_class = wrapper_class 97 | self.default_conditions = [] 98 | 99 | def finder(self): 100 | return [self._wrapper_class(webelement, '%s[%s]' % (self.locator, index)) 101 | for index, webelement in enumerate(self.context.find_elements_by_css_selector(self.locator))] 102 | 103 | def filter(self, condition_class, *condition_args): 104 | filtered_elements = [selement for selement in self.finder() 105 | if condition_class(selement, *condition_args)(config.browser)] 106 | return SmartElementsCollectionWrapper(filtered_elements, self.locator + "[filtered by ...]") # todo: refactor to be verbose 107 | 108 | def find(self, condition_class, *condition_args): 109 | return self.filter(condition_class, *condition_args)[0] 110 | 111 | # def __getattr__(self, item): 112 | # return getattr(self.finder(), item) 113 | 114 | def __getitem__(self, item): 115 | self.assure(size_at_least, item + 1) 116 | return self.finder().__getitem__(item) 117 | 118 | def __len__(self): 119 | return self.finder().__len__() 120 | 121 | def __iter__(self): 122 | return self.finder().__iter__() 123 | 124 | 125 | class SmartElementsCollectionWrapper(SmartElementsCollection): 126 | def __init__(self, smart_elements_list, locator): 127 | self.wrapped_elements_list = smart_elements_list 128 | super(SmartElementsCollectionWrapper, self).__init__(locator) 129 | 130 | def finder(self): 131 | return self.wrapped_elements_list -------------------------------------------------------------------------------- /core/tools.py: -------------------------------------------------------------------------------- 1 | from core import config 2 | from core.elements import SmartElement, SmartElementsCollection 3 | 4 | 5 | def visit(url): 6 | config.browser.get(url) 7 | 8 | 9 | def s(css_selector): 10 | return SmartElement(css_selector) 11 | 12 | 13 | def ss(css_selector): 14 | return SmartElementsCollection(css_selector) 15 | 16 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/easy-web-tests-python/319800f12fc012b90336f2816944f1a2a6d75632/tests/__init__.py -------------------------------------------------------------------------------- /tests/base_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | import core.config 4 | 5 | 6 | @pytest.fixture(scope='class') 7 | def setup(request): 8 | core.config.browser = webdriver.Firefox() 9 | 10 | def teardown(): 11 | core.config.browser.quit() 12 | 13 | request.addfinalizer(teardown) 14 | 15 | 16 | @pytest.mark.usefixtures("setup") 17 | class BaseTest(object): 18 | pass -------------------------------------------------------------------------------- /tests/pages/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ayia' 2 | -------------------------------------------------------------------------------- /tests/pages/todomvc.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.keys import Keys 2 | 3 | from core.conditions import texts, css_class, visible, exact_text, exact_texts 4 | from core.tools import s, ss 5 | 6 | tasks = ss("#todo-list>li") 7 | 8 | def assert_tasks(*task_texts): 9 | tasks.assure(exact_texts, *task_texts) 10 | 11 | 12 | def assert_visible_tasks(*task_texts): 13 | tasks.filter(visible).assure(texts, *task_texts) 14 | 15 | 16 | def add(task_name): 17 | s("#new-todo").send_keys(task_name + Keys.ENTER) 18 | 19 | 20 | def edit(old_task_name, new_task_name): 21 | tasks.find(exact_text, old_task_name).s("label").double_click() 22 | tasks.find(css_class, "editing").s(".edit").set_value(new_task_name).press_enter() 23 | 24 | 25 | def toggle(task_name): 26 | tasks.find(exact_text, task_name).s(".toggle").click() 27 | 28 | 29 | def delete(task_name): 30 | tasks.find(exact_text, task_name).hover().s(".destroy").click() 31 | 32 | 33 | def toggle_all(): 34 | s("#toggle-all").click() 35 | 36 | 37 | def clear_completed(): 38 | s("#clear-completed").click() 39 | 40 | 41 | def filter_all(): 42 | s("[href='#/']").click() 43 | 44 | 45 | def filter_active(): 46 | s("[href*='active']").click() 47 | 48 | 49 | def filter_completed(): 50 | s("[href*='completed']").click() -------------------------------------------------------------------------------- /tests/todo_mvc_test.py: -------------------------------------------------------------------------------- 1 | from tests.base_test import * 2 | from core.conditions import empty 3 | from core.tools import visit 4 | from tests.pages.todomvc import * 5 | 6 | 7 | class TestTodoMVC(BaseTest): 8 | 9 | def test_tasks_life_cycle(self): 10 | 11 | visit("http://todomvc.com/examples/troopjs_require/#") 12 | 13 | add("a") 14 | 15 | edit("a", "a edited") 16 | 17 | # complete 18 | toggle("a edited") 19 | filter_active() 20 | tasks.filter(visible).assure(empty) 21 | 22 | # create from active filter 23 | add("b") 24 | toggle("b") 25 | filter_completed() 26 | assert_visible_tasks("a edited", "b") 27 | 28 | # reopen from completed filter 29 | toggle("a edited") 30 | assert_visible_tasks("b") 31 | filter_active() 32 | assert_visible_tasks("a edited") 33 | filter_all() 34 | assert_tasks("a edited", "b") 35 | 36 | # clear completed 37 | clear_completed() 38 | assert_tasks("a edited") 39 | 40 | # complete all 41 | toggle_all() 42 | filter_active() 43 | tasks.filter(visible).assure(empty) 44 | 45 | # reopen all 46 | toggle_all() 47 | assert_visible_tasks("a edited") 48 | 49 | add("c") 50 | 51 | # delete by editing to '' 52 | edit("a edited", "") 53 | filter_all() 54 | assert_tasks("c") 55 | 56 | # delete 57 | delete("c") 58 | tasks.assure(empty) 59 | 60 | --------------------------------------------------------------------------------