├── .gitignore ├── FINAL_PROJECT └── README.md ├── README.md ├── homework_rules ├── README.md └── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── lection01 - Intro ├── Lection1_Intro.pdf ├── code │ ├── conftest.py │ ├── fixtures.py │ ├── pytest.ini │ └── tests │ │ ├── test_basic.py │ │ ├── test_fixture.py │ │ ├── test_marks.py │ │ └── test_params.py └── git_commands.txt ├── lection03 - Selenium WebDriver └── 3.pdf ├── lection04-SeleniumWebDriver ├── HW1.md ├── code │ ├── base.py │ ├── test.py │ └── ui │ │ └── locators │ │ └── basic_locators.py └── conftest.py ├── lection05-PageObject ├── code │ ├── base.py │ ├── conftest.py │ ├── test.py │ └── ui │ │ ├── fixtures.py │ │ ├── locators │ │ └── basic_locators.py │ │ └── pages │ │ ├── base_page.py │ │ └── main_page.py └── lection05.pdf ├── lection06-Advanced └── code │ ├── base.py │ ├── conftest.py │ ├── files │ └── userdata │ ├── test.py │ └── ui │ ├── fixtures.py │ ├── locators │ └── basic_locators.py │ └── pages │ ├── base_page.py │ ├── events_page.py │ └── main_page.py ├── lection07-ReportRemote ├── code │ ├── base.py │ ├── conftest.py │ ├── files │ │ └── userdata │ ├── test.py │ ├── test_login.py │ └── ui │ │ ├── fixtures.py │ │ ├── locators │ │ └── basic_locators.py │ │ └── pages │ │ ├── base_page.py │ │ ├── events_page.py │ │ └── main_page.py └── homework2.md ├── lection08-ApiSimple ├── code │ ├── api │ │ └── client.py │ ├── base.py │ ├── conftest.py │ ├── files │ │ └── userdata │ ├── test.py │ ├── test_login.py │ └── ui │ │ ├── fixtures.py │ │ ├── locators │ │ └── basic_locators.py │ │ └── pages │ │ ├── base_page.py │ │ ├── events_page.py │ │ └── main_page.py └── lection8 - ApiSimple.pdf ├── lection09-Api ├── code │ ├── api │ │ └── client.py │ ├── conftest.py │ ├── files │ │ └── userdata │ ├── test_api │ │ ├── base.py │ │ ├── builder.py │ │ └── test.py │ ├── test_ui │ │ ├── base.py │ │ ├── test.py │ │ └── test_login.py │ └── ui │ │ ├── fixtures.py │ │ ├── locators │ │ └── basic_locators.py │ │ └── pages │ │ ├── base_page.py │ │ ├── events_page.py │ │ └── main_page.py └── homework3.md ├── lection10-Appium ├── Lection_mobile_1.pdf ├── Lection_mobile_2.pdf ├── code_appium │ ├── api │ │ └── wikipedia.py │ ├── conftest.py │ ├── pytest.ini │ ├── stuff │ │ └── Wikipedia_v2.7.50337.apk │ ├── ui │ │ ├── capability.py │ │ ├── fixtures.py │ │ ├── locators │ │ │ ├── locators_android.py │ │ │ ├── locators_mw.py │ │ │ └── locators_web.py │ │ └── pages │ │ │ ├── __init__.py │ │ │ ├── base_page.py │ │ │ ├── login_page.py │ │ │ ├── main_page.py │ │ │ ├── search_page.py │ │ │ ├── title_list_page.py │ │ │ └── title_page.py │ ├── utils │ │ └── decorators.py │ └── wiki_tests │ │ ├── android_tests │ │ └── test_android_wikipedia.py │ │ ├── base.py │ │ └── web_tests │ │ └── test_web_wikipedia.py ├── code_mw │ ├── api │ │ └── wikipedia.py │ ├── conftest.py │ ├── pytest.ini │ ├── ui │ │ ├── capability.py │ │ ├── fixtures.py │ │ ├── locators │ │ │ ├── locators_mw.py │ │ │ └── locators_web.py │ │ └── pages │ │ │ ├── __init__.py │ │ │ ├── base_page.py │ │ │ ├── login_page.py │ │ │ ├── main_page.py │ │ │ ├── search_page.py │ │ │ ├── title_list_page.py │ │ │ └── title_page.py │ ├── utils │ │ └── decorators.py │ └── web_tests │ │ ├── base.py │ │ └── test_web_wikipedia.py ├── homework4.md └── marussia_1.70.0.apk ├── lection11-PerformanceTesting └── Load Testing.pdf ├── lection12-LinuxInto └── lection12.pptx.pdf ├── lection13-LinuxDebug ├── homework5.md ├── lection13.md └── vim_cheat_sheet.png ├── lection14-SQL Basics ├── MySQL Basics.pdf └── code.md ├── lection15-SQL ORM ├── code_orm │ ├── conftest.py │ ├── models │ │ └── banner.py │ ├── mysql │ │ └── client.py │ ├── test_sql │ │ └── test.py │ └── utils │ │ └── builder.py ├── code_pysql │ ├── conftest.py │ ├── mysql │ │ └── client.py │ ├── test_sql │ │ └── test.py │ └── utils │ │ └── builder.py ├── homework.md ├── sql.py └── start_tests.sh ├── lection16-Network └── lection16.pptx.pdf ├── lection17-MocksAndHttpServers ├── code │ ├── application │ │ └── app.py │ ├── conftest.py │ ├── mock │ │ └── flask_mock.py │ ├── requirements.txt │ ├── settings.py │ ├── stub │ │ ├── flask_stub.py │ │ └── simple_http_server_stub.py │ └── tests │ │ ├── test.py │ │ └── test_socket.py └── homework7.md ├── lection18-Docker └── lection17-Docker.pdf ├── lection19-Docker ├── docker-compose.yml ├── python-mock │ ├── Dockerfile │ ├── code │ │ ├── application │ │ │ └── app.py │ │ ├── conftest.py │ │ ├── mock │ │ │ └── flask_mock.py │ │ ├── requirements.txt │ │ ├── settings.py │ │ ├── start_tests.sh │ │ ├── stub │ │ │ ├── flask_stub.py │ │ │ └── simple_http_server_stub.py │ │ └── tests │ │ │ ├── test.py │ │ │ └── test_socket.py │ └── requirements.txt └── qa-nginx │ ├── Dockerfile │ └── nginx.conf └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Автоматизированное тестирование 2 | Репозиторий курса "Автоматизированное тестирование" (VK образование). -------------------------------------------------------------------------------- /homework_rules/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/homework_rules/images/1.png -------------------------------------------------------------------------------- /homework_rules/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/homework_rules/images/2.png -------------------------------------------------------------------------------- /homework_rules/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/homework_rules/images/3.png -------------------------------------------------------------------------------- /homework_rules/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/homework_rules/images/4.png -------------------------------------------------------------------------------- /lection01 - Intro/Lection1_Intro.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection01 - Intro/Lection1_Intro.pdf -------------------------------------------------------------------------------- /lection01 - Intro/code/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption('--env', type=str, default='prod') 6 | 7 | 8 | def is_master(config): 9 | if hasattr(config, 'workerinput'): 10 | return False 11 | return True 12 | 13 | 14 | def pytest_configure(config): 15 | if is_master(config): 16 | print('This is configure hook on MASTER\n') 17 | else: 18 | sys.stderr.write(f'This is configure hook on WORKER {config.workerinput["workerid"]}\n') 19 | 20 | 21 | def pytest_unconfigure(config): 22 | if is_master(config): 23 | print('This is unconfigure hook on MASTER\n') 24 | else: 25 | sys.stderr.write(f'This is unconfigure hook on WORKER {config.workerinput["workerid"]}\n') 26 | -------------------------------------------------------------------------------- /lection01 - Intro/code/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import random 3 | 4 | 5 | @pytest.fixture(scope='function') 6 | def func_fixture(): 7 | yield random.randint(0, 100) 8 | 9 | 10 | @pytest.fixture(scope='class') 11 | def class_fixture(): 12 | yield random.randint(0, 100) 13 | 14 | 15 | @pytest.fixture(scope='session') 16 | def session_fixture(): 17 | yield random.randint(0, 100) 18 | 19 | 20 | @pytest.fixture() 21 | def random_filename(): 22 | return str(random.randint(0, 1000)) 23 | -------------------------------------------------------------------------------- /lection01 - Intro/code/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | smoke: mark a test as smoke 4 | regress: mark a test as regress -------------------------------------------------------------------------------- /lection01 - Intro/code/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_one(): 5 | assert 1 == 2, 'Not divided on 0' 6 | 7 | 8 | class Test: 9 | def test_two(self): 10 | assert 1 != 2 11 | 12 | 13 | def check_zero_division(a, b): 14 | assert a / b 15 | 16 | 17 | def test_negative(): 18 | with pytest.raises(ZeroDivisionError): 19 | check_zero_division(1, 1) 20 | pytest.fail(reason='No ZeroDivisionError error occured') 21 | -------------------------------------------------------------------------------- /lection01 - Intro/code/tests/test_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import random 4 | 5 | 6 | @pytest.fixture() 7 | def random_value(): 8 | print('entering') 9 | yield random.randint(0, 100) 10 | print('exiting') 11 | 12 | 13 | def test(random_value): 14 | assert random_value > 50 15 | 16 | 17 | def test1(func_fixture, session_fixture): 18 | print(func_fixture, session_fixture) 19 | 20 | 21 | def test2(func_fixture, session_fixture): 22 | print(func_fixture, session_fixture) 23 | 24 | 25 | class Test: 26 | def test1(self, func_fixture, class_fixture, session_fixture): 27 | print(func_fixture, class_fixture, session_fixture) 28 | 29 | def test2(self, func_fixture, class_fixture, session_fixture): 30 | print(func_fixture, class_fixture, session_fixture) 31 | 32 | # метод-пустышка, по факту в тестах не использовался, только для примера autouse-фикстуры 33 | # @pytest.fixture(autouse=True) 34 | # def new_file(random_filename): 35 | # import os 36 | # f = open(random_filename, 'w') 37 | # yield 38 | # f.close() 39 | # os.remove(random_filename) 40 | -------------------------------------------------------------------------------- /lection01 - Intro/code/tests/test_marks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.smoke 5 | @pytest.mark.regress 6 | def test1(): 7 | pass 8 | 9 | 10 | @pytest.mark.smoke 11 | def test2(): 12 | pass 13 | 14 | 15 | @pytest.mark.regress 16 | def test3(): 17 | pass -------------------------------------------------------------------------------- /lection01 - Intro/code/tests/test_params.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_negative(): 5 | errors = [] 6 | for i in range(10): 7 | try: 8 | assert i % 2 == 0 9 | except AssertionError: 10 | errors.append(f'{i} not even') 11 | assert not errors 12 | 13 | 14 | @pytest.mark.parametrize('i', list(range(10))) 15 | def test_even(i): 16 | """ 17 | :param i: range of integers 18 | Parametrized test which checks that number if even. 19 | """ 20 | assert i % 2 == 0 21 | 22 | 23 | @pytest.mark.parametrize('i, j', [('user', 'test'), ('key', 'value')], ids=['User', 'Key']) 24 | def test_parametrize(i, j): 25 | """ 26 | :param i: some string 27 | :param j: and another some string 28 | Parametrized test which represent parametrize with 2 values and additional IDS in tests naming 29 | """ 30 | assert i in ['user', 'key'] 31 | assert isinstance(j, str) 32 | 33 | -------------------------------------------------------------------------------- /lection01 - Intro/git_commands.txt: -------------------------------------------------------------------------------- 1 | Basics 2 | git help : get help for a git command 3 | git init: creates a new git repo, with data stored in the .git directory 4 | git status: tells you what’s going on 5 | git add : adds files to staging area 6 | git commit: creates a new commit 7 | Write good commit messages! 8 | Even more reasons to write good commit messages! 9 | git log: shows a flattened log of history 10 | git log --all --graph --decorate: visualizes history as a DAG 11 | git diff : show changes you made relative to the staging area 12 | git diff : shows differences in a file between snapshots 13 | git checkout : updates HEAD and current branch 14 | Branching and merging 15 | git branch: shows branches 16 | git branch : creates a branch 17 | git checkout -b : creates a branch and switches to it 18 | same as git branch ; git checkout 19 | git merge : merges into current branch 20 | git mergetool: use a fancy tool to help resolve merge conflicts 21 | git rebase: rebase set of patches onto a new base 22 | Remotes 23 | git remote: list remotes 24 | git remote add : add a remote 25 | git push :: send objects to remote, and update remote reference 26 | git branch --set-upstream-to=/: set up correspondence between local and remote branch 27 | git fetch: retrieve objects/references from a remote 28 | git pull: same as git fetch; git merge 29 | git clone: download repository from remote 30 | Undo 31 | git commit --amend: edit a commit’s contents/message 32 | git reset HEAD : unstage a file 33 | git checkout -- : discard changes 34 | Advanced Git 35 | git config: Git is highly customizable 36 | git clone --depth=1: shallow clone, without entire version history 37 | git add -p: interactive staging 38 | git rebase -i: interactive rebasing 39 | git blame: show who last edited which line 40 | git stash: temporarily remove modifications to working directory 41 | git bisect: binary search history (e.g. for regressions) 42 | .gitignore: specify intentionally untracked files to ignore -------------------------------------------------------------------------------- /lection03 - Selenium WebDriver/3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection03 - Selenium WebDriver/3.pdf -------------------------------------------------------------------------------- /lection04-SeleniumWebDriver/HW1.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №1: Базовые навыки работы с Selenium 2 | 3 | #### Цель домашнего задания 4 | 5 | * Изучить минимальный набор знаний и навыков для работы с Selenium. 6 | * Научиться искать элементы с помощью Selenium и проводить с ними простые действия. 7 | 8 | #### Зависимости 9 | 10 | Вы должны выполнить соответствующую инструкцию по сдаче домашних заданий (есть в папке homework_rules). 11 | 12 | #### Задача 13 | * Тестирование портала https://target-sandbox.my.com/ 14 | * Настроить окружение для запуска UI тестов, UI тесты должны запускаться через марк -m UI 15 | * В качестве браузера используем Google Chrome версии **105.0.5195.19**. Устанавливаться драйвер должен через webdriver_manager. 16 | * Необходимо передать в качестве входного аргумента pytest (в метод pytest_addoption) значение ```parser.addoption('--headless', action='store_true')``` 17 | 18 | 19 | Также добавьте его чтение в фикстуре config, возвращайте значение переменной из этой фикстуры, либо воспользуйтесь значением request.config.option.headless. 20 | 21 | Указанный выше код будет в примерах к лекции. Все это нужно для корректной работы GitHub Actions, о котором указано в инструкции к домашкам. 22 | 23 | 24 | Разбалловка: 25 | * Написать тест на логин (0.5 балла). 26 | * Написать тест на логаут (0.5 балла) 27 | * Написать два (или более) негативных теста на авторизацию (1 балл). 28 | * Написать тест на редактирование контактной информации в профиле https://target-sandbox.my.com/profile/contacts - 1 балл 29 | * Написать параметризованный тест на переход на страницы портала через кнопки Аудитории/Баланс/Статистика/PRO/Профиль/Инструменты/Помощь в шапке меню (на любые 2). 30 | Каждый из этих тестов должен переходить по выбранным меню и проверять, что был выполнен переход именно на выбранную страницу - 1 балл (0.5 за параметризацию, 0.5 за корректно проходящий тест). 31 | Проверка может осуществляться по наличию какого-то уникального для страницы локатора. 32 | 33 | 34 | 35 | #### Советы 36 | * Тесты *НЕ* должны быть зависимыми. Все тесты должны проходить. 37 | * Локаторы должны быть простыми и понятными. Автосгенерированные локаторы не принимаются. Плохие локаторы могут привести к снижению оценки. 38 | Локаторы не должны быть языко-зависимыми, т.к. на портале есть как русскоязычная, так и англоязычная версия. Не нужно цепляться к локаторам, которые содержат в себе какие-то слова (текст в кнопках и т.д.). 39 | * В каждом тесте придётся логиниться, это нормально, так что не забывайте про вынос части логики в отдельные методы или фикстуры. 40 | 41 | #### Срок сдачи ДЗ 42 | 43 | До 05 октября 23-59 44 | -------------------------------------------------------------------------------- /lection04-SeleniumWebDriver/code/base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ui.locators import basic_locators 3 | from selenium.common.exceptions import StaleElementReferenceException 4 | 5 | CLICK_RETRY = 3 6 | 7 | 8 | class BaseCase: 9 | driver = None 10 | 11 | @pytest.fixture(scope='function', autouse=True) 12 | def setup(self, driver): 13 | self.driver = driver 14 | 15 | def find(self, by, what): 16 | return self.driver.find_element(by, what) 17 | 18 | def search(self, query): 19 | search = self.find(*basic_locators.QUERY_LOCATOR) 20 | search.clear() 21 | search.send_keys(query) 22 | go_button = self.find(*basic_locators.GO_BUTTON) 23 | go_button.click() 24 | # search.send_keys(Keys.ENTER) 25 | 26 | def click(self, locator): 27 | for i in range(CLICK_RETRY): 28 | try: 29 | elem = self.find(*locator) 30 | 31 | if i < 2: 32 | self.driver.refresh() 33 | elem.click() 34 | return 35 | except StaleElementReferenceException: 36 | if i == CLICK_RETRY - 1: 37 | raise 38 | -------------------------------------------------------------------------------- /lection04-SeleniumWebDriver/code/test.py: -------------------------------------------------------------------------------- 1 | from base import BaseCase 2 | import pytest 3 | from ui.locators import basic_locators 4 | 5 | 6 | class TestOne(BaseCase): 7 | @pytest.mark.parametrize( 8 | 'query', 9 | [ 10 | pytest.param( 11 | 'pycon' 12 | ), 13 | pytest.param( 14 | 'python' 15 | ) 16 | ] 17 | ) 18 | @pytest.mark.skip("SKIP") 19 | def test(self, query): 20 | assert "Python" in self.driver.title 21 | self.search(query=query) 22 | assert "No results found." not in self.driver.page_source 23 | 24 | def test_page_change(self): 25 | self.click(locator=basic_locators.GO_BUTTON) 26 | -------------------------------------------------------------------------------- /lection04-SeleniumWebDriver/code/ui/locators/basic_locators.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | QUERY_LOCATOR = (By.NAME, 'q') 4 | GO_BUTTON = (By.XPATH, '//*[@id="submit"]') 5 | -------------------------------------------------------------------------------- /lection04-SeleniumWebDriver/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | from webdriver_manager.chrome import ChromeDriverManager 4 | 5 | 6 | def pytest_addoption(parser): 7 | parser.addoption("--browser", default="chrome") 8 | parser.addoption("--url", default="https://www.python.org/") 9 | 10 | 11 | @pytest.fixture() 12 | def config(request): 13 | browser = request.config.getoption("--browser") 14 | url = request.config.getoption("--url") 15 | return {"browser": browser, "url": url} 16 | 17 | 18 | @pytest.fixture(scope='function') 19 | def driver(config): 20 | 21 | browser = config["browser"] 22 | url = config["url"] 23 | 24 | if browser == "chrome": 25 | driver = webdriver.Chrome(executable_path=ChromeDriverManager().install()) 26 | elif browser == "firefox": 27 | pass 28 | else: 29 | raise RuntimeError(f'Unsupported browser: "{browser}"') 30 | 31 | driver.get(url) 32 | driver.maximize_window() 33 | yield driver 34 | driver.quit() 35 | -------------------------------------------------------------------------------- /lection05-PageObject/code/base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pytest.fixtures import FixtureRequest 3 | from ui.pages.base_page import BasePage 4 | from ui.pages.main_page import MainPage 5 | 6 | CLICK_RETRY = 3 7 | 8 | 9 | class BaseCase: 10 | driver = None 11 | 12 | @pytest.fixture(scope='function', autouse=True) 13 | def setup(self, driver, config, request: FixtureRequest): 14 | self.driver = driver 15 | self.config = config 16 | 17 | self.base_page:BasePage = (request.getfixturevalue('base_page')) 18 | self.main_page:MainPage = (request.getfixturevalue('main_page')) 19 | -------------------------------------------------------------------------------- /lection05-PageObject/code/conftest.py: -------------------------------------------------------------------------------- 1 | from ui.fixtures import * 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption('--browser', default='chrome') 6 | parser.addoption('--url', default='https://www.python.org') 7 | 8 | 9 | @pytest.fixture() 10 | def config(request): 11 | browser = request.config.getoption('--browser') 12 | url = request.config.getoption('--url') 13 | return {'browser': browser, 'url': url} 14 | -------------------------------------------------------------------------------- /lection05-PageObject/code/test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | from base import BaseCase 4 | from ui.locators import basic_locators 5 | from selenium.webdriver.common.keys import Keys 6 | from selenium.webdriver.common.by import By 7 | 8 | 9 | class TestExample(BaseCase): 10 | 11 | @pytest.mark.parametrize( 12 | 'query', 13 | [ 14 | pytest.param( 15 | 'pycon' 16 | ), 17 | pytest.param( 18 | 'python' 19 | ), 20 | ], 21 | ) 22 | @pytest.mark.skip('skip') 23 | def test_search(self, query): 24 | self.base_page.search(query) 25 | assert 'No results found' not in self.driver.page_source 26 | 27 | @pytest.mark.skip('skip') 28 | def test_negative_search(self): 29 | self.base_page.search('adasdasdasdasdasda') 30 | assert 'No results found' not in self.driver.page_source 31 | 32 | @pytest.mark.skip('skip') 33 | def test_page_change(self): 34 | self.base_page.click( 35 | basic_locators.BasePageLocators.GO_BUTTON_LOCATOR, timeout=10 36 | ) 37 | 38 | @pytest.mark.skip('skip') 39 | def test_carousel(self): 40 | self.main_page.click( 41 | basic_locators.MainPageLocators.COMPREHENSIONS, timeout=15 42 | ) 43 | 44 | @pytest.mark.skip('skip') 45 | def test_iframe(self): 46 | self.main_page.click(self.main_page.locators.START_SHELL) 47 | time.sleep(15) 48 | iframe_first = self.main_page.find((By.XPATH, '//iframe')) 49 | self.driver.switch_to.frame(iframe_first) 50 | iframe_second = self.main_page.find((By.ID, 'id_console')) 51 | self.driver.switch_to.frame(iframe_second) 52 | iframe = self.main_page.find((By.XPATH, '//iframe')) 53 | self.driver.switch_to.frame(iframe) 54 | console = self.main_page.find(self.main_page.locators.PYTHON_CONSOLE) 55 | console.send_keys('assert 1 == 0') 56 | console.send_keys(Keys.ENTER) 57 | time.sleep(10) 58 | self.driver.switch_to.default_content() 59 | -------------------------------------------------------------------------------- /lection05-PageObject/code/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | from webdriver_manager.chrome import ChromeDriverManager 4 | from webdriver_manager.firefox import GeckoDriverManager 5 | from ui.pages.base_page import BasePage 6 | from ui.pages.main_page import MainPage 7 | 8 | 9 | @pytest.fixture() 10 | def driver(config): 11 | browser = config['browser'] 12 | url = config['url'] 13 | if browser == 'chrome': 14 | driver = webdriver.Chrome(executable_path=ChromeDriverManager().install()) 15 | elif browser == 'firefox': 16 | driver = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 17 | else: 18 | raise RuntimeError(f'Unsupported browser: "{browser}"') 19 | driver.get(url) 20 | driver.maximize_window() 21 | yield driver 22 | driver.quit() 23 | 24 | 25 | @pytest.fixture 26 | def base_page(driver): 27 | return BasePage(driver=driver) 28 | 29 | 30 | @pytest.fixture 31 | def main_page(driver): 32 | return MainPage(driver=driver) 33 | -------------------------------------------------------------------------------- /lection05-PageObject/code/ui/locators/basic_locators.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | QUERY_LOCATOR = (By.NAME, 'q') 6 | QUERY_LOCATOR_ID = (By.ID, 'id-search-field') 7 | GO_BUTTON_LOCATOR = (By.XPATH, '//*[@id="submit"]') 8 | START_SHELL = (By.ID, 'start-shell') 9 | PYTHON_CONSOLE = (By.ID, 'hterm:row-nodes') 10 | 11 | 12 | class MainPageLocators(BasePageLocators): 13 | COMPREHENSIONS = ( 14 | By.XPATH, 15 | '//code/span[@class="comment" and contains(text(), "comprehensions")]' 16 | ) 17 | -------------------------------------------------------------------------------- /lection05-PageObject/code/ui/pages/base_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.remote.webelement import WebElement 2 | from ui.locators import basic_locators 3 | from selenium.webdriver.support.wait import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as EC 5 | 6 | 7 | class BasePage(object): 8 | 9 | locators = basic_locators.BasePageLocators() 10 | 11 | def __init__(self, driver): 12 | self.driver = driver 13 | 14 | def wait(self, timeout=None): 15 | if timeout is None: 16 | timeout = 5 17 | return WebDriverWait(self.driver, timeout=timeout) 18 | 19 | def find(self, locator, timeout=None): 20 | return self.wait(timeout).until(EC.presence_of_element_located(locator)) 21 | 22 | def search(self, query): 23 | elem = self.find(self.locators.QUERY_LOCATOR_ID) 24 | elem.send_keys(query) 25 | go_button = self.find(self.locators.GO_BUTTON_LOCATOR) 26 | go_button.click() 27 | 28 | def click(self, locator, timeout=None) -> WebElement: 29 | self.find(locator, timeout=timeout) 30 | elem = self.wait(timeout).until(EC.element_to_be_clickable(locator)) 31 | elem.click() 32 | -------------------------------------------------------------------------------- /lection05-PageObject/code/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from ui.locators import basic_locators 2 | from ui.pages.base_page import BasePage 3 | 4 | 5 | class MainPage(BasePage): 6 | 7 | locators = basic_locators.MainPageLocators() 8 | -------------------------------------------------------------------------------- /lection05-PageObject/lection05.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection05-PageObject/lection05.pdf -------------------------------------------------------------------------------- /lection06-Advanced/code/base.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | import pytest 4 | from _pytest.fixtures import FixtureRequest 5 | from ui.pages.base_page import BasePage 6 | from ui.pages.main_page import MainPage 7 | 8 | CLICK_RETRY = 3 9 | 10 | 11 | class BaseCase: 12 | driver = None 13 | 14 | @contextmanager 15 | def switch_to_window(self, current, close=False): 16 | for w in self.driver.window_handles: 17 | if w != current: 18 | self.driver.switch_to.window(w) 19 | break 20 | yield 21 | if close: 22 | self.driver.close() 23 | self.driver.switch_to.window(current) 24 | 25 | @pytest.fixture(scope='function', autouse=True) 26 | def setup(self, driver, config, request: FixtureRequest): 27 | self.driver = driver 28 | self.config = config 29 | 30 | self.base_page: BasePage = (request.getfixturevalue('base_page')) 31 | self.main_page: MainPage = (request.getfixturevalue('main_page')) 32 | -------------------------------------------------------------------------------- /lection06-Advanced/code/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | from ui.fixtures import * 6 | 7 | 8 | def pytest_addoption(parser): 9 | parser.addoption('--browser', default='chrome') 10 | parser.addoption('--url', default='https://www.python.org') 11 | 12 | 13 | @pytest.fixture(scope='session') 14 | def repo_root(): 15 | return os.path.abspath(os.path.join(__file__, os.path.pardir)) 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | def base_temp_dir(): 20 | if sys.platform.startswith('win'): 21 | base_dir = 'C:\\tests' 22 | else: 23 | base_dir = '/tmp/tests' 24 | if os.path.exists(base_dir): 25 | shutil.rmtree(base_dir) 26 | return base_dir 27 | 28 | 29 | @pytest.fixture(scope='function') 30 | def temp_dir(base_temp_dir, request): 31 | test_dir = os.path.join(base_temp_dir, request._pyfuncitem.nodeid) 32 | os.makedirs(test_dir) 33 | return test_dir 34 | 35 | 36 | @pytest.fixture(scope='session') 37 | def config(request): 38 | browser = request.config.getoption('--browser') 39 | url = request.config.getoption('--url') 40 | return {'browser': browser, 'url': url} 41 | -------------------------------------------------------------------------------- /lection06-Advanced/code/files/userdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection06-Advanced/code/files/userdata -------------------------------------------------------------------------------- /lection06-Advanced/code/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import time 5 | 6 | from selenium.webdriver import ActionChains 7 | 8 | from base import BaseCase 9 | from ui.locators import basic_locators 10 | from selenium.webdriver.common.keys import Keys 11 | from selenium.webdriver.common.by import By 12 | 13 | 14 | class TestExample(BaseCase): 15 | 16 | @pytest.mark.parametrize( 17 | 'query', 18 | [ 19 | pytest.param( 20 | 'pycon' 21 | ), 22 | pytest.param( 23 | 'python' 24 | ), 25 | ], 26 | ) 27 | @pytest.mark.skip('skip') 28 | def test_search(self, query): 29 | self.base_page.search(query) 30 | assert 'No results found' not in self.driver.page_source 31 | 32 | @pytest.mark.skip('skip') 33 | def test_negative_search(self): 34 | self.base_page.search('adasdasdasdasdasda') 35 | assert 'No results found' not in self.driver.page_source 36 | 37 | @pytest.mark.skip('skip') 38 | def test_page_change(self): 39 | self.base_page.click( 40 | basic_locators.BasePageLocators.GO_BUTTON_LOCATOR, timeout=10 41 | ) 42 | 43 | @pytest.mark.skip('skip') 44 | def test_carousel(self): 45 | self.main_page.click( 46 | basic_locators.MainPageLocators.COMPREHENSIONS, timeout=15 47 | ) 48 | 49 | @pytest.mark.skip('skip') 50 | def test_iframe(self): 51 | self.main_page.click(self.main_page.locators.START_SHELL) 52 | time.sleep(15) 53 | iframe_first = self.main_page.find((By.XPATH, '//iframe')) 54 | self.driver.switch_to.frame(iframe_first) 55 | iframe_second = self.main_page.find((By.ID, 'id_console')) 56 | self.driver.switch_to.frame(iframe_second) 57 | iframe = self.main_page.find((By.XPATH, '//iframe')) 58 | self.driver.switch_to.frame(iframe) 59 | console = self.main_page.find(self.main_page.locators.PYTHON_CONSOLE) 60 | console.send_keys('assert 1 == 0') 61 | console.send_keys(Keys.ENTER) 62 | time.sleep(10) 63 | self.driver.switch_to.default_content() 64 | 65 | @pytest.mark.skip('skip') 66 | def test_new_tab(self): 67 | current_window = self.driver.current_window_handle 68 | 69 | news = self.main_page.find((By.ID, 'news')) 70 | ActionChains(self.driver).key_down(Keys.COMMAND).click(news).key_up(Keys.COMMAND).perform() 71 | time.sleep(5) 72 | 73 | with self.switch_to_window(current=current_window, close=True): 74 | assert self.driver.current_url == 'https://www.python.org/blogs/' 75 | time.sleep(3) 76 | time.sleep(3) 77 | 78 | @pytest.mark.skip('skip') 79 | def test_events(self): 80 | events_page = self.main_page.go_to_events_page() 81 | time.sleep(3) 82 | 83 | @pytest.mark.skip('skip') 84 | def test_relative(self): 85 | intro = self.main_page.find((By.CSS_SELECTOR, 'div.introduction')) 86 | learn_more = intro.find_element(*self.main_page.locators.READ_MORE) 87 | assert learn_more.get_attribute('href') == self.driver.current_url + 'doc/' 88 | 89 | 90 | class TestLoad(BaseCase): 91 | 92 | @pytest.mark.skip('skip') 93 | def test_download(self): 94 | self.driver.get('https://www.python.org/downloads/release/python-3107/') 95 | time.sleep(5) 96 | self.main_page.click((By.XPATH, '//a[@href="https://www.python.org/ftp/python/3.10.7/python-3.10.7-embed-amd64.zip"]')) 97 | time.sleep(10) 98 | 99 | @pytest.fixture() 100 | def file_path(self, repo_root): 101 | return os.path.join(repo_root, 'files', 'userdata') 102 | 103 | @pytest.mark.skip('skip') 104 | def test_upload(self, file_path): 105 | self.driver.get('https://ps.uci.edu/~franklin/doc/file_upload.html') 106 | input = (By.NAME, 'userfile') 107 | time.sleep(5) 108 | self.main_page.find(input).send_keys(file_path) 109 | time.sleep(5) 110 | 111 | 112 | @pytest.mark.skip('skip') 113 | def test_check_all_drivers(all_drivers): 114 | time.sleep(3) 115 | -------------------------------------------------------------------------------- /lection06-Advanced/code/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | from selenium.webdriver.chrome.options import Options 4 | from webdriver_manager.chrome import ChromeDriverManager 5 | from webdriver_manager.firefox import GeckoDriverManager 6 | from ui.pages.base_page import BasePage 7 | from ui.pages.main_page import MainPage 8 | from ui.pages.events_page import EventsPage 9 | 10 | 11 | @pytest.fixture() 12 | def driver(config, temp_dir): 13 | browser = config['browser'] 14 | url = config['url'] 15 | options = Options() 16 | options.add_experimental_option("prefs", {"download.default_directory": temp_dir}) 17 | if browser == 'chrome': 18 | driver = webdriver.Chrome(executable_path=ChromeDriverManager().install(), options=options) 19 | elif browser == 'firefox': 20 | driver = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 21 | else: 22 | raise RuntimeError(f'Unsupported browser: "{browser}"') 23 | driver.get(url) 24 | driver.maximize_window() 25 | yield driver 26 | driver.quit() 27 | 28 | 29 | def get_driver(browser_name): 30 | if browser_name == 'chrome': 31 | browser = webdriver.Chrome(executable_path=ChromeDriverManager().install()) 32 | elif browser_name == 'firefox': 33 | browser = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 34 | else: 35 | raise RuntimeError(f'Unsupported browser: "{browser_name}"') 36 | browser.maximize_window() 37 | return browser 38 | 39 | 40 | @pytest.fixture(scope='session', params=['chrome', 'firefox']) 41 | def all_drivers(config, request): 42 | url = config['url'] 43 | browser = get_driver(request.param) 44 | browser.get(url) 45 | yield browser 46 | browser.quit() 47 | 48 | 49 | @pytest.fixture 50 | def base_page(driver): 51 | return BasePage(driver=driver) 52 | 53 | 54 | @pytest.fixture 55 | def main_page(driver): 56 | return MainPage(driver=driver) 57 | 58 | 59 | @pytest.fixture 60 | def events_page(driver): 61 | return EventsPage(driver=driver) 62 | -------------------------------------------------------------------------------- /lection06-Advanced/code/ui/locators/basic_locators.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | QUERY_LOCATOR = (By.NAME, 'q') 6 | QUERY_LOCATOR_ID = (By.ID, 'id-search-field') 7 | GO_BUTTON_LOCATOR = (By.XPATH, '//*[@id="submit"]') 8 | START_SHELL = (By.ID, 'start-shell') 9 | PYTHON_CONSOLE = (By.ID, 'hterm:row-nodes') 10 | 11 | 12 | class MainPageLocators(BasePageLocators): 13 | COMPREHENSIONS = ( 14 | By.XPATH, 15 | '//code/span[@class="comment" and contains(text(), "comprehensions")]' 16 | ) 17 | EVENTS = (By.ID, 'events') 18 | READ_MORE = (By.CSS_SELECTOR, 'a.readmore') 19 | 20 | 21 | class EventsPageLocators(BasePageLocators): 22 | pass 23 | -------------------------------------------------------------------------------- /lection06-Advanced/code/ui/pages/base_page.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from selenium.webdriver.remote.webelement import WebElement 4 | from ui.locators import basic_locators 5 | from selenium.webdriver.support.wait import WebDriverWait 6 | from selenium.webdriver.support import expected_conditions as EC 7 | 8 | 9 | class PageNotOpenedExeption(Exception): 10 | pass 11 | 12 | 13 | class BasePage(object): 14 | 15 | locators = basic_locators.BasePageLocators() 16 | url = 'https://www.python.org/' 17 | 18 | def is_opened(self, timeout=15): 19 | started = time.time() 20 | while time.time() - started < timeout: 21 | if self.driver.current_url == self.url: 22 | return True 23 | raise PageNotOpenedExeption(f'{self.url} did not open in {timeout} sec, current url {self.driver.current_url}') 24 | 25 | def __init__(self, driver): 26 | self.driver = driver 27 | self.is_opened() 28 | 29 | def wait(self, timeout=None): 30 | if timeout is None: 31 | timeout = 5 32 | return WebDriverWait(self.driver, timeout=timeout) 33 | 34 | def find(self, locator, timeout=None): 35 | return self.wait(timeout).until(EC.presence_of_element_located(locator)) 36 | 37 | def search(self, query): 38 | elem = self.find(self.locators.QUERY_LOCATOR_ID) 39 | elem.send_keys(query) 40 | go_button = self.find(self.locators.GO_BUTTON_LOCATOR) 41 | go_button.click() 42 | 43 | def click(self, locator, timeout=None) -> WebElement: 44 | self.find(locator, timeout=timeout) 45 | elem = self.wait(timeout).until(EC.element_to_be_clickable(locator)) 46 | elem.click() 47 | -------------------------------------------------------------------------------- /lection06-Advanced/code/ui/pages/events_page.py: -------------------------------------------------------------------------------- 1 | from ui.locators import basic_locators 2 | from ui.pages.base_page import BasePage 3 | 4 | 5 | class EventsPage(BasePage): 6 | 7 | locators = basic_locators.EventsPageLocators() 8 | url = 'https://www.python.org/events/' 9 | -------------------------------------------------------------------------------- /lection06-Advanced/code/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from ui.locators import basic_locators 2 | from ui.pages.base_page import BasePage 3 | from ui.pages.events_page import EventsPage 4 | 5 | 6 | class MainPage(BasePage): 7 | 8 | locators = basic_locators.MainPageLocators() 9 | 10 | def go_to_events_page(self): 11 | events_button = self.find(self.locators.EVENTS) 12 | events_button.click() 13 | return EventsPage(self.driver) 14 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import contextmanager 3 | 4 | import allure 5 | import pytest 6 | from _pytest.fixtures import FixtureRequest 7 | from ui.pages.base_page import BasePage 8 | from ui.pages.main_page import MainPage 9 | 10 | CLICK_RETRY = 3 11 | 12 | 13 | class BaseCase: 14 | driver = None 15 | 16 | @contextmanager 17 | def switch_to_window(self, current, close=False): 18 | for w in self.driver.window_handles: 19 | if w != current: 20 | self.driver.switch_to.window(w) 21 | break 22 | yield 23 | if close: 24 | self.driver.close() 25 | self.driver.switch_to.window(current) 26 | 27 | @pytest.fixture(scope='function', autouse=True) 28 | def ui_report(self, driver, request, temp_dir): 29 | failed_test_count = request.session.testsfailed 30 | yield 31 | if request.session.testsfailed > failed_test_count: 32 | browser_logs = os.path.join(temp_dir, 'browser.log') 33 | with open(browser_logs, 'w') as f: 34 | for i in driver.get_log('browser'): 35 | f.write(f"{i['level']} - {i['source']}\n{i['message']}\n") 36 | screenshot_path = os.path.join(temp_dir, 'failed.png') 37 | self.driver.save_screenshot(filename=screenshot_path) 38 | allure.attach.file(screenshot_path, 'failed.png', allure.attachment_type.PNG) 39 | with open(browser_logs, 'r') as f: 40 | allure.attach(f.read(), 'test.log', allure.attachment_type.TEXT) 41 | 42 | @pytest.fixture(scope='function', autouse=True) 43 | def setup(self, driver, config, logger, request: FixtureRequest): 44 | self.driver = driver 45 | self.config = config 46 | self.logger = logger 47 | 48 | self.base_page: BasePage = (request.getfixturevalue('base_page')) 49 | self.main_page: MainPage = (request.getfixturevalue('main_page')) 50 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ui.fixtures import * 4 | 5 | 6 | def pytest_addoption(parser): 7 | parser.addoption('--browser', default='chrome') 8 | parser.addoption('--url', default='https://www.python.org') 9 | parser.addoption('--debug_log', action='store_true') 10 | parser.addoption('--selenoid', action='store_true') 11 | parser.addoption('--vnc', action='store_true') 12 | 13 | 14 | @pytest.fixture(scope='session') 15 | def repo_root(): 16 | return os.path.abspath(os.path.join(__file__, os.path.pardir)) 17 | 18 | 19 | @pytest.fixture(scope='session') 20 | def base_temp_dir(): 21 | if sys.platform.startswith('win'): 22 | base_dir = 'C:\\tests' 23 | else: 24 | base_dir = '/tmp/tests' 25 | if os.path.exists(base_dir): 26 | shutil.rmtree(base_dir) 27 | return base_dir 28 | 29 | 30 | @pytest.fixture(scope='function') 31 | def temp_dir(request): 32 | test_dir = os.path.join(request.config.base_temp_dir, request._pyfuncitem.nodeid) 33 | os.makedirs(test_dir) 34 | return test_dir 35 | 36 | 37 | @pytest.fixture(scope='session') 38 | def config(request): 39 | browser = request.config.getoption('--browser') 40 | url = request.config.getoption('--url') 41 | debug_log = request.config.getoption('--debug_log') 42 | if request.config.getoption('--selenoid'): 43 | if request.config.getoption('--vnc'): 44 | vnc = True 45 | else: 46 | vnc = False 47 | selenoid = 'http://127.0.0.1:4444/wd/hub' 48 | else: 49 | selenoid = None 50 | vnc = False 51 | 52 | return { 53 | 'browser': browser, 54 | 'url': url, 55 | 'debug_log': debug_log, 56 | 'selenoid': selenoid, 57 | 'vnc': vnc, 58 | } 59 | 60 | 61 | @pytest.fixture(scope='function') 62 | def logger(temp_dir, config): 63 | log_formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s') 64 | log_file = os.path.join(temp_dir, 'test.log') 65 | log_level = logging.DEBUG if config['debug_log'] else logging.INFO 66 | 67 | file_handler = logging.FileHandler(log_file, 'w') 68 | file_handler.setFormatter(log_formatter) 69 | file_handler.setLevel(log_level) 70 | 71 | log = logging.getLogger('test') 72 | log.propagate = False 73 | log.setLevel(log_level) 74 | log.handlers.clear() 75 | log.addHandler(file_handler) 76 | 77 | yield log 78 | 79 | for handler in log.handlers: 80 | handler.close() 81 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/files/userdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection07-ReportRemote/code/files/userdata -------------------------------------------------------------------------------- /lection07-ReportRemote/code/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import allure 4 | import pytest 5 | import time 6 | 7 | from selenium.webdriver import ActionChains 8 | 9 | from base import BaseCase 10 | from ui.locators import basic_locators 11 | from selenium.webdriver.common.keys import Keys 12 | from selenium.webdriver.common.by import By 13 | 14 | 15 | class TestExample(BaseCase): 16 | 17 | @pytest.mark.parametrize( 18 | 'query', 19 | [ 20 | pytest.param( 21 | 'pycon' 22 | ), 23 | pytest.param( 24 | 'python' 25 | ), 26 | ], 27 | ) 28 | @pytest.mark.skip('skip') 29 | def test_search(self, query): 30 | self.base_page.search(query) 31 | assert 'No results found' not in self.driver.page_source 32 | 33 | @pytest.mark.skip('skip') 34 | def test_negative_search(self): 35 | time.sleep(5) 36 | self.base_page.search('adasdasdasdasdasda') 37 | time.sleep(5) 38 | assert 'No results found' in self.driver.page_source 39 | 40 | @pytest.mark.skip('skip') 41 | def test_page_change(self): 42 | self.base_page.click( 43 | basic_locators.BasePageLocators.GO_BUTTON_LOCATOR, timeout=10 44 | ) 45 | 46 | @pytest.mark.skip('skip') 47 | def test_carousel(self): 48 | self.main_page.click( 49 | basic_locators.MainPageLocators.COMPREHENSIONS, timeout=15 50 | ) 51 | 52 | @pytest.mark.skip('skip') 53 | def test_iframe(self): 54 | self.main_page.click(self.main_page.locators.START_SHELL) 55 | time.sleep(15) 56 | iframe_first = self.main_page.find((By.XPATH, '//iframe')) 57 | self.driver.switch_to.frame(iframe_first) 58 | iframe_second = self.main_page.find((By.ID, 'id_console')) 59 | self.driver.switch_to.frame(iframe_second) 60 | iframe = self.main_page.find((By.XPATH, '//iframe')) 61 | self.driver.switch_to.frame(iframe) 62 | console = self.main_page.find(self.main_page.locators.PYTHON_CONSOLE) 63 | console.send_keys('assert 1 == 0') 64 | console.send_keys(Keys.ENTER) 65 | time.sleep(10) 66 | self.driver.switch_to.default_content() 67 | 68 | @pytest.mark.skip('skip') 69 | def test_new_tab(self): 70 | current_window = self.driver.current_window_handle 71 | 72 | news = self.main_page.find((By.ID, 'news')) 73 | ActionChains(self.driver).key_down(Keys.CONTROL).click(news).key_up(Keys.CONTROL).perform() 74 | time.sleep(5) 75 | 76 | with self.switch_to_window(current=current_window, close=True): 77 | assert self.driver.current_url == 'https://www.python.org/blogs/' 78 | time.sleep(3) 79 | time.sleep(3) 80 | 81 | @pytest.mark.skip('skip') 82 | def test_events(self): 83 | time.sleep(5) 84 | with allure.step('Going to events page'): 85 | events_page = self.main_page.go_to_events_page() 86 | time.sleep(5) 87 | with allure.step('asserting ...'): 88 | assert 1 == 1 89 | 90 | @pytest.mark.skip('skip') 91 | def test_relative(self): 92 | intro = self.main_page.find((By.CSS_SELECTOR, 'div.introduction')) 93 | learn_more = intro.find_element(*self.main_page.locators.READ_MORE) 94 | assert learn_more.get_attribute('href') == self.driver.current_url + 'doc/' 95 | 96 | 97 | class TestLoad(BaseCase): 98 | 99 | @pytest.mark.skip('skip') 100 | def test_download(self): 101 | self.driver.get('https://www.python.org/downloads/release/python-3100/') 102 | time.sleep(5) 103 | self.main_page.click((By.XPATH, '//a[@href="https://www.python.org/ftp/python/3.10.0/python-3.10.0-embed-win32.zip"]')) 104 | time.sleep(10) 105 | 106 | @pytest.fixture() 107 | def file_path(self, repo_root): 108 | return os.path.join(repo_root, 'files', 'userdata') 109 | 110 | @pytest.mark.skip('skip') 111 | def test_upload(self, file_path): 112 | self.driver.get('https://ps.uci.edu/~franklin/doc/file_upload.html') 113 | input = (By.NAME, 'userfile') 114 | time.sleep(5) 115 | self.main_page.find(input).send_keys(file_path) 116 | time.sleep(5) 117 | 118 | 119 | class TestFailed(BaseCase): 120 | 121 | @pytest.mark.skip('skip') 122 | def test_fail(self): 123 | self.main_page.find((By.XPATH, '12312312312312'), timeout=1) 124 | 125 | @pytest.mark.skip('skip') 126 | def test_logs_browser(self): 127 | self.driver.get('https://target.my.com/') 128 | time.sleep(3) 129 | assert 0 130 | 131 | @pytest.mark.skip('skip') 132 | @allure.step("Step 1") 133 | def test_log(self): 134 | self.logger.info('Ready to start') 135 | self.logger.info('Going to events page') 136 | events_page = self.main_page.go_to_events_page() 137 | self.logger.info('asserting') 138 | assert 1 == 1 139 | 140 | 141 | @pytest.mark.skip('skip') 142 | def test_check_all_drivers(all_drivers): 143 | time.sleep(3) 144 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/test_login.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | from _pytest.fixtures import FixtureRequest 5 | from selenium.webdriver.common.by import By 6 | 7 | from ui.fixtures import get_driver 8 | from ui.pages.base_page import BasePage 9 | 10 | 11 | class BaseCase: 12 | authorize = True 13 | 14 | @pytest.fixture(scope='function', autouse=True) 15 | def setup(self, driver, config, request: FixtureRequest, logger): 16 | self.driver = driver 17 | self.config = config 18 | self.logger = logger 19 | 20 | self.login_page = LoginPage(driver) 21 | if self.authorize: 22 | cookies = request.getfixturevalue('cookies') 23 | for cookie in cookies: 24 | self.driver.add_cookie(cookie) 25 | 26 | self.driver.refresh() 27 | self.main_page = MainPage(driver) 28 | 29 | 30 | @pytest.fixture(scope='session') 31 | def credentials(): 32 | with open('/Users/konstantin.ermakov/Documents/creds', 'r') as f: 33 | user = f.readline().strip() 34 | password = f.readline().strip() 35 | 36 | return user, password 37 | 38 | 39 | @pytest.fixture(scope='session') 40 | def cookies(credentials, config): 41 | driver = get_driver(config['browser']) 42 | driver.get(config['url']) 43 | login_page = LoginPage(driver) 44 | login_page.login(*credentials) 45 | 46 | cookies = driver.get_cookies() 47 | driver.quit() 48 | return cookies 49 | 50 | 51 | class LoginPage(BasePage): 52 | url = 'https://education.vk.company/' 53 | 54 | def login(self, user, password): 55 | self.click((By.XPATH, '/html/body/header/div/nav/button')) 56 | self.find((By.NAME, 'login')).send_keys(user) 57 | self.find((By.NAME, 'password')).send_keys(password) 58 | 59 | self.click((By.XPATH, '//html/body/div[4]/div/div[1]/form/button')) 60 | 61 | time.sleep(5) 62 | return MainPage(self.driver) 63 | 64 | 65 | class MainPage(BasePage): 66 | url = 'https://education.vk.company/feed/' 67 | 68 | 69 | class TestLogin(BaseCase): 70 | authorize = False 71 | 72 | @pytest.mark.skip("SKIP") 73 | def test_login(self, credentials): 74 | login_page = LoginPage(self.driver) 75 | login_page.login(*credentials) 76 | 77 | time.sleep(5) 78 | 79 | 80 | class TestLK(BaseCase): 81 | 82 | def test_lk1(self): 83 | time.sleep(3) 84 | 85 | def test_lk2(self): 86 | time.sleep(3) 87 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | import pytest 6 | from selenium import webdriver 7 | from selenium.webdriver.chrome.options import Options 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | from webdriver_manager.firefox import GeckoDriverManager 10 | from ui.pages.base_page import BasePage 11 | from ui.pages.main_page import MainPage 12 | 13 | 14 | def pytest_configure(config): 15 | if sys.platform.startswith('win'): 16 | base_dir = 'C:\\tests' 17 | else: 18 | base_dir = '/tmp/tests' 19 | if not hasattr(config, 'workerunput'): 20 | if os.path.exists(base_dir): 21 | shutil.rmtree(base_dir) 22 | os.makedirs(base_dir) 23 | 24 | config.base_temp_dir = base_dir 25 | 26 | 27 | @pytest.fixture() 28 | def driver(config, temp_dir): 29 | browser = config['browser'] 30 | url = config['url'] 31 | selenoid = config['selenoid'] 32 | vnc = config['vnc'] 33 | options = Options() 34 | options.add_experimental_option("prefs", {"download.default_directory": temp_dir}) 35 | if selenoid: 36 | capabilities = { 37 | 'browserName': 'chrome', 38 | 'version': '106.0', 39 | } 40 | if vnc: 41 | capabilities['enableVNC'] = True 42 | driver = webdriver.Remote( 43 | 'http://127.0.0.1:4444/wd/hub', 44 | options=options, 45 | desired_capabilities=capabilities 46 | ) 47 | elif browser == 'chrome': 48 | driver = webdriver.Chrome(executable_path=ChromeDriverManager().install(), options=options) 49 | elif browser == 'firefox': 50 | driver = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 51 | else: 52 | raise RuntimeError(f'Unsupported browser: "{browser}"') 53 | driver.get(url) 54 | driver.maximize_window() 55 | yield driver 56 | driver.quit() 57 | 58 | 59 | def get_driver(browser_name): 60 | if browser_name == 'chrome': 61 | browser = webdriver.Chrome(executable_path=ChromeDriverManager().install()) 62 | elif browser_name == 'firefox': 63 | browser = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 64 | else: 65 | raise RuntimeError(f'Unsupported browser: "{browser_name}"') 66 | browser.maximize_window() 67 | return browser 68 | 69 | 70 | @pytest.fixture(scope='session', params=['chrome', 'firefox']) 71 | def all_drivers(config, request): 72 | url = config['url'] 73 | browser = get_driver(request.param) 74 | browser.get(url) 75 | yield browser 76 | browser.quit() 77 | 78 | 79 | @pytest.fixture 80 | def base_page(driver): 81 | return BasePage(driver=driver) 82 | 83 | 84 | @pytest.fixture 85 | def main_page(driver): 86 | return MainPage(driver=driver) 87 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/ui/locators/basic_locators.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | QUERY_LOCATOR = (By.NAME, 'q') 6 | QUERY_LOCATOR_ID = (By.ID, 'id-search-field') 7 | GO_BUTTON_LOCATOR = (By.XPATH, '//*[@id="submit"]') 8 | START_SHELL = (By.ID, 'start-shell') 9 | PYTHON_CONSOLE = (By.ID, 'hterm:row-nodes') 10 | 11 | 12 | class MainPageLocators(BasePageLocators): 13 | COMPREHENSIONS = ( 14 | By.XPATH, 15 | '//code/span[@class="comment" and contains(text(), "comprehensions")]' 16 | ) 17 | EVENTS = (By.ID, 'events') 18 | READ_MORE = (By.CSS_SELECTOR, 'a.readmore') 19 | 20 | 21 | class EventsPageLocators(BasePageLocators): 22 | pass 23 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/ui/pages/base_page.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import allure 4 | from selenium.webdriver.remote.webelement import WebElement 5 | from ui.locators import basic_locators 6 | from selenium.webdriver.support.wait import WebDriverWait 7 | from selenium.webdriver.support import expected_conditions as EC 8 | 9 | 10 | class PageNotOpenedExeption(Exception): 11 | pass 12 | 13 | 14 | class BasePage(object): 15 | 16 | locators = basic_locators.BasePageLocators() 17 | url = 'https://www.python.org/' 18 | 19 | def is_opened(self, timeout=15): 20 | started = time.time() 21 | while time.time() - started < timeout: 22 | if self.driver.current_url == self.url: 23 | return True 24 | raise PageNotOpenedExeption(f'{self.url} did not open in {timeout} sec, current url {self.driver.current_url}') 25 | 26 | def __init__(self, driver): 27 | self.driver = driver 28 | self.is_opened() 29 | 30 | def wait(self, timeout=None): 31 | if timeout is None: 32 | timeout = 5 33 | return WebDriverWait(self.driver, timeout=timeout) 34 | 35 | def find(self, locator, timeout=None): 36 | return self.wait(timeout).until(EC.presence_of_element_located(locator)) 37 | 38 | def search(self, query): 39 | elem = self.find(self.locators.QUERY_LOCATOR_ID) 40 | elem.send_keys(query) 41 | go_button = self.find(self.locators.GO_BUTTON_LOCATOR) 42 | go_button.click() 43 | 44 | @allure.step('Click') 45 | def click(self, locator, timeout=None) -> WebElement: 46 | self.find(locator, timeout=timeout) 47 | elem = self.wait(timeout).until(EC.element_to_be_clickable(locator)) 48 | elem.click() 49 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/ui/pages/events_page.py: -------------------------------------------------------------------------------- 1 | from ui.locators import basic_locators 2 | from ui.pages.base_page import BasePage 3 | 4 | 5 | class EventsPage(BasePage): 6 | 7 | locators = basic_locators.EventsPageLocators() 8 | url = 'https://www.python.org/events/' 9 | -------------------------------------------------------------------------------- /lection07-ReportRemote/code/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | import allure 4 | from ui.locators import basic_locators 5 | from ui.pages.base_page import BasePage 6 | from ui.pages.events_page import EventsPage 7 | 8 | 9 | class MainPage(BasePage): 10 | 11 | locators = basic_locators.MainPageLocators() 12 | 13 | @allure.step("Step 2") 14 | def go_to_events_page(self): 15 | events_button = self.find(self.locators.EVENTS) 16 | # self.click(events_button) 17 | self.click((By.ID, 'events')) 18 | return EventsPage(self.driver) 19 | -------------------------------------------------------------------------------- /lection07-ReportRemote/homework2.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №2: Продвинутые навыки работы с Selenium 2 | 3 | #### Цель домашнего задания 4 | 5 | * Научиться более продвинуто работать с элементами и действиями с браузером. 6 | * Изучение паттерна PageObject. 7 | * Научиться писать тесты под параллельный запуск Selenium. 8 | * Научиться формировать отчет о тестировании. 9 | 10 | #### Зависимости 11 | 12 | ДЗ выполняется в ветке homework2 и отдельной папке homework2. 13 | 14 | Ваша ветка с ДЗ должна быть синхронизирована с `main`. 15 | 16 | #### Задача 17 | * Тестирование портала https://target-sandbox.my.com/ 18 | * UI тесты должны запускаться через марк -m UI 19 | * В качестве браузера используем Google Chrome версии **105.0.5195.19**. Устанавливаться драйвер должен через webdriver_manager. 20 | * Необходимо передать в качестве входного аргумента pytest (в метод pytest_addoption) значение ```parser.addoption('--headless', action='store_true')``` (как в первой ДЗ) 21 | * Необходимо реализовать фикстуру, которая будет сама авторизовываться и возвращать в тест PageObject главной страницы. 22 | 23 | 1. Общее (4 балла): 24 | * Тесты должны уметь запускаться параллельно (библиотека pytest-xdist) через –n (2 балла). 25 | * Тесты должны иметь хороший информативный отчет в allure (шаги, логи, скриншоты) (2 балла). 26 | 2. UI (12 баллов): 27 | * Написать тест на создание [рекламной кампании](https://target-sandbox.my.com/dashboard) любого типа и проверять что она создана (4 балла). 28 | * Написать тест на создание сегмента в [аудиториях](https://target-sandbox.my.com/segments/segments_list) с типом "Приложения и игры в соцсетях" и проверить что сегмент создан (2 балла). 29 | * Написать тест на создание сегмента, добавив в [источники данных](https://target-sandbox.my.com/segments/groups_list) группу [VK образования](https://vk.com/vkedu). 30 | После этого вам нужно создать сегмент (как и в п.2) с типом "Группы OK и VK", проверить, что он есть, а затем **удалить** именно этот сегмент и добавленный источник данных (3 балла). 31 | * Весь код должен быть реализован на паттерне PageObject (3 балла). 32 | 33 | * Самостоятельно: попробовать установить селеноид и научиться им пользоваться. Мы не будем проверять эту часть, но она вам потребуется на итоговом проекте. 34 | Также вы можете впоследствии самостоятельно позапускать домашку с selenoid'ом вне рамок сдачи ДЗ. 35 | * Так же можно приложить в мр линк на видео успешного запуска селеноида у вас локально, 36 | за это можно будет получить два дополнительных бонусных балла (2 балла) 37 | 38 | #### Советы и рекомендации 39 | * Тесты *НЕ* должны быть зависимыми. Генерируйте уникальные названия (через uuid, через время с секундами или как-то иначе) - это вам позволит определить, какие сущности вы создаете и легко их найти. 40 | * Тесты **ОБЯЗАТЕЛЬНО** должны что-то проверять. Например если мы что-то создали - необходимо проверить, что оно создалось. При необходимости выносить части кода в отдельные методы. 41 | * Если вам нужно что-то удалять - сначала создайте этот объект в тесте, а уже затем его удаляйте. 42 | * Все тесты **ДОЛЖНЫ** проходить. Если вы поддерживаете параллельный запуск, то тесты *ДОЛЖНЫ* проходить в параллельном режиме (не конфликтовать). 43 | * Тесты **ДОЛЖНЫ** уметь запускаться несколько раз подряд, т.е. чтобы данные в тестах никак не конфликтовали. 44 | * Локаторы должны быть простыми и понятными. Автосгенерированные локаторы не принимаются. 45 | * **Можно сделать только часть заданий**, оценка будет проставлена согласно разбалловке выше. 46 | 47 | #### Срок сдачи ДЗ 48 | До 20 октября, 23:59. Максимум 16 баллов + 2 дополнительно за селеноид 49 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/api/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class ApiClientException(Exception): 5 | ... 6 | 7 | 8 | class ApiClient: 9 | def __init__(self, base_url: str, login: str, password: str): 10 | self.base_url = base_url 11 | 12 | self.login = login 13 | self.password = password 14 | 15 | self.session = requests.Session() 16 | 17 | def get_token(self): 18 | headers = requests.get(url=self.base_url).headers['Set-Cookie'].split(';') 19 | token_header = [h for h in headers if 'csrftoken'] 20 | if not token_header: 21 | raise ApiClientException('Expected csrftoken in Set-Cookie') 22 | token_header = token_header[0].split('=')[-1] 23 | return token_header 24 | 25 | def post_login(self): 26 | token = self.get_token() 27 | headers = { 28 | 'Cookie': f'csrftoken={token}', 29 | 'X-CSRFToken': f'{token}' 30 | } 31 | 32 | data = { 33 | 'login': self.login, 34 | 'password': self.password 35 | } 36 | login_request = self.session.post(url='https://education.vk.company/login/', headers=headers, data=data) 37 | 38 | return login_request 39 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import contextmanager 3 | 4 | import allure 5 | import pytest 6 | from _pytest.fixtures import FixtureRequest 7 | from ui.pages.base_page import BasePage 8 | from ui.pages.main_page import MainPage 9 | 10 | CLICK_RETRY = 3 11 | 12 | 13 | class BaseCase: 14 | driver = None 15 | 16 | @contextmanager 17 | def switch_to_window(self, current, close=False): 18 | for w in self.driver.window_handles: 19 | if w != current: 20 | self.driver.switch_to.window(w) 21 | break 22 | yield 23 | if close: 24 | self.driver.close() 25 | self.driver.switch_to.window(current) 26 | 27 | @pytest.fixture(scope='function', autouse=True) 28 | def ui_report(self, driver, request, temp_dir): 29 | failed_test_count = request.session.testsfailed 30 | yield 31 | if request.session.testsfailed > failed_test_count: 32 | browser_logs = os.path.join(temp_dir, 'browser.log') 33 | with open(browser_logs, 'w') as f: 34 | for i in driver.get_log('browser'): 35 | f.write(f"{i['level']} - {i['source']}\n{i['message']}\n") 36 | screenshot_path = os.path.join(temp_dir, 'failed.png') 37 | self.driver.save_screenshot(filename=screenshot_path) 38 | allure.attach.file(screenshot_path, 'failed.png', allure.attachment_type.PNG) 39 | with open(browser_logs, 'r') as f: 40 | allure.attach(f.read(), 'test.log', allure.attachment_type.TEXT) 41 | 42 | @pytest.fixture(scope='function', autouse=True) 43 | def setup(self, driver, config, logger, request: FixtureRequest): 44 | self.driver = driver 45 | self.config = config 46 | self.logger = logger 47 | 48 | self.base_page: BasePage = (request.getfixturevalue('base_page')) 49 | self.main_page: MainPage = (request.getfixturevalue('main_page')) 50 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from api.client import ApiClient 4 | from ui.fixtures import * 5 | 6 | 7 | def pytest_addoption(parser): 8 | parser.addoption('--browser', default='chrome') 9 | parser.addoption('--url', default='https://www.python.org') 10 | parser.addoption('--debug_log', action='store_true') 11 | parser.addoption('--selenoid', action='store_true') 12 | parser.addoption('--vnc', action='store_true') 13 | 14 | 15 | @pytest.fixture(scope='session') 16 | def repo_root(): 17 | return os.path.abspath(os.path.join(__file__, os.path.pardir)) 18 | 19 | 20 | @pytest.fixture(scope='session') 21 | def base_temp_dir(): 22 | if sys.platform.startswith('win'): 23 | base_dir = 'C:\\tests' 24 | else: 25 | base_dir = '/tmp/tests' 26 | if os.path.exists(base_dir): 27 | shutil.rmtree(base_dir) 28 | return base_dir 29 | 30 | 31 | @pytest.fixture(scope='function') 32 | def temp_dir(request): 33 | test_dir = os.path.join(request.config.base_temp_dir, request._pyfuncitem.nodeid) 34 | os.makedirs(test_dir) 35 | return test_dir 36 | 37 | 38 | @pytest.fixture(scope='session') 39 | def config(request): 40 | browser = request.config.getoption('--browser') 41 | url = request.config.getoption('--url') 42 | debug_log = request.config.getoption('--debug_log') 43 | if request.config.getoption('--selenoid'): 44 | if request.config.getoption('--vnc'): 45 | vnc = True 46 | else: 47 | vnc = False 48 | selenoid = 'http://127.0.0.1:4444/wd/hub' 49 | else: 50 | selenoid = None 51 | vnc = False 52 | 53 | return { 54 | 'browser': browser, 55 | 'url': url, 56 | 'debug_log': debug_log, 57 | 'selenoid': selenoid, 58 | 'vnc': vnc, 59 | } 60 | 61 | 62 | @pytest.fixture(scope='function') 63 | def logger(temp_dir, config): 64 | log_formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s') 65 | log_file = os.path.join(temp_dir, 'test.log') 66 | log_level = logging.DEBUG if config['debug_log'] else logging.INFO 67 | 68 | file_handler = logging.FileHandler(log_file, 'w') 69 | file_handler.setFormatter(log_formatter) 70 | file_handler.setLevel(log_level) 71 | 72 | log = logging.getLogger('test') 73 | log.propagate = False 74 | log.setLevel(log_level) 75 | log.handlers.clear() 76 | log.addHandler(file_handler) 77 | 78 | yield log 79 | 80 | for handler in log.handlers: 81 | handler.close() 82 | 83 | 84 | @pytest.fixture(scope='session') 85 | def api_client(credentials, config): 86 | return ApiClient(base_url=config['url'], login=credentials[0], password=credentials[1]) 87 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/files/userdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection08-ApiSimple/code/files/userdata -------------------------------------------------------------------------------- /lection08-ApiSimple/code/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import allure 4 | import pytest 5 | import time 6 | 7 | from selenium.webdriver import ActionChains 8 | 9 | from base import BaseCase 10 | from ui.locators import basic_locators 11 | from selenium.webdriver.common.keys import Keys 12 | from selenium.webdriver.common.by import By 13 | 14 | 15 | class TestExample(BaseCase): 16 | 17 | @pytest.mark.parametrize( 18 | 'query', 19 | [ 20 | pytest.param( 21 | 'pycon' 22 | ), 23 | pytest.param( 24 | 'python' 25 | ), 26 | ], 27 | ) 28 | @pytest.mark.skip('skip') 29 | def test_search(self, query): 30 | self.base_page.search(query) 31 | assert 'No results found' not in self.driver.page_source 32 | 33 | @pytest.mark.skip('skip') 34 | def test_negative_search(self): 35 | time.sleep(5) 36 | self.base_page.search('adasdasdasdasdasda') 37 | time.sleep(5) 38 | assert 'No results found' in self.driver.page_source 39 | 40 | @pytest.mark.skip('skip') 41 | def test_page_change(self): 42 | self.base_page.click( 43 | basic_locators.BasePageLocators.GO_BUTTON_LOCATOR, timeout=10 44 | ) 45 | 46 | @pytest.mark.skip('skip') 47 | def test_carousel(self): 48 | self.main_page.click( 49 | basic_locators.MainPageLocators.COMPREHENSIONS, timeout=15 50 | ) 51 | 52 | @pytest.mark.skip('skip') 53 | def test_iframe(self): 54 | self.main_page.click(self.main_page.locators.START_SHELL) 55 | time.sleep(15) 56 | iframe_first = self.main_page.find((By.XPATH, '//iframe')) 57 | self.driver.switch_to.frame(iframe_first) 58 | iframe_second = self.main_page.find((By.ID, 'id_console')) 59 | self.driver.switch_to.frame(iframe_second) 60 | iframe = self.main_page.find((By.XPATH, '//iframe')) 61 | self.driver.switch_to.frame(iframe) 62 | console = self.main_page.find(self.main_page.locators.PYTHON_CONSOLE) 63 | console.send_keys('assert 1 == 0') 64 | console.send_keys(Keys.ENTER) 65 | time.sleep(10) 66 | self.driver.switch_to.default_content() 67 | 68 | @pytest.mark.skip('skip') 69 | def test_new_tab(self): 70 | current_window = self.driver.current_window_handle 71 | 72 | news = self.main_page.find((By.ID, 'news')) 73 | ActionChains(self.driver).key_down(Keys.CONTROL).click(news).key_up(Keys.CONTROL).perform() 74 | time.sleep(5) 75 | 76 | with self.switch_to_window(current=current_window, close=True): 77 | assert self.driver.current_url == 'https://www.python.org/blogs/' 78 | time.sleep(3) 79 | time.sleep(3) 80 | 81 | @pytest.mark.skip('skip') 82 | def test_events(self): 83 | time.sleep(5) 84 | with allure.step('Going to events page'): 85 | events_page = self.main_page.go_to_events_page() 86 | time.sleep(5) 87 | with allure.step('asserting ...'): 88 | assert 1 == 1 89 | 90 | @pytest.mark.skip('skip') 91 | def test_relative(self): 92 | intro = self.main_page.find((By.CSS_SELECTOR, 'div.introduction')) 93 | learn_more = intro.find_element(*self.main_page.locators.READ_MORE) 94 | assert learn_more.get_attribute('href') == self.driver.current_url + 'doc/' 95 | 96 | 97 | class TestLoad(BaseCase): 98 | 99 | @pytest.mark.skip('skip') 100 | def test_download(self): 101 | self.driver.get('https://www.python.org/downloads/release/python-3100/') 102 | time.sleep(5) 103 | self.main_page.click((By.XPATH, '//a[@href="https://www.python.org/ftp/python/3.10.0/python-3.10.0-embed-win32.zip"]')) 104 | time.sleep(10) 105 | 106 | @pytest.fixture() 107 | def file_path(self, repo_root): 108 | return os.path.join(repo_root, 'files', 'userdata') 109 | 110 | @pytest.mark.skip('skip') 111 | def test_upload(self, file_path): 112 | self.driver.get('https://ps.uci.edu/~franklin/doc/file_upload.html') 113 | input = (By.NAME, 'userfile') 114 | time.sleep(5) 115 | self.main_page.find(input).send_keys(file_path) 116 | time.sleep(5) 117 | 118 | 119 | class TestFailed(BaseCase): 120 | 121 | @pytest.mark.skip('skip') 122 | def test_fail(self): 123 | self.main_page.find((By.XPATH, '12312312312312'), timeout=1) 124 | 125 | @pytest.mark.skip('skip') 126 | def test_logs_browser(self): 127 | self.driver.get('https://target.my.com/') 128 | time.sleep(3) 129 | assert 0 130 | 131 | @pytest.mark.skip('skip') 132 | @allure.step("Step 1") 133 | def test_log(self): 134 | self.logger.info('Ready to start') 135 | self.logger.info('Going to events page') 136 | events_page = self.main_page.go_to_events_page() 137 | self.logger.info('asserting') 138 | assert 1 == 1 139 | 140 | 141 | @pytest.mark.skip('skip') 142 | def test_check_all_drivers(all_drivers): 143 | time.sleep(3) 144 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/test_login.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | from _pytest.fixtures import FixtureRequest 5 | from selenium.webdriver.common.by import By 6 | 7 | from ui.fixtures import get_driver 8 | from ui.pages.base_page import BasePage 9 | 10 | 11 | class BaseCase: 12 | authorize = True 13 | 14 | @pytest.fixture(scope='function', autouse=True) 15 | def setup(self, driver, config, request: FixtureRequest, logger): 16 | self.driver = driver 17 | self.config = config 18 | self.logger = logger 19 | 20 | self.login_page = LoginPage(driver) 21 | if self.authorize: 22 | cookies = request.getfixturevalue('cookies') 23 | for cookie in cookies: 24 | self.driver.add_cookie(cookie) 25 | 26 | self.driver.refresh() 27 | self.main_page = MainPage(driver) 28 | 29 | 30 | @pytest.fixture(scope='session') 31 | def credentials(): 32 | with open('/Users/k.soldatov/Documents/creds', 'r') as f: 33 | user = f.readline().strip() 34 | password = f.readline().strip() 35 | 36 | return user, password 37 | 38 | 39 | @pytest.fixture(scope='session') 40 | def cookies(credentials, config, api_client): 41 | api_client.post_login() 42 | cookies = [] 43 | for cookie in api_client.session.cookies: 44 | cookies.append({ 45 | 'name': cookie.name, 46 | 'domain': cookie.domain, 47 | 'path': cookie.path, 48 | 'value': cookie.value 49 | }) 50 | return cookies 51 | 52 | 53 | class LoginPage(BasePage): 54 | url = 'https://education.vk.company/' 55 | 56 | def login(self, user, password): 57 | self.click((By.XPATH, '/html/body/header/div/nav/button')) 58 | self.find((By.NAME, 'login')).send_keys(user) 59 | self.find((By.NAME, 'password')).send_keys(password) 60 | 61 | self.click((By.XPATH, '//html/body/div[4]/div/div[1]/form/button')) 62 | 63 | time.sleep(5) 64 | return MainPage(self.driver) 65 | 66 | 67 | class MainPage(BasePage): 68 | url = 'https://education.vk.company/feed/' 69 | 70 | 71 | class TestLogin(BaseCase): 72 | authorize = False 73 | 74 | @pytest.mark.skip("SKIP") 75 | def test_login(self, credentials): 76 | login_page = LoginPage(self.driver) 77 | login_page.login(*credentials) 78 | 79 | time.sleep(5) 80 | 81 | 82 | class TestLK(BaseCase): 83 | 84 | def test_lk1(self): 85 | time.sleep(3) 86 | 87 | def test_lk2(self): 88 | time.sleep(3) 89 | 90 | 91 | class TestLkApi: 92 | @pytest.fixture(scope='class', autouse=True) 93 | def setup(self, api_client): 94 | api_client.post_login() 95 | 96 | def test_api_login(self, api_client): 97 | assert api_client.session.get('https://education.vk.company/profile/k.soldatov/').url == \ 98 | 'https://education.vk.company/profile/k.soldatov/' 99 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | import pytest 6 | from selenium import webdriver 7 | from selenium.webdriver.chrome.options import Options 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | from webdriver_manager.firefox import GeckoDriverManager 10 | from ui.pages.base_page import BasePage 11 | from ui.pages.main_page import MainPage 12 | 13 | 14 | def pytest_configure(config): 15 | if sys.platform.startswith('win'): 16 | base_dir = 'C:\\tests' 17 | else: 18 | base_dir = '/tmp/tests' 19 | if not hasattr(config, 'workerunput'): 20 | if os.path.exists(base_dir): 21 | shutil.rmtree(base_dir) 22 | os.makedirs(base_dir) 23 | 24 | config.base_temp_dir = base_dir 25 | 26 | 27 | @pytest.fixture() 28 | def driver(config, temp_dir): 29 | browser = config['browser'] 30 | url = config['url'] 31 | selenoid = config['selenoid'] 32 | vnc = config['vnc'] 33 | options = Options() 34 | options.add_experimental_option("prefs", {"download.default_directory": temp_dir}) 35 | if selenoid: 36 | capabilities = { 37 | 'browserName': 'chrome', 38 | 'version': '106.0', 39 | } 40 | if vnc: 41 | capabilities['enableVNC'] = True 42 | driver = webdriver.Remote( 43 | 'http://127.0.0.1:4444/wd/hub', 44 | options=options, 45 | desired_capabilities=capabilities 46 | ) 47 | elif browser == 'chrome': 48 | driver = webdriver.Chrome(executable_path=ChromeDriverManager().install(), options=options) 49 | elif browser == 'firefox': 50 | driver = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 51 | else: 52 | raise RuntimeError(f'Unsupported browser: "{browser}"') 53 | driver.get(url) 54 | driver.maximize_window() 55 | yield driver 56 | driver.quit() 57 | 58 | 59 | def get_driver(browser_name): 60 | if browser_name == 'chrome': 61 | browser = webdriver.Chrome(executable_path=ChromeDriverManager().install()) 62 | elif browser_name == 'firefox': 63 | browser = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 64 | else: 65 | raise RuntimeError(f'Unsupported browser: "{browser_name}"') 66 | browser.maximize_window() 67 | return browser 68 | 69 | 70 | @pytest.fixture(scope='session', params=['chrome', 'firefox']) 71 | def all_drivers(config, request): 72 | url = config['url'] 73 | browser = get_driver(request.param) 74 | browser.get(url) 75 | yield browser 76 | browser.quit() 77 | 78 | 79 | @pytest.fixture 80 | def base_page(driver): 81 | return BasePage(driver=driver) 82 | 83 | 84 | @pytest.fixture 85 | def main_page(driver): 86 | return MainPage(driver=driver) 87 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/ui/locators/basic_locators.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | QUERY_LOCATOR = (By.NAME, 'q') 6 | QUERY_LOCATOR_ID = (By.ID, 'id-search-field') 7 | GO_BUTTON_LOCATOR = (By.XPATH, '//*[@id="submit"]') 8 | START_SHELL = (By.ID, 'start-shell') 9 | PYTHON_CONSOLE = (By.ID, 'hterm:row-nodes') 10 | 11 | 12 | class MainPageLocators(BasePageLocators): 13 | COMPREHENSIONS = ( 14 | By.XPATH, 15 | '//code/span[@class="comment" and contains(text(), "comprehensions")]' 16 | ) 17 | EVENTS = (By.ID, 'events') 18 | READ_MORE = (By.CSS_SELECTOR, 'a.readmore') 19 | 20 | 21 | class EventsPageLocators(BasePageLocators): 22 | pass 23 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/ui/pages/base_page.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import allure 4 | from selenium.webdriver.remote.webelement import WebElement 5 | from ui.locators import basic_locators 6 | from selenium.webdriver.support.wait import WebDriverWait 7 | from selenium.webdriver.support import expected_conditions as EC 8 | 9 | 10 | class PageNotOpenedExeption(Exception): 11 | pass 12 | 13 | 14 | class BasePage(object): 15 | 16 | locators = basic_locators.BasePageLocators() 17 | url = 'https://www.python.org/' 18 | 19 | def is_opened(self, timeout=15): 20 | started = time.time() 21 | while time.time() - started < timeout: 22 | if self.driver.current_url == self.url: 23 | return True 24 | raise PageNotOpenedExeption(f'{self.url} did not open in {timeout} sec, current url {self.driver.current_url}') 25 | 26 | def __init__(self, driver): 27 | self.driver = driver 28 | self.is_opened() 29 | 30 | def wait(self, timeout=None): 31 | if timeout is None: 32 | timeout = 5 33 | return WebDriverWait(self.driver, timeout=timeout) 34 | 35 | def find(self, locator, timeout=None): 36 | return self.wait(timeout).until(EC.presence_of_element_located(locator)) 37 | 38 | def search(self, query): 39 | elem = self.find(self.locators.QUERY_LOCATOR_ID) 40 | elem.send_keys(query) 41 | go_button = self.find(self.locators.GO_BUTTON_LOCATOR) 42 | go_button.click() 43 | 44 | @allure.step('Click') 45 | def click(self, locator, timeout=None) -> WebElement: 46 | self.find(locator, timeout=timeout) 47 | elem = self.wait(timeout).until(EC.element_to_be_clickable(locator)) 48 | elem.click() 49 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/ui/pages/events_page.py: -------------------------------------------------------------------------------- 1 | from ui.locators import basic_locators 2 | from ui.pages.base_page import BasePage 3 | 4 | 5 | class EventsPage(BasePage): 6 | 7 | locators = basic_locators.EventsPageLocators() 8 | url = 'https://www.python.org/events/' 9 | -------------------------------------------------------------------------------- /lection08-ApiSimple/code/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | import allure 4 | from ui.locators import basic_locators 5 | from ui.pages.base_page import BasePage 6 | from ui.pages.events_page import EventsPage 7 | 8 | 9 | class MainPage(BasePage): 10 | 11 | locators = basic_locators.MainPageLocators() 12 | 13 | @allure.step("Step 2") 14 | def go_to_events_page(self): 15 | events_button = self.find(self.locators.EVENTS) 16 | # self.click(events_button) 17 | self.click((By.ID, 'events')) 18 | return EventsPage(self.driver) 19 | -------------------------------------------------------------------------------- /lection08-ApiSimple/lection8 - ApiSimple.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection08-ApiSimple/lection8 - ApiSimple.pdf -------------------------------------------------------------------------------- /lection09-Api/code/api/client.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | import requests 4 | 5 | 6 | class ApiClientException(Exception): 7 | ... 8 | 9 | 10 | class ResponseStatusCodeException(Exception): 11 | pass 12 | 13 | 14 | class RespondErrorException(Exception): 15 | pass 16 | 17 | 18 | class ApiClient: 19 | BLOG_ID = 431 20 | 21 | def __init__(self, base_url: str, login: str, password: str): 22 | self.base_url = base_url 23 | 24 | self.login = login 25 | self.password = password 26 | 27 | self.session = requests.Session() 28 | 29 | def get_token(self): 30 | headers = requests.get(url=self.base_url).headers['Set-Cookie'].split(';') 31 | token_header = [h for h in headers if 'csrftoken'] 32 | if not token_header: 33 | raise ApiClientException('Expected csrftoken in Set-Cookie') 34 | token_header = token_header[0].split('=')[-1] 35 | return token_header 36 | 37 | def post_login(self): 38 | token = self.get_token() 39 | headers = { 40 | 'Cookie': f'csrftoken={token}', 41 | 'X-CSRFToken': f'{token}' 42 | } 43 | 44 | data = { 45 | 'login': self.login, 46 | 'password': self.password 47 | } 48 | login_request = self._request(method='POST', location='login/', headers=headers, data=data) 49 | 50 | return login_request 51 | 52 | def _request(self, method, location, headers, data, params=None, allow_redirects=False, expected_status=200, 53 | jsonify=True): 54 | url = urljoin(self.base_url, location) 55 | 56 | response = self.session.request(method=method, url=url, headers=headers, data=data, params=params, 57 | allow_redirects=allow_redirects) 58 | 59 | if response.status_code != expected_status: 60 | raise ResponseStatusCodeException(f'Expected {expected_status}, but got {response.status_code}') 61 | if jsonify: 62 | json_response: dict = response.json() 63 | if json_response.get('bStateError', False): 64 | error = json_response['sErrorMsg'] 65 | raise RespondErrorException(f'Request {url} return error : "{error}"') 66 | 67 | return json_response 68 | return response 69 | 70 | def post_topic_create(self, title, text, publish=False): 71 | data = { 72 | 'title': title, 73 | 'text': text, 74 | 'publish': 'on' if publish else 'false', 75 | 'forbid_comment': 'false', 76 | 'is_news': 'false', 77 | 'important': 'false', 78 | 'lessons': '', 79 | 'blog': self.BLOG_ID, 80 | } 81 | 82 | headers = { 83 | 'Cookie': f'sessionid_gtp={self.session.cookies["sessionid_gtp"]};' 84 | f' csrftoken={self.session.cookies["csrftoken"]}', 85 | 'X-CSRFToken': f'{self.session.cookies["csrftoken"]}' 86 | } 87 | 88 | location = urljoin(self.base_url, 'blog/topic/create/') 89 | return self._request(method='POST', location=location, data=data, headers=headers) 90 | 91 | def get_feed(self, feed_type=None): 92 | feed_type = feed_type if feed_type else 'all' 93 | params = {'type': feed_type} 94 | 95 | headers = { 96 | 'Cookie': f'sessionid_gtp={self.session.cookies["sessionid_gtp"]};' 97 | f' csrftoken={self.session.cookies["csrftoken"]}', 98 | } 99 | 100 | location = urljoin(self.base_url, 'feed/update/stream/') 101 | 102 | return self._request(method='GET', location=location, headers=headers, params=params, data=None) 103 | 104 | def post_topic_delete(self, topic_id): 105 | data = { 106 | 'submit': 'Удалить', 107 | } 108 | headers = { 109 | 'Cookie': f'sessionid_gtp={self.session.cookies["sessionid_gtp"]}; ' 110 | f'csrftoken={self.session.cookies["csrftoken"]};', 111 | 'X-CSRFToken': f'{self.session.cookies["csrftoken"]}' 112 | } 113 | 114 | location = urljoin(self.base_url, f'blog/topic/delete/{topic_id}/') 115 | 116 | delete_request = self._request(method='POST', location=location, data=data, headers=headers, 117 | allow_redirects=True, jsonify=False) 118 | return delete_request 119 | -------------------------------------------------------------------------------- /lection09-Api/code/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from api.client import ApiClient 4 | from ui.fixtures import * 5 | 6 | 7 | def pytest_addoption(parser): 8 | parser.addoption('--browser', default='chrome') 9 | parser.addoption('--url', default='https://www.python.org') 10 | parser.addoption('--debug_log', action='store_true') 11 | parser.addoption('--selenoid', action='store_true') 12 | parser.addoption('--vnc', action='store_true') 13 | 14 | 15 | @pytest.fixture(scope='session') 16 | def repo_root(): 17 | return os.path.abspath(os.path.join(__file__, os.path.pardir)) 18 | 19 | 20 | @pytest.fixture(scope='session') 21 | def base_temp_dir(): 22 | if sys.platform.startswith('win'): 23 | base_dir = 'C:\\tests' 24 | else: 25 | base_dir = '/tmp/tests' 26 | if os.path.exists(base_dir): 27 | shutil.rmtree(base_dir) 28 | return base_dir 29 | 30 | 31 | @pytest.fixture(scope='function') 32 | def temp_dir(request): 33 | test_dir = os.path.join(request.config.base_temp_dir, request._pyfuncitem.nodeid) 34 | os.makedirs(test_dir) 35 | return test_dir 36 | 37 | 38 | @pytest.fixture(scope='session') 39 | def config(request): 40 | browser = request.config.getoption('--browser') 41 | url = request.config.getoption('--url') 42 | debug_log = request.config.getoption('--debug_log') 43 | if request.config.getoption('--selenoid'): 44 | if request.config.getoption('--vnc'): 45 | vnc = True 46 | else: 47 | vnc = False 48 | selenoid = 'http://127.0.0.1:4444/wd/hub' 49 | else: 50 | selenoid = None 51 | vnc = False 52 | 53 | return { 54 | 'browser': browser, 55 | 'url': url, 56 | 'debug_log': debug_log, 57 | 'selenoid': selenoid, 58 | 'vnc': vnc, 59 | } 60 | 61 | 62 | @pytest.fixture(scope='function') 63 | def logger(temp_dir, config): 64 | log_formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s') 65 | log_file = os.path.join(temp_dir, 'test.log') 66 | log_level = logging.DEBUG if config['debug_log'] else logging.INFO 67 | 68 | file_handler = logging.FileHandler(log_file, 'w') 69 | file_handler.setFormatter(log_formatter) 70 | file_handler.setLevel(log_level) 71 | 72 | log = logging.getLogger('test') 73 | log.propagate = False 74 | log.setLevel(log_level) 75 | log.handlers.clear() 76 | log.addHandler(file_handler) 77 | 78 | yield log 79 | 80 | for handler in log.handlers: 81 | handler.close() 82 | 83 | 84 | @pytest.fixture(scope='session') 85 | def api_client(credentials, config): 86 | return ApiClient(base_url=config['url'], login=credentials[0], password=credentials[1]) 87 | 88 | 89 | @pytest.fixture(scope='session') 90 | def credentials(): 91 | with open('/Users/k.soldatov/Documents/creds', 'r') as f: 92 | user = f.readline().strip() 93 | password = f.readline().strip() 94 | 95 | return user, password 96 | -------------------------------------------------------------------------------- /lection09-Api/code/files/userdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection09-Api/code/files/userdata -------------------------------------------------------------------------------- /lection09-Api/code/test_api/base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from test_api.builder import Builder 4 | 5 | 6 | class ApiBase: 7 | authorize = True 8 | 9 | @pytest.fixture(scope='function', autouse=True) 10 | def setup(self, api_client): 11 | self.api_client = api_client 12 | self.builder = Builder() 13 | 14 | if self.authorize: 15 | self.api_client.post_login() 16 | 17 | def check_topic_in_feed(self, topic_id, text): 18 | found = False 19 | all_posts_dict = self.api_client.get_feed() 20 | for posts in all_posts_dict: 21 | for post in posts: 22 | if post['object']['id'] == topic_id and post['object']['text'] == text: 23 | found = True 24 | break 25 | assert found is True, f'Expected to find topic with id "{topic_id}" and text "{text}" in feed, but got nothing' 26 | 27 | def create_topic(self, title, text, publish=False): 28 | req = self.api_client.post_topic_create(title=title, text=text, publish=publish) 29 | assert req['success'] is True 30 | 31 | return req['redirect_url'].split('/')[-2] 32 | 33 | @pytest.fixture(scope='function') 34 | def topic(self): 35 | topic_data = self.builder.topic() 36 | topic_id = self.create_topic(text=topic_data.text, title=topic_data.title, publish=self.publish) 37 | topic_data.id = topic_id 38 | yield topic_data 39 | 40 | self.api_client.post_topic_delete(topic_id=topic_id) 41 | -------------------------------------------------------------------------------- /lection09-Api/code/test_api/builder.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import faker 4 | 5 | faker = faker.Faker() 6 | 7 | 8 | class Builder: 9 | @staticmethod 10 | def topic(text=None, title=None): 11 | @dataclass 12 | class Topic: 13 | title: str 14 | text: str 15 | id: None = None 16 | 17 | if title is None: 18 | title = faker.lexify('?? ??? ???? ???') 19 | 20 | if text is None: 21 | text = faker.bothify('?? ?#?? ?##????##? ??#?') 22 | 23 | return Topic(title=title, text=text) 24 | -------------------------------------------------------------------------------- /lection09-Api/code/test_api/test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from api.client import RespondErrorException 4 | from test_api.base import ApiBase 5 | 6 | 7 | class TestApi(ApiBase): 8 | authorize = False 9 | 10 | def test_valid_login(self): 11 | login_request = self.api_client.post_login() 12 | assert login_request['bStateError'] is False 13 | 14 | def test_invalid_login(self): 15 | self.api_client.login = '123' 16 | self.api_client.password = '456' 17 | 18 | with pytest.raises(RespondErrorException): 19 | login_request = self.api_client.post_login() 20 | assert login_request['bStateError'] is True 21 | pytest.fail( 22 | f'Unxecpect login happend with login {self.api_client.login} and password {self.api_client.password}') 23 | 24 | 25 | class TestTopicDraft(ApiBase): 26 | publish = False 27 | 28 | def test_topic_creation(self, topic): 29 | print(topic.title) 30 | print(topic.text) 31 | print(topic.id) 32 | 33 | 34 | class TestTopicCreate(TestTopicDraft): 35 | publish = True 36 | 37 | def test_topic_creation(self, topic): 38 | print(topic.title) 39 | self.check_topic_in_feed(text=topic.text, topic_id=topic.id) 40 | 41 | 42 | ################################################################# 43 | # то же самое, другим способом 44 | 45 | class TestTopicDraftVer2(ApiBase): 46 | publish = False 47 | 48 | def check(self): 49 | print(self.topic.id) 50 | 51 | def test_topic_creation(self, topic): 52 | self.topic = topic 53 | self.check() 54 | 55 | 56 | class TestTopicCreateVer2(TestTopicDraftVer2): 57 | publish = True 58 | 59 | def check(self): 60 | self.check_topic_in_feed(text=self.topic.text, topic_id=self.topic.id) 61 | -------------------------------------------------------------------------------- /lection09-Api/code/test_ui/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import contextmanager 3 | 4 | import allure 5 | import pytest 6 | from _pytest.fixtures import FixtureRequest 7 | from ui.pages.base_page import BasePage 8 | from ui.pages.main_page import MainPage 9 | 10 | CLICK_RETRY = 3 11 | 12 | 13 | class BaseCase: 14 | driver = None 15 | 16 | @contextmanager 17 | def switch_to_window(self, current, close=False): 18 | for w in self.driver.window_handles: 19 | if w != current: 20 | self.driver.switch_to.window(w) 21 | break 22 | yield 23 | if close: 24 | self.driver.close() 25 | self.driver.switch_to.window(current) 26 | 27 | @pytest.fixture(scope='function', autouse=True) 28 | def ui_report(self, driver, request, temp_dir): 29 | failed_test_count = request.session.testsfailed 30 | yield 31 | if request.session.testsfailed > failed_test_count: 32 | browser_logs = os.path.join(temp_dir, 'browser.log') 33 | with open(browser_logs, 'w') as f: 34 | for i in driver.get_log('browser'): 35 | f.write(f"{i['level']} - {i['source']}\n{i['message']}\n") 36 | screenshot_path = os.path.join(temp_dir, 'failed.png') 37 | self.driver.save_screenshot(filename=screenshot_path) 38 | allure.attach.file(screenshot_path, 'failed.png', allure.attachment_type.PNG) 39 | with open(browser_logs, 'r') as f: 40 | allure.attach(f.read(), 'test.log', allure.attachment_type.TEXT) 41 | 42 | @pytest.fixture(scope='function', autouse=True) 43 | def setup(self, driver, config, logger, request: FixtureRequest): 44 | self.driver = driver 45 | self.config = config 46 | self.logger = logger 47 | 48 | self.base_page: BasePage = (request.getfixturevalue('base_page')) 49 | self.main_page: MainPage = (request.getfixturevalue('main_page')) 50 | -------------------------------------------------------------------------------- /lection09-Api/code/test_ui/test_login.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | from _pytest.fixtures import FixtureRequest 5 | from selenium.webdriver.common.by import By 6 | 7 | from ui.fixtures import get_driver 8 | from ui.pages.base_page import BasePage 9 | 10 | 11 | class BaseCase: 12 | authorize = True 13 | 14 | @pytest.fixture(scope='function', autouse=True) 15 | def setup(self, driver, config, request: FixtureRequest, logger): 16 | self.driver = driver 17 | self.config = config 18 | self.logger = logger 19 | 20 | self.login_page = LoginPage(driver) 21 | if self.authorize: 22 | cookies = request.getfixturevalue('cookies') 23 | for cookie in cookies: 24 | self.driver.add_cookie(cookie) 25 | 26 | self.driver.refresh() 27 | self.main_page = MainPage(driver) 28 | 29 | 30 | @pytest.fixture(scope='session') 31 | def credentials(): 32 | with open('/Users/k.soldatov/Documents/creds', 'r') as f: 33 | user = f.readline().strip() 34 | password = f.readline().strip() 35 | 36 | return user, password 37 | 38 | 39 | @pytest.fixture(scope='session') 40 | def cookies(credentials, config, api_client): 41 | api_client.post_login() 42 | cookies = [] 43 | for cookie in api_client.session.cookies: 44 | cookies.append({ 45 | 'name': cookie.name, 46 | 'domain': cookie.domain, 47 | 'path': cookie.path, 48 | 'value': cookie.value 49 | }) 50 | return cookies 51 | 52 | 53 | class LoginPage(BasePage): 54 | url = 'https://education.vk.company/' 55 | 56 | def login(self, user, password): 57 | self.click((By.XPATH, '/html/body/header/div/nav/button')) 58 | self.find((By.NAME, 'login')).send_keys(user) 59 | self.find((By.NAME, 'password')).send_keys(password) 60 | 61 | self.click((By.XPATH, '//html/body/div[4]/div/div[1]/form/button')) 62 | 63 | time.sleep(5) 64 | return MainPage(self.driver) 65 | 66 | 67 | class MainPage(BasePage): 68 | url = 'https://education.vk.company/feed/' 69 | 70 | 71 | class TestLogin(BaseCase): 72 | authorize = False 73 | 74 | @pytest.mark.skip("SKIP") 75 | def test_login(self, credentials): 76 | login_page = LoginPage(self.driver) 77 | login_page.login(*credentials) 78 | 79 | time.sleep(5) 80 | 81 | 82 | class TestLK(BaseCase): 83 | 84 | def test_lk1(self): 85 | time.sleep(3) 86 | 87 | def test_lk2(self): 88 | time.sleep(3) 89 | 90 | 91 | class TestLkApi: 92 | @pytest.fixture(scope='class', autouse=True) 93 | def setup(self, api_client): 94 | api_client.post_login() 95 | 96 | def test_api_login(self, api_client): 97 | assert api_client.session.get('https://education.vk.company/profile/k.soldatov/').url == \ 98 | 'https://education.vk.company/profile/k.soldatov/' 99 | -------------------------------------------------------------------------------- /lection09-Api/code/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | import pytest 6 | from selenium import webdriver 7 | from selenium.webdriver.chrome.options import Options 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | from webdriver_manager.firefox import GeckoDriverManager 10 | from ui.pages.base_page import BasePage 11 | from ui.pages.main_page import MainPage 12 | 13 | 14 | def pytest_configure(config): 15 | if sys.platform.startswith('win'): 16 | base_dir = 'C:\\tests' 17 | else: 18 | base_dir = '/tmp/tests' 19 | if not hasattr(config, 'workerunput'): 20 | if os.path.exists(base_dir): 21 | shutil.rmtree(base_dir) 22 | os.makedirs(base_dir) 23 | 24 | config.base_temp_dir = base_dir 25 | 26 | 27 | @pytest.fixture() 28 | def driver(config, temp_dir): 29 | browser = config['browser'] 30 | url = config['url'] 31 | selenoid = config['selenoid'] 32 | vnc = config['vnc'] 33 | options = Options() 34 | options.add_experimental_option("prefs", {"download.default_directory": temp_dir}) 35 | if selenoid: 36 | capabilities = { 37 | 'browserName': 'chrome', 38 | 'version': '106.0', 39 | } 40 | if vnc: 41 | capabilities['enableVNC'] = True 42 | driver = webdriver.Remote( 43 | 'http://127.0.0.1:4444/wd/hub', 44 | options=options, 45 | desired_capabilities=capabilities 46 | ) 47 | elif browser == 'chrome': 48 | driver = webdriver.Chrome(executable_path=ChromeDriverManager().install(), options=options) 49 | elif browser == 'firefox': 50 | driver = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 51 | else: 52 | raise RuntimeError(f'Unsupported browser: "{browser}"') 53 | driver.get(url) 54 | driver.maximize_window() 55 | yield driver 56 | driver.quit() 57 | 58 | 59 | def get_driver(browser_name): 60 | if browser_name == 'chrome': 61 | browser = webdriver.Chrome(executable_path=ChromeDriverManager().install()) 62 | elif browser_name == 'firefox': 63 | browser = webdriver.Firefox(executable_path=GeckoDriverManager().install()) 64 | else: 65 | raise RuntimeError(f'Unsupported browser: "{browser_name}"') 66 | browser.maximize_window() 67 | return browser 68 | 69 | 70 | @pytest.fixture(scope='session', params=['chrome', 'firefox']) 71 | def all_drivers(config, request): 72 | url = config['url'] 73 | browser = get_driver(request.param) 74 | browser.get(url) 75 | yield browser 76 | browser.quit() 77 | 78 | 79 | @pytest.fixture 80 | def base_page(driver): 81 | return BasePage(driver=driver) 82 | 83 | 84 | @pytest.fixture 85 | def main_page(driver): 86 | return MainPage(driver=driver) 87 | -------------------------------------------------------------------------------- /lection09-Api/code/ui/locators/basic_locators.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | QUERY_LOCATOR = (By.NAME, 'q') 6 | QUERY_LOCATOR_ID = (By.ID, 'id-search-field') 7 | GO_BUTTON_LOCATOR = (By.XPATH, '//*[@id="submit"]') 8 | START_SHELL = (By.ID, 'start-shell') 9 | PYTHON_CONSOLE = (By.ID, 'hterm:row-nodes') 10 | 11 | 12 | class MainPageLocators(BasePageLocators): 13 | COMPREHENSIONS = ( 14 | By.XPATH, 15 | '//code/span[@class="comment" and contains(text(), "comprehensions")]' 16 | ) 17 | EVENTS = (By.ID, 'events') 18 | READ_MORE = (By.CSS_SELECTOR, 'a.readmore') 19 | 20 | 21 | class EventsPageLocators(BasePageLocators): 22 | pass 23 | -------------------------------------------------------------------------------- /lection09-Api/code/ui/pages/base_page.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import allure 4 | from selenium.webdriver.remote.webelement import WebElement 5 | from ui.locators import basic_locators 6 | from selenium.webdriver.support.wait import WebDriverWait 7 | from selenium.webdriver.support import expected_conditions as EC 8 | 9 | 10 | class PageNotOpenedExeption(Exception): 11 | pass 12 | 13 | 14 | class BasePage(object): 15 | 16 | locators = basic_locators.BasePageLocators() 17 | url = 'https://www.python.org/' 18 | 19 | def is_opened(self, timeout=15): 20 | started = time.time() 21 | while time.time() - started < timeout: 22 | if self.driver.current_url == self.url: 23 | return True 24 | raise PageNotOpenedExeption(f'{self.url} did not open in {timeout} sec, current url {self.driver.current_url}') 25 | 26 | def __init__(self, driver): 27 | self.driver = driver 28 | self.is_opened() 29 | 30 | def wait(self, timeout=None): 31 | if timeout is None: 32 | timeout = 5 33 | return WebDriverWait(self.driver, timeout=timeout) 34 | 35 | def find(self, locator, timeout=None): 36 | return self.wait(timeout).until(EC.presence_of_element_located(locator)) 37 | 38 | def search(self, query): 39 | elem = self.find(self.locators.QUERY_LOCATOR_ID) 40 | elem.send_keys(query) 41 | go_button = self.find(self.locators.GO_BUTTON_LOCATOR) 42 | go_button.click() 43 | 44 | @allure.step('Click') 45 | def click(self, locator, timeout=None) -> WebElement: 46 | self.find(locator, timeout=timeout) 47 | elem = self.wait(timeout).until(EC.element_to_be_clickable(locator)) 48 | elem.click() 49 | -------------------------------------------------------------------------------- /lection09-Api/code/ui/pages/events_page.py: -------------------------------------------------------------------------------- 1 | from ui.locators import basic_locators 2 | from ui.pages.base_page import BasePage 3 | 4 | 5 | class EventsPage(BasePage): 6 | 7 | locators = basic_locators.EventsPageLocators() 8 | url = 'https://www.python.org/events/' 9 | -------------------------------------------------------------------------------- /lection09-Api/code/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | import allure 4 | from ui.locators import basic_locators 5 | from ui.pages.base_page import BasePage 6 | from ui.pages.events_page import EventsPage 7 | 8 | 9 | class MainPage(BasePage): 10 | 11 | locators = basic_locators.MainPageLocators() 12 | 13 | @allure.step("Step 2") 14 | def go_to_events_page(self): 15 | events_button = self.find(self.locators.EVENTS) 16 | # self.click(events_button) 17 | self.click((By.ID, 'events')) 18 | return EventsPage(self.driver) 19 | -------------------------------------------------------------------------------- /lection09-Api/homework3.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №3: Работа с API myTarget 2 | 3 | #### Цель домашнего задания 4 | 5 | * Научиться делать то же самое, что и браузер, но самостоятельно через API-запросы. 6 | Работа с API идет без документации, просто по аналогии с тем, как работает браузер с запросами (на примере сущностей из ДЗ №2). 7 | * Научиться писать тесты с использованием библиотеки requests. 8 | 9 | #### Зависимости 10 | 11 | ДЗ выполняется в ветке homework3 и отдельной папке homework3. В папке homework3 должна лежать ваша ДЗ, никаких подпапок code и т.д. 12 | 13 | Ваша ветка с ДЗ должна быть синхронизирована с `main`. 14 | 15 | **На каждый PUSH кода в репозиторий будет срабатывать GitHub Actions.** Вы можете посмотреть результаты прогонов самостоятельно во вкладке Actions (см. пост на портале с инструкций), особенно если они будут "красными". 16 | **Настоятельно просим вас НЕ пушить постоянно код**, делайте это осмысленно и как можно реже. 17 | 18 | #### Задача 19 | * Тестирование портала https://target-sandbox.my.com 20 | * Настроить окружение для запуска API-тестов. 21 | * API-тесты должны запускаться через марк -m API. 22 | * Все тесты должны работать как минимум в 2 потока (через [pytest-xdist](https://pypi.org/project/pytest-xdist/) с аргументом -n 2). 23 | * Описание ДЗ (10 баллов): 24 | * **Написать API клиент, который будет иметь возможность авторизовываться на портале (3 балла).** 25 | * Написать тест на работу с кампанией кампании любого типа через API: кампания должна быть создана, затем проверена, что она создана, после этого - удалена (2 балла). 26 | Все в рамках одного теста. 27 | * Написать тест на работу с сегментами в аудиториях через API : сегмент должен быть создаен, затем проверен, что он создался, после этого - удален (2 балл). 28 | Все в рамках одного теста. 29 | * Написать тест на создание сегмента, предварительно добавив в [источники данных](https://target-sandbox.my.com/segments/groups_list) группу [VK образования](https://vk.com/vkedu). 30 | После этого вам нужно создать сегмент (как и в пункте выше) с типом "Группы OK и VK", проверить, что он есть, а затем **удалить** именно этот сегмент и добавленный источник данных (3 балла) 31 | 32 | PS: Мы впервые работаем с API именно на sandbox-версии, а последнее (4-е) задание ранее не давали. Если возникнут какие-то проблемы в процессе ДЗ - не бойтесь задавать вопросы в чате. 33 | 34 | #### Советы и рекомендации 35 | * По API: 36 | 37 | а) Авторизация делается по аналогии с лекциями - разобраться в авторизации на портале, разобраться где брать csrf-токен, какие запросы нужно сделать. 38 | **Куки, csrf-token и прочее должны храниться в сессии** (requests.Session). Логин и пароль - хардкодим. 39 | 40 | б) Если вы загружаете какие-то изображения для кампании - сначала загрузите их в таргет с помощью специальной API и получите их ID. 41 | **Путь к файлам задавать через repo_root**! Ваши тесты должны запускаться из любой части репозитория и не зависеть от указания пути к файлу. 42 | 43 | в) Список кампаний и сегментов (или конкретную кампанию/сегмент) можно также получить по API, а дальше с этими данными работать (например, удалять). 44 | 45 | г) Везде, где в данных для API фигурирует поле id - это значит, что эти данные хардкодить нельзя, а нужно разобраться, откуда они берутся. 46 | 47 | д) Не все заголовки, которые используются в запросах через браузер, нужны при API-запросах. Проверяйте "методом тыка". 48 | * **Можно сделать только часть заданий в любой последовательности**, оценка будет проставлена согласно разбалловке выше. Это нормально, заданий специально с запасом. 49 | * Тесты *НЕ* должны быть зависимыми. Генерируйте уникальные названия (через uuid, через время с секундами или как-то иначе) - это вам позволит определить, какие сущности вы создаете и легко их найти. 50 | * Тесты **ОБЯЗАТЕЛЬНО** должны что-то проверять. Например если мы что-то создали - необходимо проверить, что оно создалось. При необходимости выносить части кода в отдельные методы. 51 | * Если вам нужно что-то удалять - сначала создайте этот объект в тесте, а уже затем его удаляйте. 52 | * Все тесты **ДОЛЖНЫ** проходить, как в один поток, так и в параллельном режиме (не конфликтовать). 53 | * Тесты **ДОЛЖНЫ** уметь запускаться несколько раз подряд, т.е. чтобы данные в тестах никак не конфликтовали. 54 | 55 | 56 | #### Срок сдачи ДЗ 57 | До 31 октября (включительно) -------------------------------------------------------------------------------- /lection10-Appium/Lection_mobile_1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection10-Appium/Lection_mobile_1.pdf -------------------------------------------------------------------------------- /lection10-Appium/Lection_mobile_2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection10-Appium/Lection_mobile_2.pdf -------------------------------------------------------------------------------- /lection10-Appium/code_appium/api/wikipedia.py: -------------------------------------------------------------------------------- 1 | """ 2 | watch.py 3 | 4 | MediaWiki API Demos 5 | Demo of `Watch` module: Add a page to your watchlist 6 | MIT license 7 | 8 | for creating bots: https://en.wikipedia.org/wiki/Special:BotPasswords 9 | 10 | https://www.mediawiki.org/wiki/API:Watch 11 | 12 | 13 | """ 14 | 15 | import requests 16 | 17 | 18 | class WikipediaApi: 19 | api_url = "https://en.wikipedia.org/w/api.php" 20 | session = requests.Session() 21 | 22 | def retrieve_login_token(self): 23 | params = { 24 | "action": "query", 25 | "meta": "tokens", 26 | "type": "login", 27 | "format": "json" 28 | } 29 | request = self.session.get(url=self.api_url, params=params) 30 | data = request.json() 31 | login_token = data["query"]["tokens"]["logintoken"] 32 | return login_token 33 | 34 | def send_post_to_log_in(self): 35 | """ 36 | # method, Obtain credentials by first visiting 37 | https://www.en.wikipedia.org/wiki/Special:BotPasswords 38 | See https://www.mediawiki.org/wiki/API:Login for more 39 | information on log in methods. 40 | """ 41 | params = { 42 | "action": "login", 43 | 'lgname': "Testusername1090@educationmailru", 44 | 'lgpassword': "6m387r983fof7l706m082vl5l9posqat", 45 | "format": "json", 46 | "lgtoken": self.retrieve_login_token() 47 | } 48 | self.session.post(url=self.api_url, data=params) 49 | 50 | def get_csrf_token(self): 51 | self.send_post_to_log_in() 52 | params = { 53 | "action": "query", 54 | "meta": "tokens", 55 | "type": "watch", 56 | "format": "json" 57 | } 58 | request = self.session.get(url=self.api_url, params=params) 59 | data = request.json() 60 | csrf_token = data["query"]["tokens"]["watchtoken"] 61 | return csrf_token 62 | 63 | def send_request_delete_watchlist(self): 64 | params = { 65 | "action": "watch", 66 | "titles": "Iron Maiden" + "|" + "Judas Priest", 67 | "unwatch": "", 68 | "format": "json", 69 | "token": self.get_csrf_token(), 70 | } 71 | session = self.session.post(url=self.api_url, data=params) 72 | return session.status_code 73 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import allure 4 | from api.wikipedia import WikipediaApi 5 | 6 | from ui.fixtures import * 7 | 8 | 9 | def pytest_addoption(parser): 10 | parser.addoption('--browser', default='chrome') 11 | parser.addoption('--url', default='https://en.wikipedia.org/') 12 | parser.addoption('--os', default='web') 13 | parser.addoption('--debug_log', action='store_true') 14 | parser.addoption('--appium', default='http://127.0.0.1:4723/wd/hub') 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def config(request): 19 | browser = request.config.getoption('--browser') 20 | device_os = request.config.getoption('--os') 21 | appium = request.config.getoption('--appium') 22 | if device_os == 'mw': 23 | url = 'https://en.m.wikipedia.org/' 24 | elif device_os == 'web': 25 | url = 'https://en.wikipedia.org/' 26 | else: 27 | url = request.config.getoption('--url') 28 | debug_log = request.config.getoption('--debug_log') 29 | return {'url': url, 'browser': browser, 'device_os': device_os, 'debug_log': debug_log, 'appium': appium} 30 | 31 | 32 | @pytest.fixture(scope='session') 33 | def repo_root(): 34 | return os.path.abspath(os.path.join(__file__, os.pardir)) 35 | 36 | 37 | def pytest_configure(config): 38 | config.addinivalue_line( 39 | "markers", "skip_platform: skip test for necessary platform ", 40 | ) 41 | 42 | if sys.platform.startswith('win'): 43 | base_test_dir = 'C:\\tests' 44 | else: 45 | base_test_dir = '/tmp/tests' 46 | 47 | if not hasattr(config, 'workerinput'): # execute only once on main worker 48 | if os.path.exists(base_test_dir): 49 | shutil.rmtree(base_test_dir) 50 | os.makedirs(base_test_dir) 51 | if WikipediaApi().send_request_delete_watchlist() == 200: 52 | print("===========Watchlist was cleared===========\n") 53 | else: 54 | print("===========Watchlist WAS NOT cleared===========\n") 55 | 56 | # save to config for all workers 57 | config.base_test_dir = base_test_dir 58 | 59 | 60 | @pytest.fixture(scope='function') 61 | def test_dir(request): 62 | test_name = request._pyfuncitem.nodeid.replace('/', '_').replace(':', '_') 63 | test_dir = os.path.join(request.config.base_test_dir, test_name) 64 | os.makedirs(test_dir) 65 | return test_dir 66 | 67 | 68 | @pytest.fixture(scope='function') 69 | def logger(test_dir, config): 70 | log_formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s') 71 | log_file = os.path.join(test_dir, 'test.log') 72 | log_level = logging.DEBUG if config['debug_log'] else logging.INFO 73 | 74 | file_handler = logging.FileHandler(log_file, 'w') 75 | file_handler.setFormatter(log_formatter) 76 | file_handler.setLevel(log_level) 77 | 78 | log = logging.getLogger('test') 79 | log.propagate = False 80 | log.setLevel(log_level) 81 | log.handlers.clear() 82 | log.addHandler(file_handler) 83 | 84 | yield log 85 | 86 | for handler in log.handlers: 87 | handler.close() 88 | 89 | 90 | @pytest.fixture(autouse=True) 91 | def skip_by_platform(request, config): 92 | if request.node.get_closest_marker('skip_platform'): 93 | if request.node.get_closest_marker('skip_platform').args[0] == config['device_os']: 94 | pytest.skip('skipped on this platform: {}'.format(config['device_os'])) 95 | 96 | 97 | @pytest.fixture(scope='session', autouse=True) 98 | def add_allure_environment_property(request, config): 99 | """ 100 | В зависимости от типа девайса добавляем в наши environment аллюра 101 | environment.properties должен лежать внутри директории файлов allure в виде словаря 102 | """ 103 | alluredir = request.config.getoption('--alluredir') 104 | if alluredir: 105 | env_props = dict() 106 | if config['device_os'] in ('web', 'mw'): 107 | env_props['Browser'] = 'Chrome' 108 | else: 109 | env_props['Appium'] = '1.20' 110 | env_props['Android_emulator'] = '11.0' 111 | 112 | if not os.path.exists(alluredir): 113 | os.makedirs(alluredir) 114 | 115 | allure_env_path = os.path.join(alluredir, 'environment.properties') 116 | 117 | with open(allure_env_path, 'w') as f: 118 | for key, value in list(env_props.items()): 119 | f.write(f'{key}={value}\n') 120 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | UI: mark a test as UI testsuite 4 | MWUI: mark a test as MobileWebUI testsuite 5 | AndroidUI: mark a test as AndroidUI testsuite -------------------------------------------------------------------------------- /lection10-Appium/code_appium/stuff/Wikipedia_v2.7.50337.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection10-Appium/code_appium/stuff/Wikipedia_v2.7.50337.apk -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/capability.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | import os 3 | 4 | 5 | def capability_select(device_os): 6 | if device_os == 'web': 7 | capability = webdriver.ChromeOptions() 8 | capability.add_experimental_option("excludeSwitches", ["enable-logging"]) 9 | elif device_os == 'mw': 10 | mobile_emulation = {"deviceName": "Pixel 2"} 11 | # если хотите задать конкретные хар-ки устройства 12 | # подробнее: https://chromedriver.chromium.org/mobile-emulation 13 | # mobile_emulation = {"deviceMetrics": {"width": 360, "height": 640, "pixelRatio": 3.0}, 14 | # "userAgent": "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) " 15 | # "AppleWebKit/535.19 (KHTML, like Gecko) " 16 | # "Chrome/18.0.1025.166 Mobile Safari/535.19"} 17 | capability = webdriver.ChromeOptions() 18 | capability.add_experimental_option("mobileEmulation", mobile_emulation) 19 | capability.add_experimental_option("excludeSwitches", ["enable-logging"]) 20 | elif device_os == 'android': 21 | capability = {"platformName": "Android", 22 | "platformVersion": "11.0", 23 | "automationName": "Appium", 24 | "appPackage": "org.wikipedia", 25 | "appActivity": ".main.MainActivity", 26 | "app": os.path.abspath(os.path.join(os.path.dirname(__file__), 27 | '../stuff/Wikipedia_v2.7.50337.apk') 28 | ), 29 | "orientation": "PORTRAIT" 30 | } 31 | else: 32 | raise ValueError("Incorrect device os type") 33 | return capability 34 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import allure 5 | import pytest 6 | from appium import webdriver 7 | from selenium import webdriver as wd 8 | from ui.pages.base_page import BasePage 9 | from ui import pages 10 | 11 | from webdriver_manager.chrome import ChromeDriverManager 12 | from ui.capability import capability_select 13 | 14 | 15 | class UnsupportedBrowserType(Exception): 16 | pass 17 | 18 | 19 | @pytest.fixture 20 | def base_page(driver, config): 21 | return BasePage(driver=driver, config=config) 22 | 23 | 24 | @pytest.fixture 25 | def login_page(driver, config): 26 | page = get_page(config['device_os'], 'LoginPage') 27 | return page(driver=driver, config=config) 28 | 29 | 30 | @pytest.fixture 31 | def main_page(driver, config): 32 | page = get_page(config['device_os'], 'MainPage') 33 | return page(driver=driver, config=config) 34 | 35 | 36 | @pytest.fixture 37 | def search_page(driver, config): 38 | page = get_page(config['device_os'], 'SearchPage') 39 | return page(driver=driver, config=config) 40 | 41 | 42 | @pytest.fixture 43 | def title_page(driver, config): 44 | page = get_page(config['device_os'], 'TitlePage') 45 | return page(driver=driver, config=config) 46 | 47 | 48 | @pytest.fixture 49 | def title_list_page(driver, config): 50 | page = get_page(config['device_os'], 'TitleListPage') 51 | return page(driver=driver, config=config) 52 | 53 | 54 | def get_page(device, page_class): 55 | if device == 'mw': 56 | page_class += 'MW' 57 | elif device == 'android': 58 | page_class += 'ANDROID' 59 | page = getattr(pages, page_class, None) 60 | if page is None: 61 | raise Exception(f'No such page {page_class}') 62 | return page 63 | 64 | 65 | def get_driver(browser_name, device_os, download_dir, appium_url): 66 | if device_os in ['web', 'mw']: 67 | manager = ChromeDriverManager(version='106.0.5249.21') 68 | if browser_name == 'chrome': 69 | browser = wd.Chrome(executable_path=manager.install(), 70 | options=capability_select(device_os)) 71 | elif device_os == 'mw': 72 | browser = wd.Chrome(executable_path=manager.install(), 73 | options=capability_select(device_os)) 74 | else: 75 | raise UnsupportedBrowserType(f' Unsupported browser {browser_name}') 76 | elif device_os == 'android': 77 | desired_caps = capability_select(device_os) 78 | driver = webdriver.Remote(appium_url, desired_capabilities=desired_caps) 79 | return driver 80 | else: 81 | raise UnsupportedBrowserType(f' Unsupported device_os type {device_os}') 82 | return browser 83 | 84 | 85 | @pytest.fixture(scope='function') 86 | def driver(config, test_dir): 87 | url = config['url'] 88 | browser_name = config['browser'] 89 | device_os = config['device_os'] 90 | appium_url = config['appium'] 91 | browser = get_driver(browser_name, device_os, test_dir, appium_url) 92 | if device_os != 'android': 93 | browser.get(url) 94 | yield browser 95 | browser.quit() 96 | 97 | 98 | @pytest.fixture(scope='function') 99 | def ui_report(driver, request, test_dir, config): 100 | failed_tests_count = request.session.testsfailed 101 | yield 102 | if request.session.testsfailed > failed_tests_count: 103 | screenshot_file = os.path.join(test_dir, 'failure.png') 104 | driver.get_screenshot_as_file(screenshot_file) 105 | allure.attach.file(screenshot_file, 'failure.png', attachment_type=allure.attachment_type.PNG) 106 | 107 | if config['device_os'] != 'android': 108 | browser_log = os.path.join(test_dir, 'browser.log') 109 | with open(browser_log, 'w') as f: 110 | for i in driver.get_log('browser'): 111 | f.write(f"{i['level']} - {i['source']}\n{i['message']}\n") 112 | 113 | with open(browser_log, 'r') as f: 114 | allure.attach(f.read(), 'browser.log', attachment_type=allure.attachment_type.TEXT) 115 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/locators/locators_android.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | from appium.webdriver.common.mobileby import MobileBy 3 | 4 | 5 | class BasePageANDROIDLocators: 6 | pass 7 | 8 | 9 | class LoginPageANDROIDLocators(BasePageANDROIDLocators): 10 | pass 11 | 12 | 13 | class MainPageANDROIDLocators(BasePageANDROIDLocators): 14 | SKIP_BUTTON = (By.ID, 'org.wikipedia:id/fragment_onboarding_skip_button') 15 | SEARCH_ICON = (MobileBy.ACCESSIBILITY_ID, 'Search Wikipedia') 16 | MY_LISTS = (MobileBy.ACCESSIBILITY_ID, 'My lists') 17 | # MY_LISTS = (By.XPATH, '//android.widget.FrameLayout[@content-desc="My lists"]/android.widget.ImageView') 18 | 19 | 20 | class SearchPageANDROIDLocators(BasePageANDROIDLocators): 21 | SEARCH_FIELD = (MobileBy.ID, 'org.wikipedia:id/search_src_text') 22 | LIST_ITEM_TITLE = (MobileBy.ID, 'org.wikipedia:id/page_list_item_title') 23 | 24 | 25 | class TitlePageANDROIDLocators(BasePageANDROIDLocators): 26 | MENU_BOOKMARK = (MobileBy.ID, 'org.wikipedia:id/article_menu_bookmark') 27 | PAGE_TOOLBAR = (MobileBy.ID, 'org.wikipedia:id/page_toolbar_button_search') 28 | OVERFLOW_MENU = (MobileBy.ACCESSIBILITY_ID, 'More options') 29 | OVERFLOW_FEED = (MobileBy.ID, 'org.wikipedia:id/overflow_feed') 30 | 31 | 32 | class TitleListPageANDROIDLocators(BasePageANDROIDLocators): 33 | ITEM_TITLE = (By.ID, 'org.wikipedia:id/item_title') 34 | FIRST_ELEMENT = (By.XPATH, '//android.view.ViewGroup[2]/android.widget.TextView[1]') 35 | ELEMENT = "//android.widget.TextView[contains(@text, '{}')][1]" 36 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/locators/locators_mw.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageMWLocators: 5 | pass 6 | 7 | 8 | class LoginPageMWLocators(BasePageMWLocators): 9 | LOGIN_FIELD = (By.ID, 'wpName1') 10 | PASSWORD_FIELD = (By.ID, 'wpPassword1') 11 | LOGIN_BUTTON = (By.ID, 'wpLoginAttempt') 12 | 13 | 14 | class MainPagePageMWLocators(BasePageMWLocators): 15 | MAIN_MENU = (By.ID, 'mw-mf-main-menu-button') 16 | WATCH_LIST = (By.XPATH, "//span[text()='Watchlist']") 17 | SEARCH_ICON = (By.ID, 'searchIcon') 18 | 19 | 20 | class SearchPagePageMWLocators(BasePageMWLocators): 21 | SEARCH_FIELD = (By.XPATH, '//input[@class="search mw-ui-background-icon-search"]') 22 | ELEMENT_WITH_DESC = '//div[text()="{}"]' 23 | 24 | 25 | class TitlePagePageMWLocators(BasePageMWLocators): 26 | STAR_BUTTON = (By.ID, 'ca-watch') 27 | 28 | 29 | class TitleListPagePageMWLocators(BasePageMWLocators): 30 | MARKED_TITLE = (By.XPATH, "//a[contains(@class,'icon-wikimedia-unStar-progressive')]") 31 | TITLE_NAME = "//h3[text()='{}']" 32 | ELEMENT = "//h1[text()='{}']" 33 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/locators/locators_web.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | pass 6 | 7 | 8 | class LoginLocators(BasePageLocators): 9 | LOGIN_FIELD = (By.ID, 'wpName1') 10 | PASSWORD_FIELD = (By.ID, 'wpPassword1') 11 | LOGIN_BUTTON = (By.ID, 'wpLoginAttempt') 12 | 13 | 14 | class MainPageLocators(BasePageLocators): 15 | pass 16 | 17 | 18 | class SearchPageLocators(BasePageLocators): 19 | pass 20 | 21 | 22 | class TitlePageLocators(BasePageLocators): 23 | pass 24 | 25 | 26 | class TitleListPageLocators(BasePageLocators): 27 | pass 28 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.pages.main_page import MainPage, MainPageMW, MainPageANDROID 3 | from ui.pages.login_page import LoginPage, LoginPageMW, LoginPageANDROID 4 | from ui.pages.search_page import SearchPage, SearchPageMW, SearchPageANDROID 5 | from ui.pages.title_page import TitlePage, TitlePageMW,TitlePageANDROID 6 | from ui.pages.title_list_page import TitleListPage, TitleListPageMW, TitleListPageANDROID 7 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/pages/login_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import LoginLocators 3 | from ui.locators.locators_mw import LoginPageMWLocators 4 | from ui.locators.locators_android import LoginPageANDROIDLocators 5 | import allure 6 | 7 | 8 | class LoginPage(BasePage): 9 | locators = LoginLocators() 10 | 11 | login_url = "https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page" 12 | 13 | @allure.step("Авторизовываемся") 14 | def login(self, login="testusername1090", password="11qazert"): 15 | self.driver.get(self.login_url) 16 | 17 | self.find(self.locators.LOGIN_FIELD).send_keys(login) 18 | self.find(self.locators.PASSWORD_FIELD).send_keys(password) 19 | self.click(self.locators.LOGIN_BUTTON) 20 | 21 | 22 | class LoginPageMW(LoginPage): 23 | locators = LoginPageMWLocators() 24 | login_url = "https://en.m.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page" 25 | 26 | @allure.step("Авторизовываемся") 27 | def login(self, login="testusername1090", password="11qazert"): 28 | super(LoginPageMW, self).login() 29 | 30 | 31 | class LoginPageANDROID(LoginPage): 32 | locators = LoginPageANDROIDLocators() 33 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import MainPageLocators 3 | from ui.locators.locators_mw import MainPagePageMWLocators 4 | from ui.locators.locators_android import MainPageANDROIDLocators 5 | import allure 6 | 7 | 8 | class MainPage(BasePage): 9 | locators = MainPageLocators() 10 | 11 | def click_on_search_button(self): 12 | pass 13 | 14 | def open_menu_button(self): 15 | pass 16 | 17 | def open_watchlist(self): 18 | pass 19 | 20 | def skip_start_window(self): 21 | pass 22 | 23 | def click_on_lists_button(self): 24 | pass 25 | 26 | 27 | class MainPageMW(MainPage): 28 | locators = MainPagePageMWLocators() 29 | 30 | @allure.step("Нажимаем на кнопку поиска") 31 | def click_on_search_button(self): 32 | self.click(self.locators.SEARCH_ICON) 33 | 34 | @allure.step("Нажимаем на кнопку открытия меню (mobile)") 35 | def open_menu_button(self): 36 | self.click(self.locators.MAIN_MENU) 37 | 38 | @allure.step("Нажимаем на кнопку открытия меню (mobile)") 39 | def open_watchlist(self): 40 | self.click(self.locators.WATCH_LIST) 41 | 42 | 43 | class MainPageANDROID(MainPage): 44 | locators = MainPageANDROIDLocators() 45 | 46 | @allure.step("Пропускаем стартовое окно") 47 | def skip_start_window(self): 48 | self.click_for_android(self.locators.SKIP_BUTTON) 49 | 50 | @allure.step("Нажимаем на кнопку поиска") 51 | def click_on_search_button(self): 52 | self.click_for_android(self.locators.SEARCH_ICON) 53 | 54 | def click_on_lists_button(self): 55 | self.click_for_android(self.locators.MY_LISTS) 56 | 57 | 58 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/pages/search_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | from ui.pages.base_page import BasePage 3 | from ui.locators.locators_web import SearchPageLocators 4 | from ui.locators.locators_mw import SearchPagePageMWLocators 5 | from ui.locators.locators_android import SearchPageANDROIDLocators 6 | import allure 7 | 8 | 9 | class SearchPage(BasePage): 10 | locators = SearchPageLocators() 11 | 12 | def send_text_to_search_field_and_click_to_element(self, text, desc): 13 | pass 14 | 15 | def enter_value_in_search_field(self, text): 16 | pass 17 | 18 | def enter_value_in_search_field_and_click_on_first(self, text): 19 | pass 20 | 21 | 22 | class SearchPageMW(SearchPage): 23 | locators = SearchPagePageMWLocators() 24 | 25 | @allure.step("Вводим текст и нажимаем на нужный элемент") 26 | def send_text_to_search_field_and_click_to_element(self, text, desc): 27 | self.find(self.locators.SEARCH_FIELD).send_keys(text) 28 | self.find((By.XPATH, self.locators.ELEMENT_WITH_DESC.format(desc))).click() 29 | 30 | 31 | class SearchPageANDROID(SearchPage): 32 | locators = SearchPageANDROIDLocators() 33 | 34 | @allure.step("Вводим значение в поле поиска") 35 | def enter_value_in_search_field(self, text): 36 | self.find(self.locators.SEARCH_FIELD).send_keys(text) 37 | self.driver.hide_keyboard() 38 | 39 | @allure.step("Вводим значение в поле поиска и кликаем на первое") 40 | def enter_value_in_search_field_and_click_on_first(self, text): 41 | self.enter_value_in_search_field(text) 42 | self.click_for_android(self.locators.LIST_ITEM_TITLE) 43 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/pages/title_list_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import TitleListPageLocators 3 | from ui.locators.locators_mw import TitleListPagePageMWLocators 4 | from ui.locators.locators_android import TitleListPageANDROIDLocators 5 | from selenium.webdriver.common.by import By 6 | import time 7 | import allure 8 | 9 | 10 | class TitleListPage(BasePage): 11 | locators = TitleListPageLocators() 12 | 13 | def check_delete_element_from_saved_title_list(self, text): 14 | pass 15 | 16 | 17 | class TitleListPageMW(TitleListPage): 18 | locators = TitleListPagePageMWLocators() 19 | 20 | def check_delete_element_from_saved_title_list(self, text): 21 | with allure.step("Нажимаем на 'звездочку'"): 22 | self.click(self.locators.MARKED_TITLE) 23 | time.sleep(1) 24 | with allure.step("Обновляем страницу и нажимаем на оставшуюся статью"): 25 | self.driver.refresh() 26 | self.click((By.XPATH, self.locators.TITLE_NAME.format(text))) 27 | with allure.step("Проверяем, что оставшаяся статья соответствует нужной"): 28 | self.find((By.XPATH, self.locators.ELEMENT.format(text))) 29 | 30 | 31 | class TitleListPageANDROID(TitleListPage): 32 | locators = TitleListPageANDROIDLocators() 33 | 34 | @allure.step("Удаляем из избранного первую статью, проверяем, что вторая осталась в списке") 35 | def check_delete_element_from_saved_title_list(self, text): 36 | with allure.step("Удаляем первую статью из списка"): 37 | self.click_for_android(self.locators.ITEM_TITLE) 38 | self.swipe_element_lo_left(self.locators.FIRST_ELEMENT) 39 | with allure.step("Проверяем, что оставшаяся статья соответствует нужной"): 40 | self.find((By.XPATH, self.locators.ELEMENT.format(text))) 41 | 42 | 43 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/ui/pages/title_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import TitlePageLocators 3 | from ui.locators.locators_mw import TitlePagePageMWLocators 4 | from ui.locators.locators_android import TitlePageANDROIDLocators 5 | import allure 6 | 7 | 8 | class TitlePage(BasePage): 9 | locators = TitlePageLocators() 10 | 11 | def add_to_bookmark(self): 12 | pass 13 | 14 | def click_on_toolbar_button(self): 15 | pass 16 | 17 | def click_on_overflow_menu(self): 18 | pass 19 | 20 | def click_on_overflow_feed(self): 21 | pass 22 | 23 | 24 | class TitlePageMW(TitlePage): 25 | locators = TitlePagePageMWLocators() 26 | 27 | @allure.step("Добавляем статью в избранное") 28 | def add_to_bookmark(self): 29 | self.click(self.locators.STAR_BUTTON) 30 | 31 | 32 | class TitlePageANDROID(TitlePage): 33 | locators = TitlePageANDROIDLocators() 34 | 35 | def add_to_bookmark(self): 36 | self.click_for_android(self.locators.MENU_BOOKMARK) 37 | 38 | def click_on_toolbar_button(self): 39 | self.click_for_android(self.locators.PAGE_TOOLBAR) 40 | 41 | def click_on_overflow_menu(self): 42 | self.click_for_android(self.locators.OVERFLOW_MENU) 43 | 44 | def click_on_overflow_feed(self): 45 | self.click_for_android(self.locators.OVERFLOW_FEED) 46 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/utils/decorators.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class TimeoutException(Exception): 5 | pass 6 | 7 | 8 | def wait(method, error=Exception, timeout=10, interval=0.5, check=False, **kwargs): 9 | started = time.time() 10 | last_exception = None 11 | while time.time() - started < timeout: 12 | try: 13 | result = method(**kwargs) 14 | if check: 15 | if result: 16 | return result 17 | last_exception = f'Method {method.__name__} returned {result}' 18 | else: 19 | return result 20 | except error as e: 21 | last_exception = e 22 | 23 | time.sleep(interval) 24 | 25 | raise TimeoutException(f'Method {method.__name__} timeout out in {timeout}sec with exception: {last_exception}') 26 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/wiki_tests/android_tests/test_android_wikipedia.py: -------------------------------------------------------------------------------- 1 | import time 2 | import allure 3 | import pytest 4 | from wiki_tests.base import BaseCase 5 | 6 | 7 | class TestWikipediaAndroid(BaseCase): 8 | 9 | @pytest.mark.AndroidUI 10 | def test_skip_start_window(self): 11 | self.main_page.skip_start_window() 12 | 13 | @pytest.mark.AndroidUI 14 | def test_saving_two_articles(self): 15 | text1 = 'Iron Maiden' 16 | text2 = 'Judas Priest' 17 | self.main_page.skip_start_window() 18 | self.main_page.click_on_search_button() 19 | self.search_page.enter_value_in_search_field_and_click_on_first(text1) 20 | self.title_page.add_to_bookmark() 21 | self.title_page.click_on_toolbar_button() 22 | self.search_page.enter_value_in_search_field_and_click_on_first(text2) 23 | self.title_page.add_to_bookmark() 24 | self.title_page.click_on_overflow_menu() 25 | self.title_page.click_on_overflow_feed() 26 | self.main_page.click_on_lists_button() 27 | self.title_list_page.check_delete_element_from_saved_title_list(text2) 28 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/wiki_tests/base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pytest.fixtures import FixtureRequest 3 | 4 | from ui.pages.base_page import BasePage 5 | from ui.pages.main_page import MainPage 6 | from ui.pages.search_page import SearchPage 7 | from ui.pages.login_page import LoginPage 8 | from ui.pages.title_page import TitlePage 9 | from ui.pages.title_list_page import TitleListPage 10 | 11 | 12 | class BaseCase: 13 | 14 | @pytest.fixture(scope='function', autouse=True) 15 | def setup(self, driver, config, request: FixtureRequest, logger, ui_report): 16 | self.driver = driver 17 | self.config = config 18 | self.logger = logger 19 | 20 | self.base_page: BasePage = request.getfixturevalue('base_page') 21 | self.login_page: LoginPage = request.getfixturevalue('login_page') 22 | self.main_page: MainPage = request.getfixturevalue('main_page') 23 | self.search_page: SearchPage = request.getfixturevalue('search_page') 24 | self.title_page: TitlePage = request.getfixturevalue('title_page') 25 | self.title_list_page: TitleListPage = request.getfixturevalue('title_list_page') 26 | 27 | self.logger.debug('Initial setup done!') 28 | -------------------------------------------------------------------------------- /lection10-Appium/code_appium/wiki_tests/web_tests/test_web_wikipedia.py: -------------------------------------------------------------------------------- 1 | import time 2 | import allure 3 | import pytest 4 | from wiki_tests.base import BaseCase 5 | 6 | 7 | class TestWikipediaWeb(BaseCase): 8 | # @pytest.mark.skip(reason='TEMP') 9 | @pytest.mark.MWUI 10 | @pytest.mark.UI 11 | @allure.severity(allure.severity_level.NORMAL) 12 | @allure.epic("Awesome PyTest framework") 13 | @allure.title("Test login") 14 | @allure.description("Test for login") 15 | @allure.feature('Login tests') 16 | def test_login(self): 17 | self.login_page.login() 18 | 19 | # @pytest.mark.skip(reason='TEMP') 20 | @pytest.mark.MWUI 21 | @pytest.mark.skip_platform('web') 22 | @allure.epic("Awesome PyTest framework") 23 | @allure.title("Save two articles") 24 | @allure.description("Test of saving 2 articles and deleting one of them from list") 25 | @allure.feature('Saving tests') 26 | def test_saving_two_articles(self): 27 | """ 28 | Тестируем сохранение двух статей и последующее удаление одной из них из списка 29 | Шаги: 30 | - авторизуемся 31 | - нажимаем на кнопку Search 32 | - нажимаем и вводим значение в поле ввода 33 | - добавляем статью в список сохраненных 34 | - производим повторный поиск и добавляем еще одну статью в список сохраненных 35 | - возвращаемся на главный экран, переходим в список статей 36 | - удаляем первую статью 37 | - кликаем на оставшуюся статью, переходим в нее, проверяем что ее название = заданному 38 | Ожидаемый результат: в сохраненных списках осталась одна статья, при переходе на которую title = Judas Priest 39 | """ 40 | text1 = 'Iron Maiden' 41 | text2 = 'Judas Priest' 42 | self.login_page.login() 43 | with allure.step("Нажимаем на кнопку Search и вводим значения в поле ввода"): 44 | self.main_page.click_on_search_button() 45 | self.search_page.send_text_to_search_field_and_click_to_element(text1, "English heavy metal band") 46 | with allure.step("Добавляем статью в закладки"): 47 | self.title_page.add_to_bookmark() 48 | time.sleep(3) 49 | with allure.step("Нажимаем на кнопку Search и вводим значения в поле ввода"): 50 | self.main_page.click_on_search_button() 51 | self.search_page.send_text_to_search_field_and_click_to_element(text2, "British heavy metal band") 52 | with allure.step("Добавляем статью в закладки"): 53 | self.title_page.add_to_bookmark() 54 | time.sleep(3) 55 | with allure.step("Открываем список избранного"): 56 | self.main_page.open_menu_button() 57 | self.main_page.open_watchlist() 58 | self.title_list_page.check_delete_element_from_saved_title_list(text1) 59 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/api/wikipedia.py: -------------------------------------------------------------------------------- 1 | """ 2 | watch.py 3 | 4 | MediaWiki API Demos 5 | Demo of `Watch` module: Add a page to your watchlist 6 | MIT license 7 | 8 | for creating bots: https://en.wikipedia.org/wiki/Special:BotPasswords 9 | 10 | https://www.mediawiki.org/wiki/API:Watch 11 | 12 | 13 | """ 14 | 15 | import requests 16 | 17 | 18 | class WikipediaApi: 19 | api_url = "https://en.wikipedia.org/w/api.php" 20 | session = requests.Session() 21 | 22 | def retrieve_login_token(self): 23 | params = { 24 | "action": "query", 25 | "meta": "tokens", 26 | "type": "login", 27 | "format": "json" 28 | } 29 | request = self.session.get(url=self.api_url, params=params) 30 | data = request.json() 31 | login_token = data["query"]["tokens"]["logintoken"] 32 | return login_token 33 | 34 | def send_post_to_log_in(self): 35 | """ 36 | # method, Obtain credentials by first visiting 37 | https://www.en.wikipedia.org/wiki/Special:BotPasswords 38 | See https://www.mediawiki.org/wiki/API:Login for more 39 | information on log in methods. 40 | """ 41 | params = { 42 | "action": "login", 43 | 'lgname': "Testusername1090@educationmailru", 44 | 'lgpassword': "6m387r983fof7l706m082vl5l9posqat", 45 | "format": "json", 46 | "lgtoken": self.retrieve_login_token() 47 | } 48 | self.session.post(url=self.api_url, data=params) 49 | 50 | def get_csrf_token(self): 51 | self.send_post_to_log_in() 52 | params = { 53 | "action": "query", 54 | "meta": "tokens", 55 | "type": "watch", 56 | "format": "json" 57 | } 58 | request = self.session.get(url=self.api_url, params=params) 59 | data = request.json() 60 | csrf_token = data["query"]["tokens"]["watchtoken"] 61 | return csrf_token 62 | 63 | def send_request_delete_watchlist(self): 64 | params = { 65 | "action": "watch", 66 | "titles": "Parkway Drive" + "|" + "Judas Priest", 67 | "unwatch": "", 68 | "format": "json", 69 | "token": self.get_csrf_token(), 70 | } 71 | session = self.session.post(url=self.api_url, data=params) 72 | return session.status_code 73 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import allure 4 | from api.wikipedia import WikipediaApi 5 | 6 | from ui.fixtures import * 7 | 8 | 9 | def pytest_addoption(parser): 10 | parser.addoption('--browser', default='chrome') 11 | parser.addoption('--url', default='https://en.wikipedia.org/') 12 | parser.addoption('--os', default='web') 13 | parser.addoption('--debug_log', action='store_true') 14 | 15 | 16 | @pytest.fixture(scope='session') 17 | def config(request): 18 | browser = request.config.getoption('--browser') 19 | device_os = request.config.getoption('--os') 20 | if device_os == 'mw': 21 | url = 'https://en.m.wikipedia.org/' 22 | elif device_os == 'web': 23 | url = 'https://en.wikipedia.org/' 24 | else: 25 | url = request.config.getoption('--url') 26 | debug_log = request.config.getoption('--debug_log') 27 | return {'url': url, 'browser': browser, 'device_os': device_os, 'debug_log': debug_log} 28 | 29 | 30 | @pytest.fixture(scope='session') 31 | def repo_root(): 32 | return os.path.abspath(os.path.join(__file__, os.path.pardir)) 33 | 34 | 35 | def pytest_configure(config): 36 | config.addinivalue_line( 37 | "markers", "skip_platform: skip test for necessary platform ", 38 | ) 39 | 40 | if sys.platform.startswith('win'): 41 | base_test_dir = 'C:\\tests' 42 | else: 43 | base_test_dir = '/tmp/tests' 44 | 45 | if not hasattr(config, 'workerinput'): # execute only once on main worker 46 | if os.path.exists(base_test_dir): 47 | shutil.rmtree(base_test_dir) 48 | os.makedirs(base_test_dir) 49 | 50 | if WikipediaApi().send_request_delete_watchlist() == 200: 51 | print("===========Watchlist was cleared===========\n") 52 | else: 53 | print("===========Watchlist WAS NOT cleared===========\n") 54 | 55 | # save to config for all workers 56 | config.base_test_dir = base_test_dir 57 | 58 | 59 | @pytest.fixture(scope='function') 60 | def test_dir(request): 61 | test_name = request._pyfuncitem.nodeid.replace('/', '_').replace(':', '_') 62 | test_dir = os.path.join(request.config.base_test_dir, test_name) 63 | os.makedirs(test_dir) 64 | return test_dir 65 | 66 | 67 | @pytest.fixture(scope='function') 68 | def logger(test_dir, config): 69 | log_formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s') 70 | log_file = os.path.join(test_dir, 'test.log') 71 | log_level = logging.DEBUG if config['debug_log'] else logging.INFO 72 | 73 | file_handler = logging.FileHandler(log_file, 'w') 74 | file_handler.setFormatter(log_formatter) 75 | file_handler.setLevel(log_level) 76 | 77 | log = logging.getLogger('test') 78 | log.propagate = False 79 | log.setLevel(log_level) 80 | log.handlers.clear() 81 | log.addHandler(file_handler) 82 | 83 | yield log 84 | 85 | for handler in log.handlers: 86 | handler.close() 87 | 88 | 89 | @pytest.fixture(autouse=True) 90 | def skip_by_platform(request, config): 91 | if request.node.get_closest_marker('skip_platform'): 92 | if request.node.get_closest_marker('skip_platform').args[0] == config['device_os']: 93 | pytest.skip('skipped on this platform: {}'.format(config['device_os'])) 94 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | UI: mark a test as UI testsuite 4 | MWUI: mark a test as MobileWebUI testsuite -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/capability.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | 3 | 4 | def capability_select(device_os, capability=None): 5 | if device_os == 'web': 6 | capability = webdriver.ChromeOptions() 7 | capability.add_experimental_option("excludeSwitches", ["enable-logging"]) 8 | elif device_os == 'mw': 9 | mobile_emulation = {"deviceName": "Pixel 2"} 10 | # если хотите задать конкретные хар-ки устройства 11 | # подробнее: https://chromedriver.chromium.org/mobile-emulation 12 | # mobile_emulation = {"deviceMetrics": {"width": 360, "height": 640, "pixelRatio": 3.0}, 13 | # "userAgent": "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) " 14 | # "AppleWebKit/535.19 (KHTML, like Gecko) " 15 | # "Chrome/18.0.1025.166 Mobile Safari/535.19"} 16 | capability = webdriver.ChromeOptions() 17 | capability.add_experimental_option("mobileEmulation", mobile_emulation) 18 | capability.add_experimental_option("excludeSwitches", ["enable-logging"]) 19 | 20 | return capability 21 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import allure 4 | import pytest 5 | from selenium import webdriver 6 | from ui.pages.base_page import BasePage 7 | from ui import pages 8 | from ui.capability import capability_select 9 | 10 | from webdriver_manager.chrome import ChromeDriverManager 11 | 12 | 13 | class UnsupportedBrowserType(Exception): 14 | pass 15 | 16 | 17 | @pytest.fixture 18 | def base_page(driver, config): 19 | return BasePage(driver=driver, config=config) 20 | 21 | 22 | @pytest.fixture 23 | def login_page(driver, config): 24 | page = get_page(config['device_os'], 'LoginPage') 25 | return page(driver=driver, config=config) 26 | 27 | 28 | @pytest.fixture 29 | def main_page(driver, config): 30 | page = get_page(config['device_os'], 'MainPage') 31 | return page(driver=driver, config=config) 32 | 33 | 34 | @pytest.fixture 35 | def search_page(driver, config): 36 | page = get_page(config['device_os'], 'SearchPage') 37 | return page(driver=driver, config=config) 38 | 39 | 40 | @pytest.fixture 41 | def title_page(driver, config): 42 | page = get_page(config['device_os'], 'TitlePage') 43 | return page(driver=driver, config=config) 44 | 45 | 46 | @pytest.fixture 47 | def title_list_page(driver, config): 48 | page = get_page(config['device_os'], 'TitleListPage') 49 | return page(driver=driver, config=config) 50 | 51 | 52 | def get_page(device, page_class): 53 | if device == 'mw': 54 | page_class += "MW" 55 | page = getattr(pages, page_class, None) 56 | if page is None: 57 | raise Exception(f'No such page {page_class}') 58 | return page 59 | 60 | 61 | def get_driver(browser_name, device_os): 62 | manager = ChromeDriverManager(version='106.0.5249.21') 63 | if device_os == 'web': 64 | if browser_name == 'chrome': 65 | browser = webdriver.Chrome(executable_path=manager.install(), 66 | options=capability_select(device_os)) 67 | else: 68 | raise UnsupportedBrowserType(f' Unsupported browser {browser_name}') 69 | elif device_os == 'mw': 70 | browser = webdriver.Chrome(executable_path=manager.install(), 71 | options=capability_select(device_os)) 72 | else: 73 | raise UnsupportedBrowserType(f' Unsupported device_os type {device_os}') 74 | return browser 75 | 76 | 77 | @pytest.fixture(scope='function') 78 | def driver(config, test_dir): 79 | url = config['url'] 80 | browser_name = config['browser'] 81 | device_os = config['device_os'] 82 | browser = get_driver(browser_name, device_os) 83 | browser.get(url) 84 | yield browser 85 | browser.quit() 86 | 87 | 88 | @pytest.fixture(scope='function') 89 | def ui_report(driver, request, test_dir): 90 | failed_tests_count = request.session.testsfailed 91 | yield 92 | if request.session.testsfailed > failed_tests_count: 93 | screenshot_file = os.path.join(test_dir, 'failure.png') 94 | driver.get_screenshot_as_file(screenshot_file) 95 | allure.attach.file(screenshot_file, 'failure.png', attachment_type=allure.attachment_type.PNG) 96 | 97 | browser_logfile = os.path.join(test_dir, 'browser.log') 98 | with open(browser_logfile, 'w') as f: 99 | for i in driver.get_log('browser'): 100 | f.write(f"{i['level']} - {i['source']}\n{i['message']}\n\n") 101 | 102 | with open(browser_logfile, 'r') as f: 103 | allure.attach(f.read(), 'browser.log', attachment_type=allure.attachment_type.TEXT) 104 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/locators/locators_mw.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageMWLocators: 5 | pass 6 | 7 | 8 | class LoginPageMWLocators(BasePageMWLocators): 9 | LOGIN_FIELD = (By.ID, 'wpName1') 10 | PASSWORD_FIELD = (By.ID, 'wpPassword1') 11 | LOGIN_BUTTON = (By.ID, 'wpLoginAttempt') 12 | 13 | 14 | class MainPagePageMWLocators(BasePageMWLocators): 15 | MAIN_MENU = (By.ID, 'mw-mf-main-menu-button') 16 | WATCH_LIST = (By.XPATH, "//span[text()='Watchlist']") 17 | SEARCH_ICON = (By.ID, 'searchIcon') 18 | 19 | 20 | class SearchPagePageMWLocators(BasePageMWLocators): 21 | SEARCH_FIELD = (By.XPATH, '//input[@class="search mw-ui-background-icon-search"]') 22 | ELEMENT_WITH_DESC = '//div[text()="{}"]' 23 | 24 | 25 | class TitlePagePageMWLocators(BasePageMWLocators): 26 | STAR_BUTTON = (By.ID, 'ca-watch') 27 | 28 | 29 | class TitleListPagePageMWLocators(BasePageMWLocators): 30 | MARKED_TITLE = (By.XPATH, "//a[contains(@class,'icon-wikimedia-unStar-progressive')]") 31 | TITLE_NAME = "//h3[text()='{}']" 32 | ELEMENT = "//span[text()='{}']" 33 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/locators/locators_web.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | class BasePageLocators: 5 | pass 6 | 7 | 8 | class LoginLocators(BasePageLocators): 9 | LOGIN_FIELD = (By.ID, 'wpName1') 10 | PASSWORD_FIELD = (By.ID, 'wpPassword1') 11 | LOGIN_BUTTON = (By.ID, 'wpLoginAttempt') 12 | 13 | 14 | class MainPageLocators(BasePageLocators): 15 | pass 16 | 17 | 18 | class SearchPageLocators(BasePageLocators): 19 | pass 20 | 21 | 22 | class TitlePageLocators(BasePageLocators): 23 | pass 24 | 25 | 26 | class TitleListPageLocators(BasePageLocators): 27 | pass 28 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.pages.main_page import MainPage, MainPageMW 3 | from ui.pages.login_page import LoginPage, LoginPageMW 4 | from ui.pages.search_page import SearchPage, SearchPageMW 5 | from ui.pages.title_page import TitlePage, TitlePageMW 6 | from ui.pages.title_list_page import TitleListPage, TitleListPageMW 7 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/base_page.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import allure 4 | from selenium.common.exceptions import StaleElementReferenceException 5 | from selenium.webdriver.support import expected_conditions as EC 6 | from selenium.webdriver.support.wait import WebDriverWait 7 | 8 | from ui.locators.locators_web import BasePageLocators 9 | from ui.locators.locators_mw import BasePageMWLocators 10 | from utils.decorators import wait 11 | 12 | CLICK_RETRY = 3 13 | BASE_TIMEOUT = 5 14 | 15 | 16 | logger = logging.getLogger('test') 17 | 18 | 19 | class PageNotLoadedException(Exception): 20 | pass 21 | 22 | 23 | class BasePage(object): 24 | locators = BasePageLocators() 25 | 26 | def __init__(self, driver, config): 27 | self.driver = driver 28 | self.config = config 29 | self.url = self.config['url'] 30 | self.device = self.config['device_os'] 31 | 32 | logger.info(f'{self.__class__.__name__} page is opening...') 33 | assert self.is_opened() 34 | 35 | def is_opened(self): 36 | def _check_url(): 37 | if self.url not in self.driver.current_url: 38 | raise PageNotLoadedException( 39 | f'{self.url} did not opened in {BASE_TIMEOUT} for {self.__class__.__name__}.\n' 40 | f'Current url: {self.driver.current_url}.') 41 | return True 42 | 43 | return wait(_check_url, error=PageNotLoadedException, check=True, timeout=BASE_TIMEOUT, interval=0.1) 44 | 45 | def find(self, locator, timeout=None): 46 | return self.wait(timeout).until(EC.presence_of_element_located(locator)) 47 | 48 | def wait(self, timeout=None): 49 | if timeout is None: 50 | timeout = 5 51 | return WebDriverWait(self.driver, timeout=timeout) 52 | 53 | def scroll_to(self, element): 54 | self.driver.execute_script('arguments[0].scrollIntoView(true);', element) 55 | 56 | @allure.step('Clicking {locator}') 57 | def click(self, locator, timeout=None): 58 | for i in range(CLICK_RETRY): 59 | logger.info(f'Clicking on {locator}. Try {i+1} of {CLICK_RETRY}...') 60 | try: 61 | element = self.find(locator, timeout=timeout) 62 | self.scroll_to(element) 63 | element = self.wait(timeout).until(EC.element_to_be_clickable(locator)) 64 | element.click() 65 | return 66 | except StaleElementReferenceException: 67 | if i == CLICK_RETRY - 1: 68 | raise 69 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/login_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import LoginLocators 3 | from ui.locators.locators_mw import LoginPageMWLocators 4 | import allure 5 | 6 | 7 | class LoginPage(BasePage): 8 | locators = LoginLocators() 9 | 10 | login_url = "https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page" 11 | 12 | @allure.step("Авторизовываемся") 13 | def login(self, login="testusername1090", password="11qazert"): 14 | self.driver.get(self.login_url) 15 | 16 | self.find(self.locators.LOGIN_FIELD).send_keys(login) 17 | self.find(self.locators.PASSWORD_FIELD).send_keys(password) 18 | self.click(self.locators.LOGIN_BUTTON) 19 | 20 | 21 | class LoginPageMW(LoginPage): 22 | locators = LoginPageMWLocators() 23 | login_url = "https://en.m.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page" 24 | 25 | @allure.step("Авторизовываемся") 26 | def login(self, login="testusername1090", password="11qazert"): 27 | super(LoginPageMW, self).login() 28 | 29 | 30 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/main_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import MainPageLocators 3 | from ui.locators.locators_mw import MainPagePageMWLocators 4 | import allure 5 | 6 | 7 | class MainPage(BasePage): 8 | locators = MainPageLocators() 9 | 10 | @allure.step("Нажимаем на кнопку поиска") 11 | def click_on_search_button(self): 12 | pass 13 | 14 | def open_menu_button(self): 15 | pass 16 | 17 | def open_watchlist(self): 18 | pass 19 | 20 | 21 | class MainPageMW(MainPage): 22 | locators = MainPagePageMWLocators() 23 | 24 | @allure.step("Нажимаем на кнопку поиска") 25 | def click_on_search_button(self): 26 | self.click(self.locators.SEARCH_ICON) 27 | 28 | @allure.step("Нажимаем на кнопку открытия меню (mobile)") 29 | def open_menu_button(self): 30 | self.click(self.locators.MAIN_MENU) 31 | 32 | @allure.step("Нажимаем на кнопку открытия меню (mobile)") 33 | def open_watchlist(self): 34 | self.click(self.locators.WATCH_LIST) 35 | 36 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/search_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | from ui.pages.base_page import BasePage 3 | from ui.locators.locators_web import SearchPageLocators 4 | from ui.locators.locators_mw import SearchPagePageMWLocators 5 | import allure 6 | 7 | 8 | class SearchPage(BasePage): 9 | locators = SearchPageLocators() 10 | 11 | def send_text_to_search_field_and_click_to_element(self, text, desc): 12 | pass 13 | 14 | 15 | class SearchPageMW(SearchPage): 16 | locators = SearchPagePageMWLocators() 17 | 18 | @allure.step("Вводим текст и нажимаем на нужный элемент") 19 | def send_text_to_search_field_and_click_to_element(self, text, desc): 20 | self.find(self.locators.SEARCH_FIELD).send_keys(text) 21 | self.find((By.XPATH, self.locators.ELEMENT_WITH_DESC.format(desc))).click() 22 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/title_list_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import TitleListPageLocators 3 | from ui.locators.locators_mw import TitleListPagePageMWLocators 4 | from selenium.webdriver.common.by import By 5 | import time 6 | import allure 7 | 8 | 9 | class TitleListPage(BasePage): 10 | locators = TitleListPageLocators() 11 | 12 | def check_delete_element_from_saved_title_list(self, text): 13 | pass 14 | 15 | 16 | class TitleListPageMW(TitleListPage): 17 | locators = TitleListPagePageMWLocators() 18 | 19 | def check_delete_element_from_saved_title_list(self, text): 20 | with allure.step("Нажимаем на 'звездочку'"): 21 | self.click(self.locators.MARKED_TITLE) 22 | time.sleep(1) 23 | with allure.step("Обновляем страницу и нажимаем на оставшуюся статью"): 24 | self.driver.refresh() 25 | self.click((By.XPATH, self.locators.TITLE_NAME.format(text))) 26 | with allure.step("Проверяем, что оставшаяся статья соответствует нужной"): 27 | self.find((By.XPATH, self.locators.ELEMENT.format(text))) 28 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/ui/pages/title_page.py: -------------------------------------------------------------------------------- 1 | from ui.pages.base_page import BasePage 2 | from ui.locators.locators_web import TitlePageLocators 3 | from ui.locators.locators_mw import TitlePagePageMWLocators 4 | import allure 5 | 6 | 7 | class TitlePage(BasePage): 8 | locators = TitlePageLocators() 9 | 10 | def add_to_bookmark(self): 11 | pass 12 | 13 | 14 | class TitlePageMW(TitlePage): 15 | locators = TitlePagePageMWLocators() 16 | 17 | @allure.step("Добавляем статью в избранное") 18 | def add_to_bookmark(self): 19 | self.click(self.locators.STAR_BUTTON) 20 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/utils/decorators.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class TimeoutException(Exception): 5 | pass 6 | 7 | 8 | def wait(method, error=Exception, timeout=10, interval=0.5, check=False, **kwargs): 9 | started = time.time() 10 | last_exception = None 11 | while time.time() - started < timeout: 12 | try: 13 | result = method(**kwargs) 14 | if check: 15 | if result: 16 | return result 17 | last_exception = f'Method {method.__name__} returned {result}' 18 | else: 19 | return result 20 | except error as e: 21 | last_exception = e 22 | 23 | time.sleep(interval) 24 | 25 | raise TimeoutException(f'Method {method.__name__} timeout out in {timeout}sec with exception: {last_exception}') 26 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/web_tests/base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pytest.fixtures import FixtureRequest 3 | 4 | from ui.pages.base_page import BasePage 5 | from ui.pages.main_page import MainPage 6 | from ui.pages.search_page import SearchPage 7 | from ui.pages.login_page import LoginPage 8 | from ui.pages.title_page import TitlePage 9 | from ui.pages.title_list_page import TitleListPage 10 | 11 | 12 | class BaseCase: 13 | 14 | @pytest.fixture(scope='function', autouse=True) 15 | def setup(self, driver, config, request: FixtureRequest, logger, ui_report): 16 | self.driver = driver 17 | self.config = config 18 | self.logger = logger 19 | 20 | self.base_page: BasePage = request.getfixturevalue('base_page') 21 | self.login_page: LoginPage = request.getfixturevalue('login_page') 22 | self.main_page: MainPage = request.getfixturevalue('main_page') 23 | self.search_page: SearchPage = request.getfixturevalue('search_page') 24 | self.title_page: TitlePage = request.getfixturevalue('title_page') 25 | self.title_list_page: TitleListPage = request.getfixturevalue('title_list_page') 26 | 27 | self.logger.debug('Initial setup done!') 28 | -------------------------------------------------------------------------------- /lection10-Appium/code_mw/web_tests/test_web_wikipedia.py: -------------------------------------------------------------------------------- 1 | import time 2 | import allure 3 | import pytest 4 | from web_tests.base import BaseCase 5 | 6 | 7 | class TestWikipediaWeb(BaseCase): 8 | @pytest.mark.MWUI 9 | @pytest.mark.UI 10 | @allure.severity(allure.severity_level.NORMAL) 11 | @allure.epic("Awesome PyTest framework") 12 | @allure.title("Test login") 13 | @allure.description("Test for login") 14 | @allure.feature('Login tests') 15 | def test_login(self): 16 | self.login_page.login() 17 | 18 | @pytest.mark.skip_platform("web") 19 | def test_saving_two_articles(self): 20 | text1 = 'Parkway Drive' 21 | text2 = 'Judas Priest' 22 | self.login_page.login() 23 | with allure.step("Нажимаем на кнопку Search и вводим значения в поле ввода"): 24 | self.main_page.click_on_search_button() 25 | self.search_page.send_text_to_search_field_and_click_to_element(text1, "Australian metalcore band") 26 | with allure.step("Добавляем статью в закладки"): 27 | self.title_page.add_to_bookmark() 28 | time.sleep(3) 29 | with allure.step("Нажимаем на кнопку Search и вводим значения в поле ввода"): 30 | self.main_page.click_on_search_button() 31 | self.search_page.send_text_to_search_field_and_click_to_element(text2, "British heavy metal band") 32 | with allure.step("Добавляем статью в закладки"): 33 | self.title_page.add_to_bookmark() 34 | time.sleep(3) 35 | with allure.step("Открываем список избранного"): 36 | self.main_page.open_menu_button() 37 | self.main_page.open_watchlist() 38 | self.title_list_page.check_delete_element_from_saved_title_list(text1) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lection10-Appium/homework4.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №4: Appium (необязательное) 2 | 3 | #### Цель домашнего задания 4 | 5 | * Научиться использовать инструменты для автоматизации UI мобильных приложений. 6 | 7 | 8 | #### Задача 9 | * Тестирование мобильного приложения голосового помощника "Маруся" ([Google Play](https://play.google.com/store/apps/details?id=ru.mail.search.electroscope&hl=ru&gl=US)). 10 | Apk уже лежит в директории данной лекции. 11 | * Тесты должны запускаться через марк -m Android или AndroidUI 12 | * Тесты должны быть написаны для девайсов с версией Android 11 или выше. 13 | * Appium должен использоваться последней версии. 14 | * **Важное уточнение:** Данное задание является дополнительным и не влияет на вашу успеваемость, 15 | т.е. вы можете его сдать и получить доп баллы, а можете ничего не делать, и вам за это ничего не будет. 16 | * Тестирование приложения голосового помощника "Маруся": 17 | * **Настройка окружения и иные аспекты** (0.25 балла): 18 | 1. Найти appPackage, appActivity, разобраться с выдачей разрешений приложению при его запуске. 19 | 2. apk приложения должен лежать в отдельной папке, путь к нему должен быть указан относительно корня репозитория 20 | (с помощью фикстуры repo_root или иными способами). 21 | 3. весь код (тех заданий, которые вы сделаете) должен быть написан в паттерне PageObject (например, главная пейджа + пейджа настроек + внутренние пейджы для элементов настроек). 22 | 4. в requirements (в корне вашего репозитория) должны присутствовать все новые библиотеки, которые вами использовались. 23 | * **Взаимодействие с окном команд (1.5 балла)**: 24 | 25 | 1. Ввести в чат (окно команд) слово Russia (в приложении может быть баг и возможно понадобится вводить слово 2 раза), удостовериться что у нас появился на экране текст с описанием страницы страны. 26 | 2. В списке приложений для команд проскроллить до "население россии" и нажать на него. 27 | 3. Удостовериться, что результат - 146 млн. 28 | 29 | * **Взаимодействие с окном команд, функционал "Калькулятор"** (1 балл): 30 | 31 | Ввести в чат (окно команд) какое-то простое математическое выражение (сложение/умножение/возведение в степень целых чисел), 32 | удостовериться что нам пришло в качестве ответа именно то число, которое предполагалось. 33 | 34 | **Важно**: можно ввести что-то и более сложное, но есть шанс нарваться на баги приложения. 35 | * **Взаимодействие с источником новостей** (1.5 балла): 36 | 37 | 1. Зайти в настройки приложения, выбрать раздел источник новостей, и там выбрать **Mail.Ru**. 38 | 2. Удостовериться, что на странице выбора новостей у выбранного способа появилась галочка. 39 | 3. Вернуться на главное окно, ввести в окно команд команду News и удостовериться любым способом, что новости появились. 40 | * **Взаимодействие с настройками приложения** (1 балл): 41 | 42 | Открыть бургер-меню, открыть раздел "О приложении" и удостовериться: 43 | 44 | а) Что версия (текст с цифрами) приложения содержит в себе именно ту версию, которая также указана в названии APK файла. 45 | Проверку осуществлять путем чтения версии из названия файла, а не хардкодом. 46 | 47 | б) Что внизу страницы присутствует трейдмарка ("все права защищены") 48 | 49 | #### Советы 50 | * Тесты *НЕ* должны быть зависимыми. 51 | * Все тесты *ДОЛЖНЫ* проходить. Если в приложении баг и это влияет на стабильность ваших тестов - напишите об этом при сдаче домашки. 52 | * Тесты *ОБЯЗАТЕЛЬНО* должны что-то проверять. Например если мы что-то создали - необходимо проверить что оно создалось. 53 | * Тесты *ДОЛЖНЫ* уметь запускаться несколько раз подряд. 54 | * Тесты должны работать в один поток (xdist не требуется). 55 | 56 | #### Срок сдачи ДЗ 57 | 58 | До 31 октября 23:59 (включительно) -------------------------------------------------------------------------------- /lection10-Appium/marussia_1.70.0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection10-Appium/marussia_1.70.0.apk -------------------------------------------------------------------------------- /lection11-PerformanceTesting/Load Testing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection11-PerformanceTesting/Load Testing.pdf -------------------------------------------------------------------------------- /lection12-LinuxInto/lection12.pptx.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection12-LinuxInto/lection12.pptx.pdf -------------------------------------------------------------------------------- /lection13-LinuxDebug/homework5.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №5: Backend: Linux 2 | 3 | #### Цель домашнего задания 4 | 5 | - Анализировать nginx логи 6 | - Освоить написание bash скриптов в одну строчку 7 | - Научиться писать bash/python скрипты 8 | 9 | 10 | #### Задача 11 | ##### Bash/Python scripting (максимум 6 баллов) 12 | * Написать скрипты на **bash** и **python** для анализа [готового access.log](https://cloud.mail.ru/public/W2Bi/3NmgGtATH) (на python) 13 | * Должен быть README.md файл, который описывает как работает каждый скрипт. 14 | * Слово "запрос" далее означает конкретный location(path! внимательно прочитайте что это такое, буду душнить и снижать баллы), который был запрошен у сервера 15 | 16 | * Для приложенного в задании access.log файла должна собираться следующая информация: 17 | * Общее количество запросов (1 балл) 18 | * Общее количество запросов по типу, например: GET - 20, POST - 10 и т.д. (1 балл) 19 | * Топ 10 самых частых запросов (1 балл): 20 | 21 | должен выводиться url 22 | должно выводиться число запросов 23 | 24 | * Топ 5 самых больших по размеру запросов, которые завершились клиентской (4ХХ) ошибкой (1 балл): 25 | 26 | должен выводиться url 27 | должен выводиться статус код 28 | должен выводиться размер запроса 29 | должен выводиться ip адрес 30 | 31 | * Топ 5 пользователей по количеству запросов, которые завершились серверной (5ХХ) ошибкой (1 балл): 32 | 33 | должен выводиться ip адрес 34 | должно выводиться количество запросов 35 | 36 | * Результаты нужно сохранять в произвольный файл (должен быть указан в README.md) в формате "какая информация собирается" <перенос строки> "результат" 37 | 38 | * Дополнительное задание (1 балл): 39 | * Для Python скрипта реализовать возможность сохранять собранные данные в JSON (флаг запуска --json), 40 | скруктура JSON произвольная 41 | 42 | 43 | * Сделать выводы в чем минусы и плюсы решения на BASH и на PYTHON. 44 | * Для тех кто дочитал, в Python части можно сделать только 2, 3 и 4 или 5 (3 в сумме) задания, рекомендую использовать библиотеку re 45 | 46 | #### Срок сдачи ДЗ 47 | До 23:59 07.11.2022 -------------------------------------------------------------------------------- /lection13-LinuxDebug/lection13.md: -------------------------------------------------------------------------------- 1 | # Утилиты для дебага (centos7) 2 | 3 | * [net-tools](https://www.opennet.ru/docs/RUS/lfs5/appendixa/net-tools.html) - анализ сетевых подключений 4 | 5 | Использование: 6 | 7 | `netstat -nltp` - для просмотра TCP соеденений и портов 8 | 9 | `netstat -nlup` - для просмотра UDP соеденений и портов 10 | 11 | 12 | * [sysstat](https://github.com/sysstat/sysstat) - информация о дисковой подсистеме 13 | 14 | Использование: 15 | 16 | `iostat -ktx 1` - получение информации о состоянии дисков с обновлением в 1с 17 | 18 | 19 | * [htop](https://htop.dev/) - общая информация о системе 20 | 21 | * [nload](https://github.com/rolandriegel/nload) - информация о состоянии сетевых интерфейсов и траффика 22 | 23 | * [tcpdump](https://www.tcpdump.org/) - сниффинг сетевого траффика 24 | 25 | * [lsof](https://man7.org/linux/man-pages/man8/lsof.8.html) - информация об открытых файлах процесса 26 | 27 | Использование: 28 | 29 | `lsof -p ` 30 | 31 | Использование: 32 | 33 | `tcpdump -i host and port ` 34 | 35 | 36 | # Прочие команды: 37 | * `df -h` - информация о размере и утилизации дискового пространства 38 | * `ifconfig` или `ip a` - информация о сетевых интерфейсах системы 39 | * `free -m` - информация о текущем состоянии оперативной памяти 40 | 41 | 42 | # Скрипты из лекции: 43 | 44 | ## Скрипт простейшей сетевой нагрузки 45 | ```shell script 46 | while true; do 47 | curl 127.0.0.1:80 & 48 | done 49 | ``` 50 | 51 | 52 | ## Скрипт простешей дисковой нагрузки 53 | ```shell script 54 | for i in {1..20}; do 55 | dd if=/dev/zero of=/tmp/random_$i bs=1M count=1000 & 56 | done 57 | ``` 58 | 59 | ## Команда загрузки оперативной памяти в 1.5 раза больше доступной (пакет stress-ng) 60 | `stress-ng --vm-bytes $(awk '/MemFree/{printf "%d\n", $2 * 1.5;}' < /proc/meminfo)k --vm-keep -m 1` 61 | 62 | 63 | # Послевкусие: 64 | * Книга "Systems Performance 2nd Edition Book" - Brendan Gregg 65 | * Книга "Современные операционные системы" - Эндрю Таненбаум 66 | * [Advanced Bash-Scripting Guide](https://www.opennet.ru/docs/RUS/bash_scripting_guide/) 67 | * vim (:wq) 68 | ![vim](vim_cheat_sheet.png) -------------------------------------------------------------------------------- /lection13-LinuxDebug/vim_cheat_sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection13-LinuxDebug/vim_cheat_sheet.png -------------------------------------------------------------------------------- /lection14-SQL Basics/MySQL Basics.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection14-SQL Basics/MySQL Basics.pdf -------------------------------------------------------------------------------- /lection14-SQL Basics/code.md: -------------------------------------------------------------------------------- 1 | # Скрипты из лекции: 2 | 3 | Скачать образ можно тут: 4 | - https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/docker-mysql-getting-started.html#docker-download-image 5 | - https://hub.docker.com/_/mysql 6 | 7 | ### Запуск бд MySql в контейнере docker 8 | ```shell script 9 | docker run --name -p 3306:3306 -e MYSQL_ROOT_PASSWORD= -d mysql:8.0 10 | ``` 11 | Если образ еще не скачан, то docker автоматически сначала сделает pull из репозитория, а потом запустит. 12 | 13 | Проверить статус контейнера: 14 | ```shell script 15 | docker ps -a 16 | ``` 17 | Подключится к клиенту mysql: 18 | ```shell script 19 | mysql -h127.0.0.1 -P3306 -uroot -p 20 | ``` 21 | После окончания работы с БД необходимо остановить контейнер: 22 | ```shell script 23 | docker stop 24 | ``` 25 | Если он больше не нужен, его можно удалить: 26 | ```shell script 27 | docker rm 28 | ``` 29 | 30 | ### Управление данными в БД: 31 | 32 | Просмотр все схем в БД: 33 | ```mysql 34 | show databases; 35 | ``` 36 | 37 | Создание новой схемы, выбор схемы для работы и создание таблицы: 38 | ```mysql 39 | create database target; 40 | use target; 41 | 42 | create table `banner` ( 43 | `id` smallint(6) not null auto_increment, 44 | `name` char(20) not null, 45 | `url` char(50) not null, 46 | INDEX USING BTREE (id)); 47 | 48 | insert into banner values (1, 'reklama', 'vk.com'); 49 | ``` 50 | 51 | Пример транзакции с блокировкой: 52 | ```mysql 53 | begin; 54 | select name from banner where id =1 for update; 55 | commit; 56 | ``` 57 | До того как быдет выполена команда commit строка с id=1 будет заблокированна в таблице. 58 | Если в другой сессии попытаться изменить те же данные, то запрос не будет выполнен пока со строки не будет снята блокировка. 59 | Все сессии, которые ждут снятия блокировок можно увидеть тут: 60 | ```mysql 61 | SHOW SESSION VARIABLES LIKE "%wait%"; 62 | select * from x$innodb_lock_waits; 63 | ``` 64 | 65 | Пример создания таблицы с партиционированием по годам: 66 | ```mysql 67 | create table `events` ( 68 | `timestamp` timestamp not null, 69 | `date` date not null, 70 | `banner_id` smallint(6) not null, 71 | `event` char(20) not null, 72 | `placement` char(50) not null, 73 | primary key (`date`)) 74 | partition by hash(year(`date`)); 75 | ``` 76 | 77 | Проверка как именно БД будет выполнять запрос: 78 | ```mysql 79 | EXPLAIN ANALYZE SELECT * FROM banner as b JOIN events as e ON (b.id = e.banner_id) 80 | ``` 81 | Полезно для оптимизации времени выполнения запроса. 82 | 83 | 84 | ### Создание и загрузка дампа с данными: 85 | Для создания дампа используется утилита mysqldump. 86 | ```shell script 87 | mysqldump -u root -h 127.0.0.1 -P3306 -p target banner > /tmp/banner.sql 88 | ``` 89 | Загрузка дампа в БД: 90 | ```shell script 91 | mysql -u root -h 127.0.0.1 -P3306 -p target < /tmp/banner.sql 92 | ``` 93 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_orm/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mysql.client import MysqlClient 3 | 4 | 5 | def pytest_configure(config): 6 | mysql_client = MysqlClient(user='root', password='0000', db_name='target') 7 | if not hasattr(config, 'workerinput'): 8 | mysql_client.create_db() 9 | mysql_client.connect(db_created=True) 10 | if not hasattr(config, 'workerinput'): 11 | mysql_client.create_table_banner() 12 | 13 | config.mysql_client = mysql_client 14 | 15 | 16 | @pytest.fixture(scope='session') 17 | def mysql_client(request) -> MysqlClient: 18 | client = request.config.mysql_client 19 | yield client 20 | client.connection.close() 21 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_orm/models/banner.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | from sqlalchemy import Column, Integer, CHAR, VARCHAR 3 | 4 | Base = declarative_base() 5 | 6 | 7 | class BannerModel(Base): 8 | 9 | __tablename__ = 'banner' 10 | __table_arg__ = {'mysql_charset': 'utf8'} 11 | 12 | def __repr__(self): 13 | return f'Banner id={self.id}, name={self.name}, url={self.url}' 14 | 15 | id = Column(Integer, primary_key=True, autoincrement=True) 16 | name = Column(CHAR(50), nullable=False) 17 | url = Column(VARCHAR(50), nullable=False) 18 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_orm/mysql/client.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy 2 | import os 3 | from sqlalchemy.orm import sessionmaker 4 | from models.banner import Base 5 | 6 | 7 | class MysqlClient: 8 | 9 | def __init__(self, db_name, user, password): 10 | self.user = user 11 | self.port = '3306' 12 | self.password = password 13 | self.host = 'percona' 14 | self.db_name = db_name 15 | 16 | self.connection = None 17 | self.engine = None 18 | self.session = None 19 | 20 | def connect(self, db_created=True): 21 | db = self.db_name if db_created else '' 22 | url = f'mysql+pymysql://{self.user}:{self.password}@{self.host}:{self.port}/{db}' 23 | self.engine = sqlalchemy.create_engine(url) 24 | self.connection = self.engine.connect() 25 | 26 | session = sessionmaker(bind=self.connection.engine) 27 | self.session = session() 28 | 29 | def create_db(self): 30 | self.connect(db_created=False) 31 | self.execute_query(f'DROP database IF EXISTS {self.db_name}') 32 | self.execute_query(f'CREATE database {self.db_name}') 33 | 34 | def create_table_banner(self): 35 | if not sqlalchemy.inspect(self.engine).has_table('banner'): 36 | Base.metadata.tables['banner'].create(self.engine) 37 | 38 | def execute_query(self, query, fetch=False): 39 | res = self.connection.execute(query) 40 | if fetch: 41 | return res.fetchall() 42 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_orm/test_sql/test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mysql.client import MysqlClient 3 | from utils.builder import MysqlBuilder 4 | from models.banner import BannerModel 5 | 6 | 7 | class MyTest: 8 | 9 | def prepare(self): 10 | pass 11 | 12 | @pytest.fixture(scope='function', autouse=True) 13 | def setup(self, mysql_client): 14 | self.client:MysqlClient = mysql_client 15 | self.builder: MysqlBuilder = MysqlBuilder(self.client) 16 | self.prepare() 17 | 18 | def get_banners(self, **filters): 19 | self.client.session.commit() 20 | return self.client.session.query(BannerModel).filter_by(**filters).all() 21 | 22 | 23 | class TestMysql(MyTest): 24 | 25 | def prepare(self): 26 | self.builder.create_banner() 27 | 28 | def test(self): 29 | banners = self.get_banners() 30 | assert len(banners) == 1 31 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_orm/utils/builder.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | 3 | 4 | class MysqlBuilder: 5 | def __init__(self, client): 6 | self.client = client 7 | 8 | def create_banner(self, name=None, url=None): 9 | fake = Faker() 10 | banner_name = name or fake.job() 11 | banner_url = url or fake.url() 12 | self.client.execute_query(f'insert into `banner` (`name`, `url`) values ("{banner_name}", "{banner_url}")') 13 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_pysql/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mysql.client import MysqlClient 3 | 4 | 5 | def pytest_configure(config): 6 | mysql_client = MysqlClient(user='root', password='pass', db_name='target') 7 | if not hasattr(config, 'workerinput'): 8 | mysql_client.create_db() 9 | mysql_client.connect(db_created=True) 10 | if not hasattr(config, 'workerinput'): 11 | mysql_client.create_table_banner() 12 | 13 | config.mysql_client = mysql_client 14 | 15 | 16 | @pytest.fixture(scope='session') 17 | def mysql_client(request) -> MysqlClient: 18 | client = request.config.mysql_client 19 | yield client 20 | client.connection.close() 21 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_pysql/mysql/client.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | 4 | class MysqlClient: 5 | 6 | def __init__(self, db_name, user, password): 7 | self.user = 'root' 8 | self.port = 3306 9 | self.password = '0000' 10 | self.host = '127.0.0.1' 11 | self.db_name = db_name 12 | self.connection = None 13 | 14 | def connect(self, db_created=True): 15 | self.connection = pymysql.connect(host=self.host, 16 | port=self.port, 17 | user=self.user, 18 | password=self.password, 19 | db=self.db_name if db_created else None, 20 | charset='utf8', 21 | autocommit=True, 22 | cursorclass=pymysql.cursors.DictCursor 23 | ) 24 | 25 | def create_db(self): 26 | self.connect(db_created=False) 27 | self.connection.query(f'DROP database IF EXISTS {self.db_name}') 28 | self.connection.query(f'CREATE database {self.db_name}') 29 | 30 | def create_table_banner(self): 31 | create_banner = """ 32 | create table `banner`( 33 | `id` smallint(6) not null auto_increment, 34 | `name` char(50) not null, 35 | `url` char(50) not null, 36 | primary key (`id`) 37 | ) 38 | """ 39 | self.connection.query(create_banner) 40 | 41 | def execute_query(self, query, fetch=False): 42 | cursor = self.connection.cursor() 43 | cursor.execute(query) 44 | if fetch: 45 | return cursor.fetchall() 46 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_pysql/test_sql/test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mysql.client import MysqlClient 3 | from utils.builder import MysqlBuilder 4 | 5 | 6 | class MyTest: 7 | 8 | def prepare(self): 9 | pass 10 | 11 | @pytest.fixture(scope='function', autouse=True) 12 | def setup(self, mysql_client): 13 | self.client:MysqlClient = mysql_client 14 | self.builder: MysqlBuilder = MysqlBuilder(self.client) 15 | self.prepare() 16 | 17 | def get_banners(self): 18 | return self.client.execute_query('select * from `banner`', fetch=True) 19 | 20 | 21 | class TestMysql(MyTest): 22 | 23 | def prepare(self): 24 | self.builder.create_banner() 25 | 26 | def test(self): 27 | banners = self.get_banners() 28 | assert len(banners) == 1 29 | -------------------------------------------------------------------------------- /lection15-SQL ORM/code_pysql/utils/builder.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | 3 | 4 | class MysqlBuilder: 5 | def __init__(self, client): 6 | self.client = client 7 | 8 | def create_banner(self, name=None, url=None): 9 | fake = Faker() 10 | banner_name = name or fake.job() 11 | banner_url = url or fake.url() 12 | self.client.execute_query(f'insert into `banner` (`name`, `url`) values ("{banner_name}", "{banner_url}")') 13 | -------------------------------------------------------------------------------- /lection15-SQL ORM/homework.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №6 - Backend: MySQL 2 | 3 | ### Цель домашнего задания: 4 | 5 | - Научиться работать с СУБД из Python 6 | - Получить навыки использования ORM (sqlalchemy) 7 | 8 | 9 | ### Задача (максимум 7 баллов) 10 | 1. Написать в формате тестов код для создания БД и добавления access логов из домашнего задания №5. 11 | 2. Все нижеперечисленные действия должны выполняться из Python. Все баллы за задания эквивалентны прошлому ДЗ, за исключением пункта с опцией `--json` 12 | для python скрипта, т.е. нужно: 13 | 14 | 2.1. Создать mysql БД для результатов подсчета прямо из тестов, на каждый запуск тестов БД должна пересоздаваться. 15 | 2.2. Переписать скрипт на python из предыдущего домашнего задания в формат тестов pytest, так, чтобы данные по каждому заданию заливались в БД. 16 | 2.3. Каждое задание заливается в отдельную таблицу. 17 | 18 | 3. Проверить работу своего кода нужно по итоговому количеству записей в БД для каждого задания. В идеале - проверять и наполнение. 19 | То есть, если вам нужно вывести топ-10 записей, то проверяем, что их действительно столько. НЕ ХАРДКОДИТЬ, т.е. мы можем попросить выводить топ-30 записей и т.д. 20 | 4. Базу данных поднимаем и настраиваем так же как это делалось в лекции (127.0.0.1:3306, user=root, password=pass). Имя БД - **TEST_SQL**. 21 | 22 | Условия: 23 | - 100% баллов (7) за задание можно получить только реализовав его по концепции ORM (sqlalchemy). 24 | При не использовании ORM - максимальный балл 50% (3.5) 25 | - Если задание будет выполнено частично (не все запросы переведены на Python) - это тоже будет принято, но с более низкой оценкой. 26 | 27 | 28 | Важные нюансы: 29 | - **ТЕСТЫ ДОЛЖНЫ РАБОТАТЬ в параллельном режиме (-n 2)**. 30 | 31 | 1. Могут быть проблемы при создании БД - вспоминайте про выполнение каких-то операций только на главном потоке, только на воркерах и т.д. 32 | Например, не нужно создавать базу данных и таблицы в каждом тесте - это нужно сделать один раз при запуске тестов. 33 | 34 | 2. Соединение с базой также должно делаться один раз в рамках мастер-потока 35 | 3. Проверяйте, что ваш пайплайн GitHub Actions - зеленый. 36 | - Не забывайте дропать базу после окончания тестов/пересоздавать при запуске тестов, чтобы не оставалось "хвостов". 37 | 38 | #### Сроки сдачи ДЗ 39 | До 14 ноября включительно. 40 | -------------------------------------------------------------------------------- /lection15-SQL ORM/sql.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | connection = pymysql.connect( 4 | host='127.0.0.1', 5 | port=3306, 6 | user='root', 7 | password='0000', 8 | db=None, 9 | charset='utf8', 10 | ) 11 | connection.query('drop database if exists `target`') 12 | connection.query('create database target;') 13 | connection.close() 14 | 15 | connection = pymysql.connect( 16 | host='127.0.0.1', 17 | port=3306, 18 | user='root', 19 | password='0000', 20 | db='target', 21 | charset='utf8', 22 | cursorclass=pymysql.cursors.DictCursor, 23 | autocommit=True 24 | ) 25 | 26 | text_query='create table banner (`id` smallint(10) not null auto_increment, `name` char(50) not null, `url` varchar(50) not null, primary key (`id`))' 27 | connection.query(text_query) 28 | 29 | connection.query('insert into `banner` values (1 , "test name", "vk.com")') 30 | 31 | res = connection.query('select * from `banner`;') 32 | print(res) 33 | 34 | 35 | cursor = connection.cursor() 36 | cursor.execute('select * from `banner`;') 37 | 38 | res_2 = cursor.fetchall() 39 | print(res_2) 40 | 41 | connection.close() 42 | -------------------------------------------------------------------------------- /lection15-SQL ORM/start_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker ps -a 4 | pytest -s -l -v "${TESTS_PATH}" --alluredir /tmp/allure -------------------------------------------------------------------------------- /lection16-Network/lection16.pptx.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection16-Network/lection16.pptx.pdf -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/application/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | 5 | import requests 6 | from flask import Flask, request, jsonify 7 | 8 | app = Flask(__name__) 9 | 10 | app_data = {} 11 | user_id_seq = 1 12 | 13 | 14 | @app.route('/add_user', methods=['POST']) 15 | def create_user(): 16 | global user_id_seq 17 | 18 | user_name = json.loads(request.data)['name'] 19 | if user_name not in app_data: 20 | app_data[user_name] = user_id_seq 21 | user_id_seq += 1 22 | return jsonify({'user_id': app_data[user_name]}), 201 23 | 24 | else: 25 | return jsonify(f'User_name {user_name} already exists: id: {app_data[user_name]}'), 400 26 | 27 | 28 | @app.route('/get_user/', methods=['GET']) 29 | def get_user_id_by_name(name): 30 | if user_id := app_data.get(name): 31 | age_host = os.environ['STUB_HOST'] 32 | age_port = os.environ['STUB_PORT'] 33 | 34 | age = None 35 | try: 36 | age = requests.get(f'http://{age_host}:{age_port}/get_age/{name}').json() 37 | except Exception as e: 38 | print(f'Unable to get age from external system:\n{e}') 39 | 40 | surname_host = os.environ['MOCK_HOST'] 41 | surname_port = os.environ['MOCK_PORT'] 42 | 43 | surname = None 44 | try: 45 | response = requests.get(f'http://{surname_host}:{surname_port}/get_surname/{name}') 46 | if response.status_code == 200: 47 | surname = response.json() 48 | except Exception as e: 49 | print(f'Unable to get surname from external system:\n{e}') 50 | print(f'No surname found for user {name}') 51 | data = {'user_id': user_id, 52 | 'age': age, 53 | 'surname': surname} 54 | 55 | return jsonify(data), 200 56 | 57 | else: 58 | return jsonify(f'User_name {name} not found'), 404 59 | 60 | 61 | if __name__ == '__main__': 62 | host = os.environ.get('APP_HOST', '127.0.0.1') 63 | port = os.environ.get('APP_PORT', '8083') 64 | 65 | app.run(host=host, port=port) 66 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | import time 5 | from copy import copy 6 | 7 | import requests 8 | 9 | import settings 10 | 11 | repo_root = os.path.abspath(os.path.join(__file__, os.pardir)) 12 | 13 | 14 | def wait_ready(host, port): 15 | started = False 16 | st = time.time() 17 | while time.time() - st <= 5: 18 | try: 19 | requests.get(f'http://{host}:{port}') 20 | started = True 21 | break 22 | except ConnectionError: 23 | pass 24 | 25 | if not started: 26 | raise RuntimeError('App did not started in 5s!') 27 | 28 | 29 | def pytest_configure(config): 30 | if not hasattr(config, 'workerinput'): 31 | ######### app configuration ######### 32 | 33 | app_path = os.path.join(repo_root, 'application', 'app.py') 34 | 35 | env = copy(os.environ) 36 | env.update({'APP_HOST': settings.APP_HOST, 'APP_PORT': settings.APP_PORT}) 37 | env.update({'STUB_HOST': settings.STUB_HOST, 'STUB_PORT': settings.STUB_PORT}) 38 | env.update({'MOCK_HOST': settings.MOCK_HOST, 'MOCK_PORT': settings.MOCK_PORT}) 39 | 40 | 41 | app_stderr = open('/tmp/stub_stderr', 'w') 42 | app_stdout = open('/tmp/stub_stdout', 'w') 43 | # windows 44 | # app_stderr_path = os.path.join(repo_root, 'tmp', 'app_stderr') 45 | # app_stdout_path = os.path.join(repo_root, 'tmp', 'app_stdout') 46 | # app_stderr = open(app_stderr_path, 'w') 47 | # app_stdout = open(app_stdout_path, 'w') 48 | app_proc = subprocess.Popen(['python3', app_path], stderr=app_stderr, stdout=app_stdout, env=env) 49 | 50 | # windows 51 | # app_proc = subprocess.Popen(['c:\\tp\\venv\\Scripts\\python', app_path], 52 | # stderr=app_stderr, stdout=app_stdout, env=env 53 | # ) 54 | config.app_proc = app_proc 55 | config.app_stderr = app_stderr 56 | config.app_stdout = app_stdout 57 | wait_ready(settings.APP_HOST, settings.APP_PORT) 58 | 59 | ######### stub configuration ######### 60 | 61 | # stub_path = os.path.join(repo_root, 'stub', 'flask_stub.py') 62 | stub_path = os.path.join(repo_root, 'stub', 'simple_http_server_stub.py') 63 | 64 | env = copy(os.environ) 65 | env.update({'STUB_HOST': settings.STUB_HOST, 'STUB_PORT': settings.STUB_PORT}) 66 | 67 | stub_stderr = open('/tmp/stub_stderr', 'w') 68 | stub_stdout = open('/tmp/stub_stdout', 'w') 69 | # windows 70 | # stub_stderr_path = os.path.join(repo_root, 'tmp', 'stub_stderr') 71 | # stub_stdout_path = os.path.join(repo_root, 'tmp', 'stub_stdout') 72 | # stub_stderr = open(stub_stderr_path, 'w') 73 | # stub_stdout = open(stub_stdout_path, 'w') 74 | 75 | # stub_proc = subprocess.Popen(['c:\\tp\\venv\\Scripts\\python', stub_path], 76 | stub_proc = subprocess.Popen(['python3', app_path], 77 | stderr=stub_stderr, stdout=stub_stdout, env=env 78 | ) 79 | config.stub_proc = stub_proc 80 | config.stub_stderr = stub_stderr 81 | config.stub_stdout = stub_stdout 82 | wait_ready(settings.STUB_HOST, settings.STUB_PORT) 83 | 84 | 85 | ######### mock configuration ######### 86 | 87 | from mock import flask_mock 88 | flask_mock.run_mock() 89 | 90 | wait_ready(settings.MOCK_HOST, settings.MOCK_PORT) 91 | 92 | 93 | def pytest_unconfigure(config): 94 | # config.proc.send_signal(signal.SIGINT) 95 | # exit_code = config.proc.wait() 96 | # assert exit_code == 0 97 | 98 | config.app_proc.terminate() 99 | exit_code = config.app_proc.wait() 100 | 101 | config.app_stderr.close() 102 | config.app_stdout.close() 103 | 104 | assert exit_code == 1 105 | 106 | config.stub_proc.terminate() 107 | exit_code = config.stub_proc.wait() 108 | 109 | config.stub_stderr.close() 110 | config.stub_stdout.close() 111 | 112 | assert exit_code == 1 113 | 114 | # TODO: add mock shutdown 115 | requests.get(f'http://{settings.MOCK_HOST}:{settings.MOCK_PORT}/shutdown') 116 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/mock/flask_mock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import threading 4 | 5 | from flask import Flask, jsonify, request 6 | 7 | import settings 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | SURNAME_DATA = {} 13 | 14 | 15 | @app.route('/get_surname/', methods=['GET']) 16 | def get_user_surname(name): 17 | if surname := SURNAME_DATA.get(name): 18 | return jsonify(surname), 200 19 | else: 20 | return jsonify(f'Surname for user "{name}" not found'), 404 21 | 22 | 23 | def shutdown_stub(): 24 | terminate_func = request.environ.get('werkzeug.server.shutdown') 25 | if terminate_func: 26 | terminate_func() 27 | else: 28 | raise RuntimeError('Not running with the Werkzeug Server') 29 | 30 | 31 | @app.route('/shutdown') 32 | def shutdown(): 33 | shutdown_stub() 34 | return jsonify(f'Ok, exiting'), 200 35 | 36 | 37 | def run_mock(): 38 | server = threading.Thread(target=app.run, kwargs={ 39 | 'host': settings.MOCK_HOST, 40 | 'port': settings.MOCK_PORT 41 | }) 42 | 43 | server.start() 44 | return server 45 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection17-MocksAndHttpServers/code/requirements.txt -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/settings.py: -------------------------------------------------------------------------------- 1 | APP_HOST = '127.0.0.1' 2 | APP_PORT = '8083' 3 | 4 | STUB_HOST = '127.0.0.1' 5 | STUB_PORT = '8081' 6 | 7 | MOCK_HOST = '127.0.0.1' 8 | MOCK_PORT = '8082' 9 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/stub/flask_stub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import random 5 | 6 | from flask import Flask, jsonify 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | @app.route('/get_age/', methods=['GET']) 12 | def get_user_age(name): 13 | return jsonify(random.randint(18, 105)), 200 14 | 15 | 16 | if __name__ == '__main__': 17 | host = os.environ.get('STUB_HOST', '127.0.0.1') 18 | port = os.environ.get('STUB_PORT', '4444') 19 | 20 | app.run(host, port) 21 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/stub/simple_http_server_stub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import random 6 | from http.server import BaseHTTPRequestHandler, HTTPServer 7 | 8 | 9 | class AgeStubHandleRequests(BaseHTTPRequestHandler): 10 | data = None 11 | 12 | def _set_headers(self, status): 13 | self.send_response(status) 14 | self.send_header('Content-type', 'application/json') 15 | self.end_headers() 16 | 17 | def do_GET(self): 18 | location = self.path.split('/') 19 | if len(location) == 3 and location[1] == 'get_age' and isinstance(location[2], str): 20 | self._set_headers(200) 21 | self.wfile.write(json.dumps(random.randint(18, 105)).encode()) 22 | else: 23 | self._set_headers(500) 24 | self.wfile.write(b'error') 25 | 26 | 27 | if __name__ == '__main__': 28 | host = os.environ.get('STUB_HOST', '127.0.0.1') 29 | port = int(os.environ.get('STUB_PORT', 4444)) 30 | 31 | server = HTTPServer((host, port), RequestHandlerClass=AgeStubHandleRequests) 32 | server.allow_reuse_address = True 33 | 34 | server.serve_forever() 35 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/tests/test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | import settings 4 | from mock.flask_mock import SURNAME_DATA 5 | 6 | url = f'http://{settings.APP_HOST}:{settings.APP_PORT}' 7 | 8 | 9 | def test(): 10 | print(requests.get(url).text) 11 | 12 | 13 | def test_add_get_user(): 14 | resp = requests.post(f'{url}/add_user', json={'name': 'Kostya'}) 15 | user_id_from_add = resp.json()['user_id'] 16 | 17 | resp = requests.get(f'{url}/get_user/Kostya') 18 | user_id_from_get = resp.json()['user_id'] 19 | 20 | assert user_id_from_add == user_id_from_get 21 | 22 | 23 | def test_add_existent_user(): 24 | requests.post(f'{url}/add_user', json={'name': 'Vasya'}) 25 | resp = requests.post(f'{url}/add_user', json={'name': 'Vasya'}) 26 | 27 | assert resp.status_code == 400 28 | 29 | 30 | def test_get_non_existent_user(): 31 | resp = requests.get(f'{url}/Masha') 32 | 33 | assert resp.status_code == 404 34 | 35 | 36 | def test_with_age(): 37 | requests.post(f'{url}/add_user', json={'name': 'Stepan'}) 38 | 39 | resp = requests.get(f'{url}/get_user/Stepan') 40 | age = resp.json()['age'] 41 | assert isinstance(age, int) 42 | assert 18 <= age <= 105 43 | 44 | 45 | def test_has_surname(): 46 | requests.post(f'{url}/add_user', json={'name': 'Olya'}) 47 | SURNAME_DATA['Olya'] = 'OLOLOEVA' 48 | 49 | resp = requests.get(f'{url}/get_user/Olya') 50 | surname = resp.json()['surname'] 51 | assert surname == 'OLOLOEVA' 52 | 53 | 54 | def test_has_no_surname(): 55 | requests.post(f'{url}/add_user', json={'name': 'Sveta'}) 56 | 57 | resp = requests.get(f'{url}/get_user/Sveta') 58 | surname = resp.json()['surname'] 59 | assert surname == None 60 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/code/tests/test_socket.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from _socket import timeout 3 | 4 | import settings 5 | 6 | 7 | url = f'http://{settings.APP_HOST}:{settings.APP_PORT}' 8 | 9 | 10 | def test_with_socket_client(): 11 | requests.post(f'{url}/add_user', json={'name': '123'}) 12 | 13 | import socket 14 | import json 15 | 16 | target_host = settings.APP_HOST 17 | target_port = int(settings.APP_PORT) 18 | 19 | params = '/get_user/123' 20 | 21 | # создаём объект клиентского сокета 22 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 23 | 24 | # выставляем ожидание для сокета 25 | client.settimeout(0.1) 26 | 27 | # устанавливаем соединение 28 | client.connect((target_host, target_port)) 29 | 30 | # создаем и выполняем запрос 31 | request = f'GET {params} HTTP/1.1\r\nHost:{target_host}\r\n\r\n' 32 | client.send(request.encode()) 33 | 34 | total_data = [] 35 | 36 | try: 37 | while True: 38 | # читаем данные из сокета до тех пор пока они там есть 39 | data = client.recv(4096) 40 | if data: 41 | print(f'received data: {data}') 42 | total_data.append(data.decode()) 43 | else: 44 | break 45 | except timeout: 46 | pass 47 | 48 | data = ''.join(total_data).splitlines() 49 | assert json.loads(data[-1])['age'] > 0 50 | -------------------------------------------------------------------------------- /lection17-MocksAndHttpServers/homework7.md: -------------------------------------------------------------------------------- 1 | ## Домашнее задание №7 - Http Mock 2 | 3 | ### Цель домашнего задания: 4 | 5 | - Научиться создавать HTTP-клиенты и Mock сервера с использованием различных библиотек и фреймворков; 6 | - Научиться писать тесты, когда необходимо использовать Mock сервера. 7 | 8 | 9 | ### Условия: 10 | 11 | Максимум 10 баллов. 12 | 13 | Взаимодействие может производиться с теми же сущностями (люди), либо вы можете предложить собственную реализацию. 14 | 15 | 16 | - Разобраться в библиотеке fastapi (базовые функционал) и повтроить функционал на базе кода из лекции: 17 | - **переписать и дополнить код клиента**, разнеся его на отдельные классы и методы (запуск, отправка get/post-запросов в мок) - **2 балла**; 18 | - **реализовать обработчик ответов** и выводить в лог-файл результат запроса и ответа: код ответа, заголовки, тело ответа - **2 балл**; 19 | - Используя mock из лекции, **реализовать обработку PUT (для обновления) и DELETE (для удаления) запросов** - **2 балл**. 20 | - Написать тесты на собственный Mock сервер (необходимое и достаточное количество, чтобы убедиться, что Mock работает корректно) - **2 балла** 21 | - Привести код из лекции в порядок согласно концепциям, рассмотренных на курсе - **2 балла**: 22 | - все разложено по классам 23 | - использутся фикстуры и хуки 24 | - код оптимизирован, убрана копипаста 25 | - можно трогать код приложения (app/app.py) 26 | - поправить логгирование мока запущенного в треде, чтобы выводил в файл 27 | - запускать flask на протоколе HTTP/1.1 28 | 29 | 30 | #### Сроки сдачи ДЗ 31 | До 23:59 24 ноября -------------------------------------------------------------------------------- /lection18-Docker/lection17-Docker.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection18-Docker/lection17-Docker.pdf -------------------------------------------------------------------------------- /lection19-Docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | percona: 5 | environment: 6 | MYSQL_ROOT_PASSWORD: '0000' 7 | image: "percona:latest" 8 | healthcheck: 9 | test: [ "CMD", "mysqladmin" ,"-uroot", "-p0000", "ping", "-h", "127.0.0.1" ] 10 | timeout: 1s 11 | retries: 30 12 | 13 | tests: 14 | image: python_mock 15 | tty: true 16 | volumes: 17 | - /Users/k.soldatov/PycharmProjects/education-vk-python-2022/lection15-SQL ORM:/code 18 | - /Users/k.soldatov/PycharmProjects/education-vk-python-2022/lection15-SQL ORM/allure:/tmp/allure 19 | - /var/run/docker.sock:/var/run/docker.sock 20 | - /usr/bin/docker:/bin/docker 21 | 22 | entrypoint: /bin/bash start_tests.sh 23 | 24 | environment: 25 | TESTS_PATH: "code_orm/test_sql/test.py" 26 | 27 | depends_on: 28 | - percona 29 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | ADD requirements.txt /requirements.txt 4 | RUN pip3.8 install -r /requirements.txt 5 | 6 | COPY code code 7 | 8 | WORKDIR code 9 | 10 | CMD ["/code/start_tests.sh"] -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/application/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | 5 | import requests 6 | from flask import Flask, request, jsonify 7 | 8 | app = Flask(__name__) 9 | 10 | app_data = {} 11 | user_id_seq = 1 12 | 13 | 14 | @app.route('/add_user', methods=['POST']) 15 | def create_user(): 16 | global user_id_seq 17 | 18 | user_name = json.loads(request.data)['name'] 19 | if user_name not in app_data: 20 | app_data[user_name] = user_id_seq 21 | user_id_seq += 1 22 | return jsonify({'user_id': app_data[user_name]}), 201 23 | 24 | else: 25 | return jsonify(f'User_name {user_name} already exists: id: {app_data[user_name]}'), 400 26 | 27 | 28 | @app.route('/get_user/', methods=['GET']) 29 | def get_user_id_by_name(name): 30 | if user_id := app_data.get(name): 31 | age_host = os.environ['STUB_HOST'] 32 | age_port = os.environ['STUB_PORT'] 33 | 34 | age = None 35 | try: 36 | age = requests.get(f'http://{age_host}:{age_port}/get_age/{name}').json() 37 | except Exception as e: 38 | print(f'Unable to get age from external system:\n{e}') 39 | 40 | surname_host = os.environ['MOCK_HOST'] 41 | surname_port = os.environ['MOCK_PORT'] 42 | 43 | surname = None 44 | try: 45 | response = requests.get(f'http://{surname_host}:{surname_port}/get_surname/{name}') 46 | if response.status_code == 200: 47 | surname = response.json() 48 | except Exception as e: 49 | print(f'Unable to get surname from external system:\n{e}') 50 | print(f'No surname found for user {name}') 51 | data = {'user_id': user_id, 52 | 'age': age, 53 | 'surname': surname} 54 | 55 | return jsonify(data), 200 56 | 57 | else: 58 | return jsonify(f'User_name {name} not found'), 404 59 | 60 | 61 | if __name__ == '__main__': 62 | host = os.environ.get('APP_HOST', '127.0.0.1') 63 | port = os.environ.get('APP_PORT', '8083') 64 | 65 | app.run(host=host, port=port) 66 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | import time 5 | from copy import copy 6 | 7 | import requests 8 | 9 | import settings 10 | 11 | repo_root = os.path.abspath(os.path.join(__file__, os.pardir)) 12 | 13 | 14 | def wait_ready(host, port): 15 | started = False 16 | st = time.time() 17 | while time.time() - st <= 5: 18 | try: 19 | requests.get(f'http://{host}:{port}') 20 | started = True 21 | break 22 | except ConnectionError: 23 | pass 24 | 25 | if not started: 26 | raise RuntimeError('App did not started in 5s!') 27 | 28 | 29 | def pytest_configure(config): 30 | 31 | if not hasattr(config, 'workerinput'): 32 | ######### app configuration ######### 33 | 34 | app_path = os.path.join(repo_root, 'application', 'app.py') 35 | 36 | env = copy(os.environ) 37 | env.update({'APP_HOST': settings.APP_HOST, 'APP_PORT': settings.APP_PORT}) 38 | env.update({'STUB_HOST': settings.STUB_HOST, 'STUB_PORT': settings.STUB_PORT}) 39 | env.update({'MOCK_HOST': settings.MOCK_HOST, 'MOCK_PORT': settings.MOCK_PORT}) 40 | 41 | 42 | app_stderr = open('/tmp/stub_stderr', 'w') 43 | app_stdout = open('/tmp/stub_stdout', 'w') 44 | # windows 45 | # app_stderr_path = os.path.join(repo_root, 'tmp', 'app_stderr') 46 | # app_stdout_path = os.path.join(repo_root, 'tmp', 'app_stdout') 47 | # app_stderr = open(app_stderr_path, 'w') 48 | # app_stdout = open(app_stdout_path, 'w') 49 | app_proc = subprocess.Popen(['python3', app_path], stderr=app_stderr, stdout=app_stdout, env=env) 50 | # windows 51 | # app_proc = subprocess.Popen(['c:\\tp\\venv\\Scripts\\python', app_path], 52 | # stderr=app_stderr, stdout=app_stdout, env=env 53 | # ) 54 | config.app_proc = app_proc 55 | config.app_stderr = app_stderr 56 | config.app_stdout = app_stdout 57 | wait_ready(settings.APP_HOST, settings.APP_PORT) 58 | 59 | ######### stub configuration ######### 60 | 61 | # stub_path = os.path.join(repo_root, 'stub', 'flask_stub.py') 62 | stub_path = os.path.join(repo_root, 'stub', 'simple_http_server_stub.py') 63 | 64 | env = copy(os.environ) 65 | env.update({'APP_HOST': settings.STUB_HOST, 'APP_PORT': settings.STUB_PORT}) 66 | 67 | stub_stderr = open('/tmp/stub_stderr', 'w') 68 | stub_stdout = open('/tmp/stub_stdout', 'w') 69 | # windows 70 | # stub_stderr_path = os.path.join(repo_root, 'tmp', 'stub_stderr') 71 | # stub_stdout_path = os.path.join(repo_root, 'tmp', 'stub_stdout') 72 | # stub_stderr = open(stub_stderr_path, 'w') 73 | # stub_stdout = open(stub_stdout_path, 'w') 74 | 75 | # stub_proc = subprocess.Popen(['c:\\tp\\venv\\Scripts\\python', stub_path], 76 | stub_proc = subprocess.Popen(['python3', app_path], 77 | stderr=stub_stderr, stdout=stub_stdout, env=env 78 | ) 79 | import time; time.sleep(2) 80 | config.stub_proc = stub_proc 81 | config.stub_stderr = stub_stderr 82 | config.stub_stdout = stub_stdout 83 | wait_ready(settings.STUB_HOST, settings.STUB_PORT) 84 | 85 | 86 | ######### mock configuration ######### 87 | 88 | from mock import flask_mock 89 | flask_mock.run_mock() 90 | 91 | wait_ready(settings.MOCK_HOST, settings.MOCK_PORT) 92 | 93 | 94 | def pytest_unconfigure(config): 95 | # config.proc.send_signal(signal.SIGINT) 96 | # exit_code = config.proc.wait() 97 | # assert exit_code == 0 98 | 99 | config.app_proc.terminate() 100 | exit_code = config.app_proc.wait() 101 | 102 | config.app_stderr.close() 103 | config.app_stdout.close() 104 | 105 | assert exit_code == -15 106 | 107 | config.stub_proc.terminate() 108 | exit_code = config.stub_proc.wait() 109 | 110 | config.stub_stderr.close() 111 | config.stub_stdout.close() 112 | 113 | assert exit_code == -15 114 | 115 | # TODO: add mock shutdown 116 | requests.get(f'http://{settings.MOCK_HOST}:{settings.MOCK_PORT}/shutdown') 117 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/mock/flask_mock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import threading 4 | 5 | from flask import Flask, jsonify, request 6 | 7 | import settings 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | SURNAME_DATA = {} 13 | 14 | 15 | @app.route('/get_surname/', methods=['GET']) 16 | def get_user_surname(name): 17 | if surname := SURNAME_DATA.get(name): 18 | return jsonify(surname), 200 19 | else: 20 | return jsonify(f'Surname for user "{name}" not found'), 404 21 | 22 | 23 | def shutdown_stub(): 24 | terminate_func = request.environ.get('werkzeug.server.shutdown') 25 | if terminate_func: 26 | terminate_func() 27 | else: 28 | raise RuntimeError('Not running with the Werkzeug Server') 29 | 30 | 31 | @app.route('/shutdown') 32 | def shutdown(): 33 | shutdown_stub() 34 | return jsonify(f'Ok, exiting'), 200 35 | 36 | 37 | def run_mock(): 38 | server = threading.Thread(target=app.run, kwargs={ 39 | 'host': settings.MOCK_HOST, 40 | 'port': settings.MOCK_PORT 41 | }) 42 | 43 | server.start() 44 | return server 45 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection19-Docker/python-mock/code/requirements.txt -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/settings.py: -------------------------------------------------------------------------------- 1 | APP_HOST = '127.0.0.1' 2 | APP_PORT = '8083' 3 | 4 | STUB_HOST = '127.0.0.1' 5 | STUB_PORT = '8081' 6 | 7 | MOCK_HOST = '127.0.0.1' 8 | MOCK_PORT = '8082' 9 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/start_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | pytest -s -l -v tests/test.py --alluredir /tmp/allure -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/stub/flask_stub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import random 5 | 6 | from flask import Flask, jsonify 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | @app.route('/get_age/', methods=['GET']) 12 | def get_user_age(name): 13 | return jsonify(random.randint(18, 105)), 200 14 | 15 | 16 | if __name__ == '__main__': 17 | host = os.environ.get('STUB_HOST', '127.0.0.1') 18 | port = os.environ.get('STUB_PORT', '4444') 19 | 20 | app.run(host, port) 21 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/stub/simple_http_server_stub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import random 6 | from http.server import BaseHTTPRequestHandler, HTTPServer 7 | 8 | 9 | class AgeStubHandleRequests(BaseHTTPRequestHandler): 10 | data = None 11 | 12 | def _set_headers(self, status): 13 | self.send_response(status) 14 | self.send_header('Content-type', 'application/json') 15 | self.end_headers() 16 | 17 | def do_GET(self): 18 | location = self.path.split('/') 19 | if len(location) == 3 and location[1] == 'get_age' and isinstance(location[2], str): 20 | self._set_headers(200) 21 | self.wfile.write(json.dumps(random.randint(18, 105)).encode()) 22 | else: 23 | self._set_headers(500) 24 | self.wfile.write(b'error') 25 | 26 | 27 | if __name__ == '__main__': 28 | host = os.environ.get('STUB_HOST', '127.0.0.1') 29 | port = int(os.environ.get('STUB_PORT', 4444)) 30 | 31 | server = HTTPServer((host, port), RequestHandlerClass=AgeStubHandleRequests) 32 | server.allow_reuse_address = True 33 | 34 | server.serve_forever() 35 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/tests/test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | import settings 4 | from mock.flask_mock import SURNAME_DATA 5 | 6 | url = f'http://{settings.APP_HOST}:{settings.APP_PORT}' 7 | 8 | 9 | def test(): 10 | print(requests.get(url).text) 11 | 12 | 13 | def test_add_get_user(): 14 | resp = requests.post(f'{url}/add_user', json={'name': 'Kostya'}) 15 | user_id_from_add = resp.json()['user_id'] 16 | 17 | resp = requests.get(f'{url}/get_user/Kostya') 18 | user_id_from_get = resp.json()['user_id'] 19 | 20 | assert user_id_from_add == user_id_from_get 21 | 22 | 23 | def test_add_existent_user(): 24 | requests.post(f'{url}/add_user', json={'name': 'Vasya'}) 25 | resp = requests.post(f'{url}/add_user', json={'name': 'Vasya'}) 26 | 27 | assert resp.status_code == 400 28 | 29 | 30 | def test_get_non_existent_user(): 31 | resp = requests.get(f'{url}/Masha') 32 | 33 | assert resp.status_code == 404 34 | 35 | 36 | def test_with_age(): 37 | requests.post(f'{url}/add_user', json={'name': 'Stepan'}) 38 | 39 | resp = requests.get(f'{url}/get_user/Stepan') 40 | age = resp.json()['age'] 41 | assert isinstance(age, int) 42 | assert 18 <= age <= 105 43 | 44 | 45 | def test_has_surname(): 46 | requests.post(f'{url}/add_user', json={'name': 'Olya'}) 47 | SURNAME_DATA['Olya'] = 'OLOLOEVA' 48 | 49 | resp = requests.get(f'{url}/get_user/Olya') 50 | surname = resp.json()['surname'] 51 | assert surname == 'OLOLOEVA' 52 | 53 | 54 | def test_has_no_surname(): 55 | requests.post(f'{url}/add_user', json={'name': 'Sveta'}) 56 | 57 | resp = requests.get(f'{url}/get_user/Sveta') 58 | surname = resp.json()['surname'] 59 | assert surname == None 60 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/code/tests/test_socket.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from _socket import timeout 3 | 4 | import settings 5 | 6 | 7 | url = f'http://{settings.APP_HOST}:{settings.APP_PORT}' 8 | 9 | 10 | def test_with_socket_client(): 11 | requests.post(f'{url}/add_user', json={'name': '123'}) 12 | 13 | import socket 14 | import json 15 | 16 | target_host = settings.APP_HOST 17 | target_port = int(settings.APP_PORT) 18 | 19 | params = '/get_user/123' 20 | 21 | # создаём объект клиентского сокета 22 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 23 | 24 | # выставляем ожидание для сокета 25 | client.settimeout(0.1) 26 | 27 | # устанавливаем соединение 28 | client.connect((target_host, target_port)) 29 | 30 | # создаем и выполняем запрос 31 | request = f'GET {params} HTTP/1.1\r\nHost:{target_host}\r\n\r\n' 32 | client.send(request.encode()) 33 | 34 | total_data = [] 35 | 36 | try: 37 | while True: 38 | # читаем данные из сокета до тех пор пока они там есть 39 | data = client.recv(4096) 40 | if data: 41 | print(f'received data: {data}') 42 | total_data.append(data.decode()) 43 | else: 44 | break 45 | except timeout: 46 | pass 47 | 48 | data = ''.join(total_data).splitlines() 49 | assert json.loads(data[-1])['age'] > 0 50 | -------------------------------------------------------------------------------- /lection19-Docker/python-mock/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/lection19-Docker/python-mock/requirements.txt -------------------------------------------------------------------------------- /lection19-Docker/qa-nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | RUN yum update -y 4 | RUN yum install epel-release -y 5 | RUN yum install nginx -y 6 | 7 | ADD nginx.conf /etc/nginx/nginx.conf 8 | 9 | EXPOSE 80 10 | 11 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /lection19-Docker/qa-nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | error_log /var/log/nginx/error.log; 4 | pid /run/nginx.pid; 5 | 6 | include /usr/share/nginx/modules/*.conf; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 14 | '$status $body_bytes_sent "$http_referer" ' 15 | '"$http_user_agent" "$http_x_forwarded_for"'; 16 | 17 | access_log /var/log/nginx/access.log main; 18 | 19 | sendfile on; 20 | tcp_nopush on; 21 | tcp_nodelay on; 22 | keepalive_timeout 65; 23 | types_hash_max_size 2048; 24 | 25 | include /etc/nginx/mime.types; 26 | default_type application/octet-stream; 27 | 28 | include /etc/nginx/conf.d/*.conf; 29 | 30 | server { 31 | listen 80 default_server; 32 | listen [::]:80 default_server; 33 | server_name _; 34 | root /usr/share/nginx/html; 35 | 36 | include /etc/nginx/default.d/*.conf; 37 | 38 | location / { 39 | add_header Content-Type text/plain; 40 | return 200 'Hi!'; 41 | } 42 | 43 | error_page 404 /404.html; 44 | location = /40x.html { 45 | } 46 | 47 | error_page 500 502 503 504 /50x.html; 48 | location = /50x.html { 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK-Education-QA-Python/education-vk-python-2022/15f172e851bc019b2e19695c06a7355889a9751b/requirements.txt --------------------------------------------------------------------------------