├── .gitignore ├── .travis.yml ├── README.md ├── conftest.py ├── requirements.txt └── tests ├── test_add_to_cart.py ├── test_login_fail.py ├── test_login_success.py └── test_remove_from_cart.py /.gitignore: -------------------------------------------------------------------------------- 1 | */**/*~ 2 | .cache 3 | .pytest_cache/**/* 4 | *.testlog 5 | venv*/ 6 | .idea/ 7 | .DS_Store 8 | .vscode 9 | __pycache__ 10 | .vscode/**/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.6" 6 | install: pip install -r requirements.txt 7 | script: pytest -n 10 8 | env: 9 | global: 10 | - SAUCE_USERNAME=sauce_examples 11 | addons: 12 | jwt: 13 | secure: mpyuz5I26OGYdKkEguK0W4aqAhmswjEKz0zqEy8lyXR19utEHxHPx/yIPrlGO3A5XwC4liEnzBCzMm01sHjZWLu7KU6be84D2CQbTFV9lO2JAyl4SowgOUJrCGmf0wNOuiQdnNoHKm7CCef2OYUrgvBIuu6Nt5Z+GZAxMQHEv5Ot7gxg7ug0Xn23zcNm9xUkvOZI0TRaGMBuBTyFj4N1jL4xiT23wCdyYejHiWr76VFTMFnEWuDtwxAyyrJ1SftU1RN1r3Od7EhKTF4A2OavOsoBC8/V7FcMSaIfdViEH/c1uowJFC/Ffnwn9TUdbM3EGaqII1Rqm7tO/yEqhRRIfOFOBRq49IvzJQ9KDG+Izv6zoQ/Tl9YV5A4aTyOS2YBsHmvI3G/P0nLw4X2jQdURT9FjpbMT7LJjsWmFrL8VF2EJXfAN0UdXs0skmfC6LyLD6++5y+NRfXHgurFlTi+8bjjehu4pDTDcl9LNxgZEGmn5yHgMToxyFWSjQOzo6exf8ZmXQVbtjx6HyW4Atwb4YrJbKJ2MGBi44rcKRI5gD4jllXU3pv7TeQqWjpmzOsz0408RZ98yr5pz0M8ZZb4OzEVdNCRa/309hNblf/hJGTyMI9Yb08PkRkLPnVDWFqHZWXVjmSnreRIKBJiSFpsq3LvPvbv2HauV0Wc9Kn+/UwQ= 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Python-Pytest-Selenium 2 | [![Travis Status](https://travis-ci.org/saucelabs-sample-test-frameworks/Python-Pytest-Selenium.svg?branch=master)](https://travis-ci.org/saucelabs-sample-test-frameworks/Python-Pytest-Selenium) 3 | 4 | *NOTE*: This repository is now deprecated. Please see the [Demo Python](https://github.com/saucelabs-training/demo-python) project for up-to-date code samples. 5 | 6 | This code is meant for actual py.test users with aspirations beyond running unittest style tests using py.test. 7 | It demonstrates the use of py.test fixtures in conjunction with Selenium and SauceLabs. 8 | 9 | This code is provided on an "AS-IS” basis without warranty of any kind, either express or implied, including without limitation any implied warranties of condition, uninterrupted use, merchantability, fitness for a particular purpose, or non-infringement. Your tests and testing environments may require you to modify this framework. Issues regarding this framework should be submitted through GitHub. For questions regarding Sauce Labs integration, please see the Sauce Labs documentation at https://wiki.saucelabs.com/. This framework is not maintained by Sauce Labs Support. 10 | 11 | ### Environment Setup 12 | 13 | 1. Global Dependencies 14 | * [Install Python](https://www.python.org/downloads/) 15 | * Or Install Python with [Homebrew](http://brew.sh/) 16 | ``` 17 | $ brew install python 18 | ``` 19 | * Install [pip](https://pip.pypa.io/en/stable/installing/) for package installation 20 | 21 | 2. Sauce Credentials 22 | * In the terminal export your Sauce Labs Credentials as environmental variables: 23 | ``` 24 | $ export SAUCE_USERNAME= 25 | $ export SAUCE_ACCESS_KEY= 26 | ``` 27 | 3. Project 28 | * The recommended way to run your tests would be in [virtualenv](https://virtualenv.readthedocs.org/en/latest/). It will isolate the build from other setups you may have running and ensure that the tests run with the specified versions of the modules specified in the requirements.txt file. 29 | ```$ pip install virtualenv``` 30 | * Create a virtual environment in your project folder the environment name is arbitrary. 31 | ```$ virtualenv venv``` 32 | * Activate the environment: 33 | ```$ source venv/bin/activate``` 34 | * Install the required packages: 35 | ```$ pip install -r requirements.txt``` 36 | 37 | ### Running Tests: -n option designates number of parallel tests and -s to disable output capture. 38 | 39 | * Tests in Parallel: 40 | ```$ py.test -s -n 2 tests``` 41 | 42 | * Dump session ids for the SauceLabs CI plugins: 43 | ```$ cat $(find . -name "*.testlog")``` 44 | 45 | 46 | [Sauce Labs Dashboard](https://saucelabs.com/beta/dashboard/) 47 | 48 | ### Advice/Troubleshooting 49 | 50 | There may be additional latency when using a remote webdriver to run tests on Sauce Labs. Timeouts or Waits may need to be increased. 51 | * [Selenium tips regarding explicit waits](https://wiki.saucelabs.com/display/DOCS/Best+Practice%3A+Use+Explicit+Waits) 52 | 53 | ### Resources 54 | ##### [Sauce Labs Documentation](https://wiki.saucelabs.com/) 55 | 56 | ##### [Selenium Documentation](http://www.seleniumhq.org/docs/) 57 | 58 | ##### [Python Documentation](https://docs.python.org/2.7/) 59 | 60 | ##### [Pytest Documentation](http://pytest.org/latest/contents.html) 61 | 62 | ##### [Stack Overflow](http://stackoverflow.com/) 63 | * A great resource to search for issues not explicitly covered by documentation. 64 | 65 | ### Known Issues: 66 | * Test output will be captured in .testlog files as the pytest-xdist plugin has issues with not capturing stdout and stderr. You can use the following commands to output session id's for CI integration and clean up. 67 | ``` 68 | $ cat *.testlog 69 | $ rm -rf *.testlog 70 | ``` 71 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from os import environ 3 | 4 | from selenium import webdriver 5 | from selenium.common.exceptions import WebDriverException 6 | from selenium.webdriver.remote.remote_connection import RemoteConnection 7 | 8 | import urllib3 9 | urllib3.disable_warnings() 10 | 11 | browsers = [ 12 | { 13 | "seleniumVersion": '3.4.0', 14 | "platform": "Windows 10", 15 | "browserName": "MicrosoftEdge", 16 | "version": "latest" 17 | }, { 18 | "seleniumVersion": '3.4.0', 19 | "platform": "Windows 10", 20 | "browserName": "firefox", 21 | "version": "latest" 22 | }, { 23 | "seleniumVersion": '3.4.0', 24 | "platform": "Windows 7", 25 | "browserName": "internet explorer", 26 | "version": "latest" 27 | }, { 28 | "seleniumVersion": '3.4.0', 29 | "platform": "OS X 10.13", 30 | "browserName": "safari", 31 | "version": "latest-1" 32 | }, { 33 | "seleniumVersion": '3.4.0', 34 | "platform": "OS X 10.11", 35 | "browserName": "chrome", 36 | "version": "latest", 37 | "extendedDebugging": True 38 | }] 39 | 40 | def pytest_generate_tests(metafunc): 41 | if 'driver' in metafunc.fixturenames: 42 | metafunc.parametrize('browser_config', 43 | browsers, 44 | ids=_generate_param_ids('broswerConfig', browsers), 45 | scope='function') 46 | 47 | 48 | def _generate_param_ids(name, values): 49 | return [("<%s:%s>" % (name, value)).replace('.', '_') for value in values] 50 | 51 | 52 | @pytest.yield_fixture(scope='function') 53 | def driver(request, browser_config): 54 | # if the assignment below does not make sense to you please read up on object assignments. 55 | # The point is to make a copy and not mess with the original test spec. 56 | desired_caps = dict() 57 | desired_caps.update(browser_config) 58 | test_name = request.node.name 59 | build_tag = environ.get('BUILD_TAG', None) 60 | tunnel_id = environ.get('TUNNEL_IDENTIFIER', None) 61 | username = environ.get('SAUCE_USERNAME', None) 62 | access_key = environ.get('SAUCE_ACCESS_KEY', None) 63 | 64 | selenium_endpoint = "https://%s:%s@ondemand.saucelabs.com:443/wd/hub" % (username, access_key) 65 | desired_caps['build'] = build_tag 66 | # we can move this to the config load or not, also messing with this on a test to test basis is possible :) 67 | desired_caps['tunnelIdentifier'] = tunnel_id 68 | desired_caps['name'] = test_name 69 | 70 | executor = RemoteConnection(selenium_endpoint, resolve_ip=False) 71 | browser = webdriver.Remote( 72 | command_executor=executor, 73 | desired_capabilities=desired_caps, 74 | keep_alive=True 75 | ) 76 | 77 | # This is specifically for SauceLabs plugin. 78 | # In case test fails after selenium session creation having this here will help track it down. 79 | # creates one file per test non ideal but xdist is awful 80 | if browser is not None: 81 | print("SauceOnDemandSessionID={} job-name={}".format(browser.session_id, test_name)) 82 | else: 83 | raise WebDriverException("Never created!") 84 | 85 | yield browser 86 | # Teardown starts here 87 | # report results 88 | # use the test result to send the pass/fail status to Sauce Labs 89 | sauce_result = "failed" if request.node.rep_call.failed else "passed" 90 | browser.execute_script("sauce:job-result={}".format(sauce_result)) 91 | browser.quit() 92 | 93 | 94 | @pytest.hookimpl(hookwrapper=True, tryfirst=True) 95 | def pytest_runtest_makereport(item, call): 96 | # this sets the result as a test attribute for Sauce Labs reporting. 97 | # execute all other hooks to obtain the report object 98 | outcome = yield 99 | rep = outcome.get_result() 100 | 101 | # set an report attribute for each phase of a call, which can 102 | # be "setup", "call", "teardown" 103 | setattr(item, "rep_" + rep.when, rep) 104 | 105 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==3.14.0 2 | sauceclient>=0.2.1 3 | pytest==4.4.0 4 | pytest-xdist 5 | requests 6 | -------------------------------------------------------------------------------- /tests/test_add_to_cart.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures("driver") 5 | def test_add_to_cart(driver): 6 | driver.get('http://www.saucedemo.com/inventory.html') 7 | driver.find_element_by_class_name('btn_primary').click() 8 | 9 | assert driver.find_element_by_class_name('shopping_cart_badge').text == '1' 10 | 11 | driver.get('http://www.saucedemo.com/cart.html') 12 | expected = driver.find_elements_by_class_name('inventory_item_name') 13 | assert len(expected) == 1 14 | 15 | @pytest.mark.usefixtures("driver") 16 | def test_add_two_to_cart(driver): 17 | driver.get('http://www.saucedemo.com/inventory.html') 18 | driver.find_element_by_class_name('btn_primary').click() 19 | driver.find_element_by_class_name('btn_primary').click() 20 | 21 | assert driver.find_element_by_class_name('shopping_cart_badge').text == '2' 22 | 23 | driver.get('http://www.saucedemo.com/cart.html') 24 | expected = driver.find_elements_by_class_name('inventory_item_name') 25 | assert len(expected) == 2 26 | -------------------------------------------------------------------------------- /tests/test_login_fail.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures("driver") 5 | def test_valid_crentials_login(driver): 6 | driver.get('http://www.saucedemo.com') 7 | 8 | driver.find_element_by_id('user-name').send_keys('locked_out_user') 9 | driver.find_element_by_id('password').send_keys('secret_sauce') 10 | driver.find_element_by_css_selector('.btn_action').click() 11 | 12 | assert driver.find_element_by_css_selector('.error-button').is_displayed() 13 | -------------------------------------------------------------------------------- /tests/test_login_success.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures("driver") 5 | def test_valid_crentials_login(driver): 6 | driver.get('http://www.saucedemo.com') 7 | 8 | driver.find_element_by_id('user-name').send_keys('standard_user') 9 | driver.find_element_by_id('password').send_keys('secret_sauce') 10 | driver.find_element_by_css_selector('.btn_action').click() 11 | 12 | assert "/inventory.html" in driver.current_url 13 | -------------------------------------------------------------------------------- /tests/test_remove_from_cart.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures("driver") 5 | def test_add_and_remove_from_cart(driver): 6 | driver.get('http://www.saucedemo.com/inventory.html') 7 | driver.find_element_by_class_name('btn_primary').click() 8 | driver.find_element_by_class_name('btn_primary').click() 9 | driver.find_element_by_class_name('btn_secondary').click() 10 | 11 | assert driver.find_element_by_class_name('shopping_cart_badge').text == '1' 12 | 13 | driver.get('http://www.saucedemo.com/cart.html') 14 | expected = driver.find_elements_by_class_name('inventory_item_name') 15 | assert len(expected) == 1 16 | --------------------------------------------------------------------------------