Feedback on my pytest / Selenium project

Hello,

I hope the community here could give me some feedback on on my first practice project using pytest and Selenium. The project contains a separate file for the pytest fixture and then two separate tests.

Some notes on my decisions:

I chose not to use page objects mostly because I prioritize visibility, and I prefer that I all code for a test be neatly visible in one file.

My locators are maybe odd. I tried xpath relative paths but I must have done something wrong, so I settled for absolute paths which works just fine. But this kind of locator is far from readable of course. I have more to learn about locators.

I have learned that flakiness is a thing. I have read about flaky tests from tutorials and blogs but now I know what it means. This test fails on my machine every now and then. (Timeouts when another browser is open or element not interactable) It would be easier to deal with a consistent fail rather than one that appears occasionally. I don’t expect anyone to run my test files, but if there is anything in the code that stands out as a likely culprit please let me know.

One line of code is identical in both tests. A constant:

URL = “Home - Oodi

On one hand I understand repeated code is a code smell, on the other hand the two tests are isolated from one another. Let me know if there is a better way to declare this constant.

All in all I enjoy thinking trough a test case and see Selenium execute it with my own eyes. Working with Selenium seems to be a lot about making deliberate and thoughtful decisions on what to test and why.

Anyhow. If anyone spots a particularly rankling code smell let me know. All feedback is greatly appreciated.

Code below:

conftest.py

import pytest
from selenium.webdriver import Chrome


\# Initialize and quit handled by a pytest fixture
@pytest.fixture
*def* browser(*scope*="module"):
  driver = Chrome()
  
  driver.implicitly_wait(15)
  yield driver
  driver.quit()


test_oodi_FAQS.py

from selenium.webdriver.common.by import By
from selenium.common.exceptions import ElementClickInterceptedException

from selenium.common.exceptions import NoSuchElementException

*def* test_oodi_faq_interaction(*browser*):
  \# GIVEN The Oodi homepage is displayed
  URL = "https://www.oodihelsinki.fi/en/"

  browser.get(URL)
  browser.implicitly_wait(20)

  \# WHEN the user clicks on FAQ

  faq_search_window = browser.find_element(By.LINK_TEXT, "Questions and answers")

  browser.implicitly_wait(20)

  while True:
​    try:
​      faq_search_window.click()
​      break
​    except ElementClickInterceptedException:
​      browser.execute_script("window.scrollBy(0,-100);")

  \# THEN the FAQ page is displayed

  assert browser.current_url == "https://www.oodihelsinki.fi/en/faq/"

  browser.implicitly_wait(20)

  \# AND the fact about dogs in the library is displayed

  xpath = "/html/body/div[1]/div[1]/div/div/section[2]/div[16]/div/div[1]/h3"

  *def* element_present_check():
​    try:
​      browser.find_element(By.XPATH, xpath)
​      return True
​    except NoSuchElementException:
​      return False

  assert element_present_check() is True

test_oodi_facilities.py

from selenium.webdriver.common.by import By

\# from selenium.common.exceptions import ElementClickInterceptedException

*def* test_oodi_facilities_interaction(*browser*):
  \# Given The Oodi homepage is displayed
  URL = "https://www.oodihelsinki.fi/en/"

  browser.get(URL)
  browser.implicitly_wait(20)

  \# WHEN the user clicks the plus buttton to expand 'Services and facilities'
  \# AND the user clicks on 'facilities'

  plus_button_xpath = ("/html/body/section/div[1]/div[1]/nav/ul/li[2]/button/div/div[2]")

  facilities_xpath = "/html/body/section/div[1]/div[1]/nav/ul/li[2]/ul/li/ul/li[2]/a"

  browser.implicitly_wait(20)
  plus_button = browser.find_element(By.XPATH, plus_button_xpath)
  plus_button.click()
  browser.implicitly_wait(20)
  facilities = browser.find_element(By.XPATH, facilities_xpath)
  facilities.click()
  browser.implicitly_wait(20)

  \# THEN the user is on the facilities page
  assert (browser.current_url == "https://www.oodihelsinki.fi/en/services-and-facilities/facilities/")
1 Like

Hello!

This is a great attempt. I am by no means an expert and am still learning as I go myself, but I have spotted a couple of things I would like to question.

browser.implicitly_wait(seconds)
What exactly are you waiting for?
Are you waiting for an element to appear on the screen before the script attempts to click? If so, you can use wait untils. The main reason for this is performance. Your script will be waiting an arbitrary number of seconds which a) may be too long or b) not long enough so it is good practise to try not to rely on implicit waits. I would personally fail a code review if a person favoured waiting a set number of seconds instead of for an element to load without a justification as to why - just a comment in the code is enough.

xpaths
You have commented that you have had to use absolute paths. This has happened to me before and I created my own HTML tags purely for automation. When I inspected the HTML of a page then it would have ‘selenium=“submit_button”’ for example. Appreciate you may not be able to do that in this scenario, but just thought I would tell you how I worked around it in work.

Let me know if you have any questions :slightly_smiling_face:

2 Likes

Hello @froberts and thanks a lot for your feedback on my script.

The implicit waits in my scripts are indeed there because I am waiting for elements to load (I haven’t yet practiced Selenium explicit waits at all).

I have a fairly slow internet connection at present and my decision on using long implicit waits had to do with me noticing how the page loads loaded quite slowly combined with my test failing occasionally.

Often it was a case of me noticing a test failed, next I added an implicit wait and the next time the test ran successfully. But with this approach I am not really concluding what fails and why, so your suggestion about being clear about why I use implicit waits is spot on. I should also look into if explicit waits are preferable.

As for the xpath: Your solution where you add custom html attributes is very interesting and I may use that if I am testing a page where I have access to edit the html code. I imagine that as a last resort when there is no id or a relative xpath that can be used. I found a course on test automation university that deals specifically about html locators. I think I will dig deeper into that course, and also learn more about waits, before my next practice project.

1 Like

Sorry, have put off coming to this code review session. I’m 2 years late on my input, but to be honest I’ve only recently started using selenium myself. The biggest problem I have is testing framework components like selenium and pytest, is that they are like hammers and rusty nails, when sometimes you really need screwdrivers and even brass screws after a while.

  • A super handy python module called WebdriverManager is a must, it resolves most of your cross-browser and cross-platform bootstrapping pain. I like webdrivermanager so much, I have a local hack I made to it to let WebdriverManager download any SeleniumGrid jar updates as well as webdriver updates for me.
  • As for PyTest, it’s sometimes necessary when you want to control remote browsers in a grid, to abstract the webdriver with a wrapper, and that can create code sharing pain if you have opted for Tests that do not follow the PyTest workflow. So I’ve basically gone back to an XUnit style for fixtures because we wanted more control of remotely executing code. Basically be aware that your needs can change.
  • implicit waits, as @froberts pointed out already, are evil. Use webdriver waits and expected-conditions
  • All test code transparent, and all in one file - that is a nice idea. And I am a big fan of reducing abstraction dirt. Decide how you can design for that. In the codebase I manage test cases are impossible to write without calling other classes to help the tester do things like translate strings, handle a specific browser incompatibility, handle a remote service and so on. We created a thing called “helpers” which are basically abstraction classes that tests can use. We write helpers and put them into a separate module space and have separate set of unit tests for them. So all that scaffolding code you need in a test does at least get some kind of unit-test like treatment.
  • XPATH, to be honest I’ve not managed to remove it entirely either. One of the teams uses CSS class ID’s (guaranteed unique) and another uses regular element ID’s. It’s super helpful to wrap your find_element() helper function so that it “raises” to let you know whenever a XPATH returns more than one element. I still have much to learn about locators though, also, check out Appium. Appium adds a few locator helpers, and although I’ve not tried them yet has moved into the desktop browser space.
  • Page objects, I’m a fan of these. I wrote a blog post all about it a while back when I designed my own ones. I ended up adding a decorator or two before they worked well for us, and also stole some ideas from the selenium library, but I abandoned it entirely and wrote my own because it’s far too simplistic an implementation that actually hampered code re-use. In my blog I link a blog I found on the wayback machine that pre-dated the selenium implementation and was my biggest clue.

As someone who has “seleniumed” for almost a year now, I’m keen to know what kind of changes you did make though and how far you have journeyed in 2 years @rydberg ?