Tutorials / Selenium WebDriver series

Part 4: Writing the Selenium tests

With the page objects in place, the tests are short. Test Junkie's parameters turn two test methods into 33 navigation checks, and its threads run them side by side.

Navigation tests

Every page has the same header and footer, so we check every link from every page. The suite parameters are the page objects to start from, the test parameters the links to click:

tests/suites/NavigationSuite.pypython
from test_junkie.decorators import Suite, afterTest, test

from src.pages.AboutPage import AboutPage
from src.pages.Browser import Browser
from src.pages.DocumentationPage import DocumentationPage
from src.pages.HomePage import HomePage


def validate_page(page):
    # expected vs. actual URL and title, from the page object's own definition
    expected_url, actual_url = page.expected_url, page.get_actual_url()
    assert actual_url.startswith(expected_url), "Expected URL: {} Actual URL: {}".format(expected_url, actual_url)
    expected_title, actual_title = page.expected_title, page.get_actual_title()
    assert expected_title in actual_title, "Expected title: {} Actual title: {}".format(expected_title, actual_title)


@Suite(parameters=[HomePage, DocumentationPage, AboutPage], feature="Navigation")
class NavigationSuite:

    @afterTest()
    def close_browser(self):
        Browser.shutdown()  # a fresh browser for every test

    @test(parameters=["logo", "home", "get_started", "tutorials", "documentation", "about"],
          parallelized_parameters=True, component="Header")
    def header_navigation(self, suite_parameter, parameter):
        page = suite_parameter().open()
        destination = getattr(page.header, "click_{}".format(parameter))()
        validate_page(destination)

    @test(parameters=["home", "get_started", "tutorials", "documentation", "about"],
          parallelized_parameters=True, component="Footer")
    def footer_navigation(self, suite_parameter, parameter):
        page = suite_parameter().open()
        destination = getattr(page.footer, "click_{}".format(parameter))()
        validate_page(destination)
  • 3 pages × 6 header links + 3 pages × 5 footer links = 33 checks from two methods.
  • parallelized_parameters=True runs a test's link checks at the same time, each in its own thread, and so with its own browser (part 1).
  • @afterTest closes the browser after every check, so each one starts clean.
  • feature and component label the tests, so tj run -c Header runs just the header checks.

Documentation link tests

The documentation page is long and full of links to its own sections. One test clicks every in-page link in the content, another every link in the menu:

tests/suites/DocumentationSuite.pypython
from test_junkie.decorators import Suite, afterTest, test

from src.pages.Browser import Browser
from src.pages.DocumentationPage import DocumentationPage
from src.pages.components.PageHeader import PageHeader


@Suite(feature="Documentation")
class DocumentationSuite:

    @afterTest()
    def close_browser(self):
        Browser.shutdown()  # a fresh browser for every test

    @staticmethod
    def __hide_header():
        # a sticky header can sit on top of the link you click ("element click intercepted")
        Browser.get_driver().execute_script("arguments[0].style.display = 'none'",
                                            PageHeader.NAV_BAR.get_element())

    @test(component="Anchor links")
    def content_anchor_links(self):
        broken = []
        page = DocumentationPage().open()
        DocumentationSuite.__hide_header()
        for section, links in page.get_content_links_per_section().items():
            for link in links:  # UiObjects
                if not link.get_element().is_displayed():
                    continue  # e.g. inside a collapsed block
                href = link.get_attribute("href")
                link.click()
                if page.get_actual_url() != href:
                    broken.append({section: href})
        # collect every broken link, then assert once - the failure lists all of them, not just the first
        assert not broken, "Anchor links that don't work: {}".format(broken)

    @test(component="Left menu")
    def left_menu_links(self):
        broken = []
        page = DocumentationPage().open()
        DocumentationSuite.__hide_header()
        for link in page.get_left_nav_links():
            href = link.get_attribute("href")  # read it before the click
            link.click()
            if page.get_actual_url() != href:
                broken.append(href)
        assert not broken, "Menu links that don't work: {}".format(broken)

Both collect the broken links and assert once at the end, so a failure lists every broken link instead of stopping at the first. The sticky navigation bar is hidden first: it can sit on top of the link being clicked and make Selenium raise element click intercepted.

Run them

Save where the tests are and how many threads to use in a tj.cfg at the project root, then run tj run from the project folder. Test Junkie puts the project folder on the import path itself, so from src.pages... imports work without setting PYTHONPATH:

terminalbash
tj config update -s tests/suites -T 6 -S 2 --config ./tj.cfg
tj run
tj runtext
Test Junkie 0.9a6 · Python 3.12.5 · Linux
  tests    tests/suites · 2 suites, 35 tests · found in 0.35s
  mode     parallel · 2 suite threads · 6 test threads
  config   /home/you/selenium_demo/tj.cfg
           sources tests/suites · suite_multithreading_limit 2 · test_multithreading_limit 6

DocumentationSuite  tests/suites/DocumentationSuite.py  [||||||||||||||||||||||||]    2/2  84.61s
NavigationSuite     tests/suites/NavigationSuite.py     [||||||||||||||||||||||||]  33/33  87.05s

Total                                                   [||||||||||||||||||||||||]  35/35  100%  87.0s

Summary ────────────────────────────────────────────────────────────────────────

  Suite                 Pass  Fail  Error  Ignore  Skip  Cancel     Time
  DocumentationSuite       2     ·      ·       ·     ·       ·   84.61s
  NavigationSuite         33     ·      ·       ·     ·       ·   87.05s
  ──────────────────────────────────────────────────────────────────────
  Total                   35     ·      ·       ·     ·       ·   87.05s
  35 tests              100%

────────────────────────────────────────────────────────────────────────────────

[PASSED]  exit code 0

Up to 2 suites and 6 tests run at once. Every browser is a real Chrome, so set the thread limits to what the machine can take; tj run -m charts CPU and memory to help you choose. The same run from Python is Runner([DocumentationSuite, NavigationSuite]).run(suite_multithreading_limit=2, test_multithreading_limit=6).