{"id":21372387,"url":"https://github.com/automationpanda/tau-intro-selenium-py","last_synced_at":"2025-05-07T13:01:26.539Z","repository":{"id":45974196,"uuid":"237553876","full_name":"AutomationPanda/tau-intro-selenium-py","owner":"AutomationPanda","description":"Test Automation University: Introduction to Selenium WebDriver with Python (Example Code)","archived":false,"fork":false,"pushed_at":"2024-04-17T19:29:06.000Z","size":2251,"stargazers_count":86,"open_issues_count":0,"forks_count":142,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-31T10:11:58.533Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AutomationPanda.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-01T03:08:12.000Z","updated_at":"2025-03-25T11:47:14.000Z","dependencies_parsed_at":"2024-04-28T03:37:41.079Z","dependency_job_id":"6a07a9cb-9c7b-45df-a598-4b17e6bf272b","html_url":"https://github.com/AutomationPanda/tau-intro-selenium-py","commit_stats":null,"previous_names":["automationpanda/tau-intro-selenium-py"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AutomationPanda%2Ftau-intro-selenium-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AutomationPanda%2Ftau-intro-selenium-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AutomationPanda%2Ftau-intro-selenium-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AutomationPanda%2Ftau-intro-selenium-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AutomationPanda","download_url":"https://codeload.github.com/AutomationPanda/tau-intro-selenium-py/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252883219,"owners_count":21819157,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-22T08:19:29.150Z","updated_at":"2025-05-07T13:01:26.484Z","avatar_url":"https://github.com/AutomationPanda.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tau-intro-selenium-py\nThis repository contains the companion project for the\n*Introduction to Selenium WebDriver with Python* course\ntaught by [Andrew \"Pandy\" Knight](https://twitter.com/AutomationPanda)\non [Test Automation University](https://testautomationu.applitools.com/).\nDuring the course, you will build a basic Web UI test automation solution using Python and Selenium WebDriver.\nEach chapter will add a new layer to the solution.\nFollow the instructions in this README to code the solution as you take each chapter.\nIf you get stuck, refer to the example code in this repository for help.\n\n# Setup Instructions\n\n## Python Setup\n\nYou can complete this course using any OS: Windows, macOS, Linux, etc.\n\nThis course requires Python 3.8 or higher.\nYou can download the latest Python version from [Python.org](https://www.python.org/downloads/).\n\nThis course also requires [pipenv](https://docs.pipenv.org/).\nTo install pipenv, run `pip install pipenv` from the command line.\n\nYou should also have a Python editor/IDE of your choice.\nGood choices include [PyCharm](https://www.jetbrains.com/pycharm/)\nand [Visual Studio Code](https://code.visualstudio.com/docs/languages/python).\n\nYou will also need [Git](https://git-scm.com/) to copy this project code.\nIf you are new to Git, [try learning the basics](https://try.github.io/).\n\n## WebDriver Setup\n\nFor Web UI testing, you will need to install the latest versions of\n[Google Chrome](https://www.google.com/chrome/)\nand [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/).\nYou can use other browsers with Selenium WebDriver, but the course will use Chrome and Firefox.\n\nYou will also need to install the latest versions of the WebDriver executables for these browsers: [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/) for Chrome\nand [geckodriver](https://github.com/mozilla/geckodriver/releases) for Firefox.\nEach test case will launch the WebDriver executable for its target browser.\nThe WebDriver executable will act as a proxy between the test automation and the browser instance.\nPlease use the latest versions of both the browsers and the WebDriver executables.\nOlder versions might be incompatible with each other.\n\nChromeDriver and geckodriver must be installed on the\n[system path](https://en.wikipedia.org/wiki/PATH_(variable)).\n\n### WebDriver Setup for Windows\n\nTo install ChromeDriver and geckodriver on Windows:\n\n1. Create a folder named `C:\\Selenium`.\n2. Move the executables into this folder.\n3. Add this folder to the *Path* environment variable. (See [How to Add to Windows PATH Environment Variable](https://helpdeskgeek.com/windows-10/add-windows-path-environment-variable/).)\n\n### WebDriver Setup for *NIX\n\nTo install ChromeDriver and geckodriver on Linux, macOS, and other UNIX variants,\nsimply move them to the `/usr/local/bin/` directory:\n\n```bash\n$ mv /path/to/ChromeDriver /usr/local/bin\n$ mv /path/to/geckodriver /usr/local/bin\n```\n\nThis directory should already be included in the system path.\nFor troubleshooting, see:\n\n* [Setting the path on macOS](https://www.cyberciti.biz/faq/appleosx-bash-unix-change-set-path-environment-variable/)\n* [Setting the path on Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)\n\n### Test WebDriver Setup\n\nTo verify correct setup on any operating system, simply try to run them from the terminal:\n\n```bash\n$ ChromeDriver\n$ geckodriver\n```\n\nYou may or may not see any output.\nJust verify that you can run them without errors.\nUse Ctrl-C to kill them.\n\n## Project Setup\n\n1. Clone this repository.\n2. Run `cd tau-intro-selenium-py` to enter the project.\n3. Run `pipenv install` to install the dependencies.\n4. Run `pipenv run python -m pytest` to verify that the framework can run tests.\n5. Create a branch for your code changes. (See *Repository Branching* below.)\n\n### Project Setup Troubleshooting\n\nA few people attempting to set up this project\nencountered the following error when executing `pipenv run python -m pytest`:\n\n```\nModuleNotFoundError: No module named 'atomicwrites'\n```\n\nI'm not exactly sure why `pipenv install` does not include `atomicwrites`.\nSo far, I have seen it happen only on Windows.\nTo resolve the error, please attempt the following:\n\n* Upgrade Python to the latest versions. The following worked for me on Windows:\n  * Python 3.8.3 (`python --version`)\n  * pip 20.1 (`pip --version`)\n  * pipenv 2018.11.26 (`pipenv --version`)\n* Run `pipenv update` from within the project directory.\n\nIf upgrades don't work, try forcing package installation:\n\n* Run `pipenv install pytest` from within the project directory.\n* Run `pipenv install atomicwrites` from within the project directory.\n\nIf these steps don't work in your project, then try to run without pipenv:\n\n* Install Python packages directly using `pip`.\n* Run tests directly using `python -m pytest`.\n\n## Repository Branching\n\nThe `master` branch contains the code for the course's starting point.\nThe project is basically empty in the `master` branch.\n\nIf you want to code along with the course, then create a branch for your work off the `master` branch.\nTo create your own branch named `course/develop`, run:\n\n    \u003e git checkout master\n    \u003e git branch course/develop\n    \u003e git checkout course/develop\n\nThe `example/*` branches contain the completed code for course parts.\nIf you get stuck, you can always check the example code.\n\n* `example/2-pytest-setup`\n* `example/3-webdriver-setup`\n* `example/4-page-objects`\n* `example/5-locators`\n* `example/6-webdriver-calls`\n* `example/7-browser-config`\n* `example/8-race-conditions`\n* `example/9-parallel-testing`\n* `example/develop` (main development branch for the examples)\n\n(*Note:* Chapter 1 does not have any example code.)\n\n# Course Instructions\n\n## Chapter 1: Writing Our First Web UI Test\n\n*No Example Branch for this chapter*\n\nWe should always write test *cases* before writing any test *code*.\nTest cases are procedures that exercise behavior to verify goodness and identify badness.\nTest code simply automates test cases.\nWriting a test case first helps us form our thoughts well.\nI like to write my test cases in\n[Gherkin](https://automationpanda.com/2017/01/26/bdd-101-the-gherkin-language/).\n\nIn this course, we will automate a test for a basic DuckDuckGo search.\n[DuckDuckGo](https://duckduckgo.com/) is a popular search engine that's easy to test.\nHere's our first Web UI test case:\n\n```gherkin\nScenario: Basic DuckDuckGo Search\n    Given the DuckDuckGo home page is displayed\n    When the user searches for \"panda\"\n    Then the search result title contains \"panda\"\n    And the search result query is \"panda\"\n    And the search result links pertain to \"panda\"\n```\n\n## Chapter 2: Setting Up pytest\n\n*Example Branch: example/2-pytest-setup*\n\nLet's implement the test using pytest.\nCreate a new file named `test_search.py` under the `tests` directory,\nand add the following code:\n\n```python\n\"\"\"\nThese tests cover DuckDuckGo searches.\n\"\"\"\n\ndef test_basic_duckduckgo_search():\n\n    # Given the DuckDuckGo home page is displayed\n    # TODO\n\n    # When the user searches for \"panda\"\n    # TODO\n\n    # Then the search result title contains \"panda\"\n    # TODO\n    \n    # And the search result query is \"panda\"\n    # TODO\n    \n    # And the search result links pertain to \"panda\"\n    # TODO\n\n    raise Exception(\"Incomplete Test\")\n```\n\nAdding comments to stub each step may seem trivial,\nbut it's a good first step when writing new test cases.\nWe can simply add code at each TODO line as we automate.\nOnce we finish writing the test's code, we will remove the exception at the end.\nAlso, note that pytest expects all test functions to begin with `test_`.\n\nTo avoid confusion when we run tests, let's remove the old placeholder test.\nDelete `tests/test_fw.py`.\n\nRerun the tests using `pipenv run python -m pytest`.\nThe `test_basic_duckduckgo_search` should be the only test that runs,\nand it should fail due to the \"Incomplete Test\" exception.\n\n## Chapter 3: Setting Up Selenium WebDriver\n\n*Example Branch: example/3-webdriver-setup*\n\n[Selenium WebDriver](https://www.seleniumhq.org/projects/webdriver/)\nis a tool for automating Web UI interactions with live browsers.\nIt works with several popular programming languages and browser types.\n\nThe Selenium WebDriver package for Python is named `selenium`.\nRun `pipenv install selenium` to install it for our project.\n\nEvery test should use its own WebDriver instance.\nThis keeps things simple and safe.\nThe best way to set up the WebDriver instance is to use a\n[pytest fixture](https://docs.pytest.org/en/latest/fixture.html).\nFixtures are basically setup and cleanup functions.\nAs a best practice, they should be placed in a `conftest.py` module so they can be used by any test.\n\nCreate a new file named `tests/conftest.py` and add the following code:\n\n```python\n\"\"\"\nThis module contains shared fixtures.\n\"\"\"\n\nimport pytest\nimport selenium.webdriver\n\n\n@pytest.fixture\ndef browser():\n\n  # Initialize the ChromeDriver instance\n  b = selenium.webdriver.Chrome()\n\n  # Make its calls wait up to 10 seconds for elements to appear\n  b.implicitly_wait(10)\n\n  # Return the WebDriver instance for the setup\n  yield b\n\n  # Quit the WebDriver instance for the cleanup\n  b.quit()\n```\n\nThe `browser` fixture uses Chrome.\nOther browser types could be used instead.\nReal-world projects often read browser choice from a config file here.\n\nThe implicit wait will make sure WebDriver calls wait for elements to appear before sending calls to them.\n10 seconds should be reasonable for this test project's needs.\nFor larger projects, however, setting explicit waits is a better practice\nbecause different calls need different wait times.\nRead more about implicit versus explicit waits [here](https://selenium-python.readthedocs.io/waits.html).\n\nThe `yield` statement makes the `browser` fixture a generator.\nThe first iteration will do the \"setup\" steps,\nwhile the second iteration will do the \"cleanup\" steps.\nEach test must make sure to *quit* the WebDriver instance as part of cleanup,\nor else zombie processes might lock system resources!\n\nNow, update `test_basic_duckduckgo_search` in `tests/test_search.py` to call the new fixture:\n\n```python\ndef test_basic_duckduckgo_search(browser):\n  # ...\n```\n\nWhenever a pytest test function declares a fixture by name as an argument,\npytest will automatically call that fixture before the test runs.\nWhatever the fixture returns will be passed into the test function.\nTherefore, we can access the WebDriver instance using the `browser` variable!\n\nRerun the test using `pipenv run python -m pytest` to test the fixture.\nEven though the test should still fail,\nChrome should briefly pop up for a few seconds while the test is running.\nMake sure Chrome quits once the test is done.\nThen, commit your latest code changes.\n\n## Chapter 4: Defining Page Objects\n\n*Example Branch: example/4-page-objects*\n\nA **page object** is an object representing a Web page or component.\nThey have *locators* for finding elements,\nas well as *interaction methods* that interact with the page under test.\nPage objects make low-level Selenium WebDriver calls\nso that tests can make short, readable calls instead of complex ones.\n\nSince we have our test steps, we know what pages and elements our test needs.\nThere are two pages under test, each with a few interactions:\n\n1. The DuckDuckGo search page\n   * Load the page\n   * Search a phrase\n2. The DuckDuckGo results page\n   * Get the result link titles\n   * Get the search query\n   * Get the title\n\nLet's write stubs for our page object classes.\nEach interaction should have its own method.\nLater, we can implement the interaction methods with Selenium WebDriver calls.\nCreate a new Python package named `pages`.\nTo do this create a directory under the root directory named `pages`.\nThen, put a blank file in it named `__init__.py`.\nThe `pages` directory should *not* be under the `tests` directory.\nWhy? When using pytest, the `tests` folder should *not* be a package.\n\nCreate a new module named `pages/search.py` and add the following code\nfor the DuckDuckGo search page:\n\n```python\n\"\"\"\nThis module contains DuckDuckGoSearchPage,\nthe page object for the DuckDuckGo search page.\n\"\"\"\n\n\nclass DuckDuckGoSearchPage:\n\n  def __init__(self, browser):\n    self.browser = browser\n\n  def load(self):\n    # TODO\n    pass\n\n  def search(self, phrase):\n    # TODO\n    pass\n```\n\nCreate another new module named `pages/result.py` and add the following code\nfor the DuckDuckGo result page:\n\n```python\n\"\"\"\nThis module contains DuckDuckGoResultPage,\nthe page object for the DuckDuckGo search result page.\n\"\"\"\n\n\nclass DuckDuckGoResultPage:\n  \n  def __init__(self, browser):\n    self.browser = browser\n\n  def result_link_titles(self):\n    # TODO\n    return []\n  \n  def search_input_value(self):\n    # TODO\n    return \"\"\n\n  def title(self):\n    # TODO\n    return \"\"\n```\n\nEvery page object needs a reference to the WebDriver instance.\nThat's why the `__init__` methods take in and store a reference to `browser`.\n\nFinally, update `test_basic_duckduckgo_search` in `tests/test_search.py`\nwith the following code:\n\n```python\n\"\"\"\nThese tests cover DuckDuckGo searches.\n\"\"\"\n\nfrom pages.result import DuckDuckGoResultPage\nfrom pages.search import DuckDuckGoSearchPage\n\n\ndef test_basic_duckduckgo_search(browser):\n  search_page = DuckDuckGoSearchPage(browser)\n  result_page = DuckDuckGoResultPage(browser)\n  PHRASE = \"panda\"\n  \n  # Given the DuckDuckGo home page is displayed\n  search_page.load()\n\n  # When the user searches for \"panda\"\n  search_page.search(PHRASE)\n\n  # Then the search result title contains \"panda\"\n  assert PHRASE in result_page.title()\n  \n  # And the search result query is \"panda\"\n  assert PHRASE == result_page.search_input_value()\n  \n  # And the search result links pertain to \"panda\"\n  for title in result_page.result_link_titles():\n    assert PHRASE.lower() in title.lower()\n\n  # TODO: Remove this exception once the test is complete\n  raise Exception(\"Incomplete Test\")\n```\n\nNotice how we are able to write all the test steps using page object calls and assertions.\nWe also kept the step comments so the code is well-documented.\nEven though we haven't made any Selenium WebDriver calls, our test case function is nearly complete!\nOur code is readable and understandable.\nIt delivers clear testing value.\n\nRerun the test using `pipenv run python -m pytest`.\nThe test should fail again, but this time, it should fail on one of the assertions.\nThen, commit your latest code changes.\n\n## Chapter 5: Finding Locators for Elements\n\n*Example Branch: example/5-locators*\n\nAn *element* is a \"thing\" on a Web page.\nBrowsers render elements such as buttons, dropdowns, and input fields using the page's HTML code.\nUsers interact directly with the page's elements.\nTests use page objects to interact with elements like a user.\n\nInteractions typically require three steps:\n\n1. Wait for the target element to exist\n2. Get an object representing the target element\n3. Send commands to the element object\n\nIn our solution, waiting is handled automatically thanks to the browser fixture's `implicitly_wait` call.\nGetting the element object, however, requires a locator.\n\n*Locators* are query strings that use HTML attributes to find elements on a Web page.\nThere are many types of locators:\n\n* ID\n* Name\n* Class name\n* CSS Selector\n* XPath\n* Link text\n* Partial link text\n* Tag name\n\nFor example, if the page has the following element:\n\n```html\n\u003cbutton id=\"django_ok\"\u003eOK\u003c/button\u003e\n```\n\nThen, a page object could use an ID locator for \"django_ok\" to get this element.\n\nLocators are not element objects themselves but instead point to elements. \nThe WebDriver instance uses locators to fetch and construct element objects.\nWhy are locators and elements separate concerns?\nElements on a page are always changing:\nthey may take time to load, or they may change with user interaction.\nLocators, however, are always the same:\nthey simply specify how to get elements.\nFor example, a locator could be used to prove that an element does *not* exist.\n\nFor our test, we need locators for three elements:\n\n1. The search input on the DuckDuckGo search page\n2. The search input on the DuckDuckGo results page\n3. The result links on the DuckDuckGo results page\n\n(Note: The page title is not a Web element. It can be fetched as a `browser` property.)\n\nWriting good locators is a bit of an art.\nInspecting the HTML source of a live page makes it easy.\nTo do this, open the [DuckDuckGo search page](https://duckduckgo.com/) in Chrome.\nThen, right-click the page and select \"Inspect\".\n[Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/) will open.\nThe \"Elements\" tab shows the HTML source.\nAs you move the cursor over HTML elements in the source,\nChrome will highlight the elements on the page.\nNow, click the icon with the box and cursor in the upper-left corner of the DevTools pane.\nMove the cursor over elements on the page, and you will see them highlighted in the source.\nNeat!\n\nTry to find the search input element.\nIts HTML should look like this:\n\n```html\n\u003cinput\n  id=\"search_form_input_homepage\"\n  class=\"js-search-input search__input--adv\"\n  type=\"text\"\n  autocomplete=\"off\"\n  name=\"q\"\n  tabindex=\"1\"\n  value=\"\"\n  autocapitalize=\"off\"\n  autocorrect=\"off\"\u003e\n```\n\nNotice that there is an `id` attribute set to \"search_form_input_homepage\".\nLet's use this as our locator and update `pages/search.py`.\n\nFirst, import `By` from the `selenium` package so we can write locators:\n\n```python\nfrom selenium.webdriver.common.by import By\n```\n\nThen, add the following attribute to the `DuckDuckGoSearchPage` class:\n\n```python\nSEARCH_INPUT = (By.ID, 'search_form_input_homepage')\n```\n\n`By` contains property keys for each type of locator.\nWe can write locators as tuples of the locator type and the query string.\n(We will use this locator for interaction calls in the next part of the course.)\n\nThe full code for `pages/search.py` should now look like this:\n\n```python\n\"\"\"\nThis module contains DuckDuckGoSearchPage,\nthe page object for the DuckDuckGo search page.\n\"\"\"\n\nfrom selenium.webdriver.common.by import By\n\n\nclass DuckDuckGoSearchPage:\n\n  SEARCH_INPUT = (By.ID, 'search_form_input_homepage')\n\n  def __init__(self, browser):\n    self.browser = browser\n\n  def load(self):\n    # TODO\n    pass\n\n  def search(self, phrase):\n    # TODO\n    pass\n```\n\nLet's write locators for the `DuckDuckGoResultPage` next.\nPerform a search, inspect the page, and try to come up with locators on your own.\n\nBelow is the code for `pages/result.py` with locators:\n\n```python\n\"\"\"\nThis module contains DuckDuckGoResultPage,\nthe page object for the DuckDuckGo search result page.\n\"\"\"\n\nfrom selenium.webdriver.common.by import By\n\n\nclass DuckDuckGoResultPage:\n  \n  RESULT_LINKS = (By.CSS_SELECTOR, 'a.result__a')\n  SEARCH_INPUT = (By.ID, 'search_form_input')\n  \n  def __init__(self, browser):\n    self.browser = browser\n\n  def result_link_titles(self):\n    # TODO\n    return []\n  \n  def search_input_value(self):\n    # TODO\n    return \"\"\n\n  def title(self):\n    # TODO\n    return \"\"\n```\n\nThankfully, the search input element on the result page\nis similar to the one on the search page.\n\nThe locator for the result links is a bit trickier, though.\nIt must find all result elements that contain the search phrase in their display texts.\nThis locator will return a list of elements, not just one.\nThe result links are all \"a\" hyperlink elements with a class named \"result__a\".\nWe can use a CSS selector for its query.\n\nWe should always try to use the simplest locator that uniquely finds the target elements.\nIDs, names, and class names are the easiest,\nbut sometimes, we must use CSS selectors and XPaths.\nTo learn more about writing good locators,\ntake the [Web Element Locator Strategies](https://testautomationu.applitools.com/web-element-locator-strategies/) course\nfrom [Test Automation University](https://testautomationu.applitools.com/).\n\nAlthough the test will still fail,\nrerun it using `pipenv run python -m pytest` to make sure our changes did no harm.\nThen, commit your latest code changes.\n\n## Chapter 6: Making WebDriver Calls\n\n*Example Branch: example/6-webdriver-calls*\n\nNow we can implement all the page object methods using WebDriver calls.\nThe [WebDriver API for Python](https://selenium-python.readthedocs.io/api.html)\ndocuments all WebDriver calls.\nIf you aren't sure how to do something, look it up.\nWebDriver can do anything a user can do on a Web page!\n\nLet's start with `DuckDuckGoSearchPage`.\nThe `load` method is a one-line WebDriver call,\nbut it's good practice to make the URL a class variable:\n\n```python\nURL = 'https://www.duckduckgo.com'\n\ndef load(self):\n  self.browser.get(self.URL)\n```\n\nThe `search` method is a bit more complex because it interacts with an element.\nWe need to use a *locator* to find the search input element,\nand then we need to *send keys* to type the search phrase into the element.\n\nFirst, update the `selenium` package imports:\n\n```python\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n```\n\nThe `search` method needs two parts: finding the element and sending the keystrokes.\nThankfully, we already have the locator for the element.\n\n```python\ndef search(self, phrase):\n  search_input = self.browser.find_element(*self.SEARCH_INPUT)\n  search_input.send_keys(phrase + Keys.RETURN)\n```\n\nThe `find_element` method will return the first element found by the locator.\nNotice how the locator uses the `*` operator to expand the SEARCH_INPUT locator tuple into arguments.\nThe `selenium` package offers specific locator type methods (like `find_element_by_name`),\nbut using the generic `find_element` method with argument expansion is better practice.\nIf the locator type must be changed due to Web page updates,\nthen the `find_element` call would not need to be changed.\n\nThe `send_keys` method sends the search phrase passed into the `search` method.\nThis means that the page object can search any phrase!\nThe addition of `Keys.RETURN` will send the ENTER/RETURN key as well,\nwhich will submit the input value to perform the search and load the results page.\n\nThe full code for `pages/search.py` should look like this:\n\n```python\n\"\"\"\nThis module contains DuckDuckGoSearchPage,\nthe page object for the DuckDuckGo search page.\n\"\"\"\n\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\n\n\nclass DuckDuckGoSearchPage:\n\n  # URL\n\n  URL = 'https://www.duckduckgo.com'\n\n  # Locators\n\n  SEARCH_INPUT = (By.ID, 'search_form_input_homepage')\n\n  # Initializer\n\n  def __init__(self, browser):\n    self.browser = browser\n\n  # Interaction Methods\n\n  def load(self):\n    self.browser.get(self.URL)\n\n  def search(self, phrase):\n    search_input = self.browser.find_element(*self.SEARCH_INPUT)\n    search_input.send_keys(phrase + Keys.RETURN)\n```\n\nNow, let's do `DuckDuckGoResultPage`.\nThe `title` method is the easiest one because it just returns a property value:\n\n```python\ndef title(self):\n  return self.browser.title\n```\n\nThe `search_input_value` method is similar to the `search` method from `DuckDuckGoSearchPage`,\nbut instead of sending a command, it asks for state from the page.\nThe \"value\" attribute contains the text a user types into an \"input\" element.\n\n```python\ndef search_input_value(self):\n  search_input = self.browser.find_element(*self.SEARCH_INPUT)\n  value = search_input.get_attribute('value')\n  return value\n```\n\nThe `result_link_titles` method is a bit more complex.\nThe test must verify that the result page displays links relating to the search phrase.\nThis method should find all result links on the page.\nThen, it should get the titles for those result links.\nRemember, the test asserts that the search phrase is in each title.\nThis assertion may seem too stringent because it could fail the test for possibly valid links,\nbut it should be good enough for simple search terms.\n(Again, remember, the test is merely a basic search test.)\n\nThe `result_link_titles` method should look like this:\n\n```python\ndef result_link_titles(self):\n  links = self.browser.find_elements(*self.RESULT_LINKS)\n  titles = [link.text for link in links]\n  return titles\n```\n\nNotice that it uses `find_elements` (plural) to get a list of matching elements.\n\nThe full code for `pages/result.py` should look like this:\n\n```python\n\"\"\"\nThis module contains DuckDuckGoResultPage,\nthe page object for the DuckDuckGo search result page.\n\"\"\"\n\nfrom selenium.webdriver.common.by import By\n\n\nclass DuckDuckGoResultPage:\n  \n  # Locators\n\n  RESULT_LINKS = (By.CSS_SELECTOR, 'a.result__a')\n  SEARCH_INPUT = (By.ID, 'search_form_input')\n\n  # Initializer\n\n  def __init__(self, browser):\n    self.browser = browser\n\n  # Interaction Methods\n\n  def result_link_titles(self):\n    links = self.browser.find_elements(*self.RESULT_LINKS)\n    titles = [link.text for link in links]\n    return titles\n  \n  def search_input_value(self):\n    search_input = self.browser.find_element(*self.SEARCH_INPUT)\n    value = search_input.get_attribute('value')\n    return value\n\n  def title(self):\n    return self.browser.title\n```\n\nFinally, remove the \"incomplete\" exception from `tests/test_search.py`.\nThat module's code should look like this:\n\n```python\n\"\"\"\nThese tests cover DuckDuckGo searches.\n\"\"\"\n\nfrom pages.result import DuckDuckGoResultPage\nfrom pages.search import DuckDuckGoSearchPage\n\n\ndef test_basic_duckduckgo_search(browser):\n  search_page = DuckDuckGoSearchPage(browser)\n  result_page = DuckDuckGoResultPage(browser)\n  PHRASE = \"panda\"\n\n  # Given the DuckDuckGo home page is displayed\n  search_page.load()\n\n  # When the user searches for \"panda\"\n  search_page.search(PHRASE)\n\n  # Then the search result title contains \"panda\"\n  assert PHRASE in result_page.title()\n  \n  # And the search result query is \"panda\"\n  assert PHRASE == result_page.search_input_value()\n  \n  # And the search result links pertain to \"panda\"\n  for title in result_page.result_link_titles():\n    assert PHRASE.lower() in title.lower()\n```\n\nRerun the test using `pipenv run python -m pytest`.\nNow, finally, it should run to completion and pass!\nThe test will take a few second to run because it must wait for page loads.\nChrome should pop up and automatically go through all test steps.\nTry not to interfere with the browser as the test runs.\nMake sure pytest doesn't report any failures when it completes.\n\n## Chapter 7: Configuring Multiple Browsers\n\n*Example Branch: example/7-browser-config*\n\nOur test currently runs on Chrome,\nbut it should be able to run on other browsers, too.\nAny Web UI test should be configurable to run on any applicable browser.\nLet's run it on Headless Chrome and Firefox!\n\nBrowser choice is an aspect of testing.\nIn theory, every test should run on every supported browser.\nThus, browser choice should be treated as an input for test automation.\nIt should not be hard-coded into automation code.\nIt should also not be written as pytest parameters.\nOne test session should use one browser.\nIf another browser needs to be tested, then launch another test session.\nThis design keeps test code and test executions simpler.\n\nCreate a new file named `config.json` in the project's root directory.\nJSON files are very easy to use in Python.\nThe `json` module is part of the standard library,\nand JSON files can be parsed into dictionaries with one line.\nAdd the following lines:\n\n```json\n{\n  \"browser\": \"Chrome\",\n  \"implicit_wait\": 10\n}\n```\n\nNotice how these inputs correspond to values in the `browser` fixture.\nThen, add a new fixture to `tests/conftest.py`:\n\n```python\nimport json\n\n@pytest.fixture\ndef config(scope='session'):\n\n  # Read the file\n  with open('config.json') as config_file:\n    config = json.load(config_file)\n  \n  # Assert values are acceptable\n  assert config['browser'] in ['Firefox', 'Chrome', 'Headless Chrome']\n  assert isinstance(config['implicit_wait'], int)\n  assert config['implicit_wait'] \u003e 0\n\n  # Return config so it can be used\n  return config\n```\n\nThis fixture reads the `config.json` file.\nIt also validates the inputs so that tests won't run if the inputs are bad.\nThe fixture's *scope* is set to \"session\" so that the fixture is called only one time for all tests.\nThere is no need to read it repeatedly for every test.\n\nUpdate the `browser` fixture to use these inputs:\n\n```python\n@pytest.fixture\ndef browser(config):\n\n  # Initialize the WebDriver instance\n  if config['browser'] == 'Firefox':\n    b = selenium.webdriver.Firefox()\n  elif config['browser'] == 'Chrome':\n    b = selenium.webdriver.Chrome()\n  elif config['browser'] == 'Headless Chrome':\n    opts = selenium.webdriver.ChromeOptions()\n    opts.add_argument('headless')\n    b = selenium.webdriver.Chrome(options=opts)\n  else:\n    raise Exception(f'Browser \"{config[\"browser\"]}\" is not supported')\n\n  # Make its calls wait for elements to appear\n  b.implicitly_wait(config['implicit_wait'])\n\n  # Return the WebDriver instance for the setup\n  yield b\n\n  # Quit the WebDriver instance for the cleanup\n  b.quit()\n```\n\nFixtures can call fixtures.\nHere, `browser` calls `config` and then uses its parts to set the browser and implicit wait time.\nNotice that Headless Chrome just uses the Chrome WebDriver with extra arguments.\n\nNothing else needs to be updated in order to change the browser.\nRun the test using `pipenv run python -m pytest` with Chrome to verify no harm was done.\nYou should see the test run successfully.\n\nThen, change the config's *browser* to \"Headless Chrome\" and rerun the test.\nYou won't see the browser window appear, but the test should still pass.\nWhy? \"Headless\" mode won't render pages visibly.\nIt's great for automated testing because it's slightly more efficient than \"regular\" Chrome.\n\nFinally, try \"Firefox\". Does it work? Warning: it may or may not! Oh no!\nDon't panic if it doesn't work. We'll fix it in the next part.\n\n## Chapter 8: Handling Race Conditions\n\n*Example Branch: example/8-race-conditions*\n\nWhen running the search test using Firefox, you might hit the following failure:\n\n```\n      # Then the search result title contains \"panda\"\n\u003e     assert PHRASE in result_page.title()\nE     AssertionError: assert 'panda' in 'DuckDuckGo — Privacy, simplified.'\n```\n\nOr, the test might pass.\nWhy would the test fail on Firefox if it passed for Chrome?\nAnd why is there a chance that it *might* fail or *might* pass?\nLet's revisit the test case steps:\n\n```gherkin\nScenario: Basic DuckDuckGo Search\n    Given the DuckDuckGo home page is displayed\n    When the user searches for \"panda\"\n    Then the search result title contains \"panda\"\n    And the search result query is \"panda\"\n    And the search result links pertain to \"panda\"\n```\n\nStep 2 performs the search.\nThen, step 3 checks the title of the page.\nUnfortunately, step 3 has a *race condition*.\nRemember, the browser and the automation are two separate processes.\nWhen the automation triggers the search, the browser will load the new page and title.\nAt the same time, the automation will continue to execute the test.\nIf the automation executes the assertion *before* the new page title loads,\nthen the assertion will fail.\nChrome was fast enough to avoid the race condition,\nbut Firefox was slow enough to trigger it.\n\nRace conditions are the bane of Web UI testing.\nThey can be difficult to predict when writing tests.\nThey can also be difficult to identify in test results\nbecause they typically happen *intermittently*.\nWeb UI tests gain a bad reputation for being \"flaky\"\nwhenever race conditions are not handled appropriately.\n\nAutomation must always wait for page components to be ready before interacting with them.\n[Implicit waits](https://selenium-python.readthedocs.io/waits.html#implicit-waits)\nwork well for Web elements,\nbut they don't work for browser attributes like page title.\nThey are best for small projects.\n[Explicit waits](https://selenium-python.readthedocs.io/waits.html#explicit-waits)\nare more customizable, but they require more code.\nThey are typically the better option for large projects that need different times and conditions.\nAs a best practice, automation should use only one type of waiting.\nMixing implicit and explicit waits can have unexpected consequences.\n\nThankfully, there's a shortcut we can use to fix `test_basic_duckduckgo_search`.\nThe other two assertions use implicit waits for other elements on the page.\nBy the time those elements are loaded, the title would also be loaded.\nTherefore, we can move step 3 to the end of the scenario to be the last thing we check.\n\nThe updated code for `tests/test_search.py` should be:\n\n```python\n\"\"\"\nThese tests cover DuckDuckGo searches.\n\"\"\"\n\nfrom pages.result import DuckDuckGoResultPage\nfrom pages.search import DuckDuckGoSearchPage\n\n\ndef test_basic_duckduckgo_search(browser):\n  search_page = DuckDuckGoSearchPage(browser)\n  result_page = DuckDuckGoResultPage(browser)\n  PHRASE = \"panda\"\n  \n  # Given the DuckDuckGo home page is displayed\n  search_page.load()\n\n  # When the user searches for \"panda\"\n  search_page.search(PHRASE)\n\n  # Then the search result query is \"panda\"\n  assert PHRASE == result_page.search_input_value()\n  \n  # And the search result links pertain to \"panda\"\n  for title in result_page.result_link_titles():\n    assert PHRASE.lower() in title.lower()\n\n  # And the search result title contains \"panda\"\n  # (Putting this assertion last guarantees that the page title will be ready)\n  assert PHRASE in result_page.title()\n```\n\nRerun the test using `pipenv run python -m pytest` with Firefox to verify the fix.\nThen, rerun it again with Chrome and Headless Chrome to make sure those browsers still work.\n\nAlways watch out for race conditions,\nalways wait for things to be ready before interacting with them,\nand always run tests multiple times across multiple configurations to identify problems.\n\n## Chapter 9: Running Tests in Parallel\n\n*Example Branch: example/9-parallel-testing*\n\nUnfortunately, Web UI tests are very slow compared to unit tests and service API tests.\nThe best way to speed them up is to run them in parallel.\n\nFirst, let's parametrize `test_basic_duckduckgo_search` so that we have more than one test to run.\nAny pytest test or fixture may be [parametrized](https://docs.pytest.org/en/latest/parametrize.html).\nUpdate the code in `tests/test_search.py` to be:\n\n```python\n\"\"\"\nThese tests cover DuckDuckGo searches.\n\"\"\"\n\nimport pytest\n\nfrom pages.result import DuckDuckGoResultPage\nfrom pages.search import DuckDuckGoSearchPage\n\n\n@pytest.mark.parametrize('phrase', ['panda', 'python', 'polar bear'])\ndef test_basic_duckduckgo_search(browser, phrase):\n  search_page = DuckDuckGoSearchPage(browser)\n  result_page = DuckDuckGoResultPage(browser)\n  \n  # Given the DuckDuckGo home page is displayed\n  search_page.load()\n\n  # When the user searches for the phrase\n  search_page.search(phrase)\n\n  # Then the search result query is the phrase\n  assert phrase == result_page.search_input_value()\n  \n  # And the search result links pertain to the phrase\n  for title in result_page.result_link_titles():\n    assert phrase.lower() in title.lower()\n\n  # And the search result title contains the phrase\n  # (Putting this assertion last guarantees that the page title will be ready)\n  assert phrase in result_page.title()\n```\n\nThe test will now run three times with different search phrases.\nRerun the tests to make sure they all work.\nYou will notice that they run one at a time.\n\nNext, install [pytest-xdist](https://docs.pytest.org/en/3.0.1/xdist.html),\nthe pytest plugin for parallel testing:\n\n```bash\n$ pipenv install pytest-xdist\n```\n\nFinally, run the tests using the following command:\n\n```bash\n$ pipenv run python -m pytest -n 3\n```\n\nThe \"-n 3\" arguments tells pytest to run 3 tests in parallel.\nWe have 3 example tests, and most machines can handle 3 Web UI tests simultaneously.\nWhen the tests run, notice how 3 browser instances open at once - one per test.\n\nRun the tests a few times using Chrome and Firefox.\nLook to see how long the tests typically take per browser.\nAlso, look to see if any intermittent failures happen.\nThen, try using Headless Chrome.\nMost likely, Headless Chrome will be significantly faster and more reliable\nthat regular Chrome and Firefox.\n\nAs a warning, parallel testing can be dangerous.\nMake sure that tests avoid *collisions*.\nCollisions happen when tests simultaneously access shared state.\nFor example, one test could try to access a database record while another test deletes it.\nThankfully, our DuckDuckGo search tests do not have any collisions\nbecause they make independent searches in separate browser instances.\n\nWhenever running tests in parallel,\ncarefully tune the number of threads to minimize the total test execution time.\nMore threads does *not* necessarily mean faster testing.\nToo many parallel tests will choke system resources.\n\nAnecdotally, for Web UI tests:\n\n* 1 test per processor minimizes total execution time without slowing down individual tests\n* 2 tests per processor minimizes total execution time further but slows down individual tests\n* more than 2 tests per processor does not meaningfully shrink total execution time further\n* memory size does not have a significant impact on total execution time\n\nOne machine can scale up only so far.\nFor massive parallel testing, try using\n[Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2).\nAlternatively, many companies provide cloud-based solutions for parallel WebDriver testing.\nCheck the *Resources* section below for a list.\n\nTo learn more about parallel testing in general, read\n[To Infinity and Beyond: A Guide to Parallel Testing](https://automationpanda.com/2018/01/21/to-infinity-and-beyond-a-guide-to-parallel-testing/).\n\nCongrats! You have completed the guided part of this course!\n\n## Independent Exercises\n\nThe guided course covered one very basic search test, but DuckDuckGo has many more features.\nTry to write some new tests for DuckDuckGo independently.\nHere are some suggestions:\n\n* search for different phrases\n* search by clicking the button instead of typing RETURN\n* click a search result\n* expand \"More Results\" at the bottom of the result page\n* verify auto-complete suggestions pertain to the search text\n* search by selecting an auto-complete suggestion\n* search a new phrase from the results page\n* do an image search\n* do a video search\n* do a news search\n* change settings\n* change region\n\nThese tests will require new page objects, locators, and interaction methods.\nSee how many tests you can automate on your own!\nIf you get stuck, ask for help.\n\n## Additional Resources\n\nThis Test Automation University course is based on several other tutorials by Andrew Knight:\n\n* DjangoCon 2019: [Hands-On Web UI Testing](https://github.com/AndyLPK247/djangocon-2019-web-ui-testing)\n* PyOhio 2019: [Hands-On Web UI Testing](https://github.com/AndyLPK247/pyohio-2019-web-ui-testing)\n* TestProject: [Web Testing Made Easy with Python, Pytest and Selenium WebDriver](https://blog.testproject.io/2019/07/09/open-source-test-automation-python-pytest-selenium-webdriver/)\n\nRelated TAU courses:\n\n* [Web Element Locator Strategies](https://testautomationu.applitools.com/web-element-locator-strategies/) shows how to write good locators and use Chrome DevTools.\n* [Behavior-Driven Python with pytest-bdd](https://testautomationu.applitools.com/behavior-driven-python-with-pytest-bdd/) shows how to use `pytest-bdd` to write BDD-style tests.\n* [Setting a Foundation for Successful Test Automation](https://testautomationu.applitools.com/setting-a-foundation-for-successful-test-automation/) shows how to run a testing project the right way.\n\nOther helpful links:\n\n* [AutomationPanda.com](https://automationpanda.com/)\n  * [Python](https://automationpanda.com/python/)\n  * [Testing](https://automationpanda.com/testing/)\n  * [Why Python is Great for Test Automation](https://automationpanda.com/2018/07/26/why-python-is-great-for-test-automation/)\n  * [Web Element Locators for Test Automation](https://automationpanda.com/2019/01/15/web-element-locators-for-test-automation/)\n  * [The Testing Pyramid](https://automationpanda.com/2018/08/01/the-testing-pyramid/)\n  * [To Infinity and Beyond: A Guide to Parallel Testing](https://automationpanda.com/2018/01/21/to-infinity-and-beyond-a-guide-to-parallel-testing/)\n* [Selenium with Python](https://selenium-python.readthedocs.io/)\n  * [WebDriver API](https://selenium-python.readthedocs.io/api.html)\n  * [Waits](https://selenium-python.readthedocs.io/waits.html)\n  * [Locating Elements](https://selenium-python.readthedocs.io/locating-elements.html)\n* [pytest.org](https://docs.pytest.org/)\n* [Selenium Grid wiki](https://github.com/SeleniumHQ/selenium/wiki/Grid2)\n\n## About the Author\n\nThis course was written and delivered by **Andrew Knight** (aka *Pandy*), the \"Automation Panda\".\nAndy is a Pythonista who specializes in testing and automation.\n\n* Twitter: [@AutomationPanda](https://twitter.com/AutomationPanda)\n* Blog: [AutomationPanda.com](https://automationpanda.com/)\n* LinkedIn: [andrew-leland-knight](https://www.linkedin.com/in/andrew-leland-knight/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fautomationpanda%2Ftau-intro-selenium-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fautomationpanda%2Ftau-intro-selenium-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fautomationpanda%2Ftau-intro-selenium-py/lists"}