How to properly handle lots of (format) constants in Python?

Viewed 29

I am setting up an end-to-end testing framework with Selenium in Python. In order to click on DOM elements I have to define a lot of locators (like XPaths, CSS-selectors etc.) which are mostly constant.

However, there are some constants that vary in each run, so some content needs to be formatted in at runtime.

At the moment I have a basic locators.py file which organizes all of the constants in a view-corresponding way like this:

class Locators:

    class LoginPage:
        USERNAME_INPUT = "//input[@id='username']"
        PASSWORD_INPUT = "//input[@id='password']"
        LOGIN_BUTTON = "//button[text()='Login']"
        # ...
    
    class Homepage:
        ACTION_BUTTON = lambda t: f"//buton[text()='{t}']"
        # ...

# ...

This can then be used like this:

from locators import Locators

browser.click(Locators.LoginPage.USERNAME_INPUT)
browser.click(Locators.Homepage.ACTION_BUTTON(var))

The last example here (ACTION_BUTTON) is of most interest to me. Since e.g. PyCharm displays a PEP 8 warning that using lambdas is discouraged I wonder whether there is a more elegant way of handling such "formattable" constants?

Furthermore, I would like to be able to generalize e.g. all buttons to the outer class like this:

class Locators:

    _BUTTON = lambda s: f"//button[text()='{s}']"

    class LoginPage:
        USERNAME_INPUT = "//input[@id='username']"
        PASSWORD_INPUT = "//input[@id='password']"
        LOGIN_BUTTON = _BUTTON("Login")

    class Homepage:
        ACTION_BUTTON = _BUTTON

But this gives a syntax error.

2 Answers

I guess this is up to personal style.

Personally I would use regular format strings for this use case.

class Locators:

    _BUTTON = "//button[text()='{action}']"

    class LoginPage:
        USERNAME_INPUT = "//input[@id='username']"
        PASSWORD_INPUT = "//input[@id='password']"
        LOGIN_BUTTON = _BUTTON.format(action="Login")

    class Homepage:
        ACTION_BUTTON = _BUTTON

browser.click(Locators.LoginPage.USERNAME_INPUT)
browser.click(Locators.Homepage.ACTION_BUTTON.format(action=var))

I don't see any issues with putting your helper functions outside the Locators namespace. Afterall, they are NOT locators until they are executed.

_BUTTON = lambda s: f"//button[text()='{s}']"

class Locators:

    class LoginPage:
        USERNAME_INPUT = "//input[@id='username']"
        PASSWORD_INPUT = "//input[@id='password']"
        LOGIN_BUTTON = _BUTTON("Login")

That being said I ran into a very similar problem. I solved this by creating a separate namespace for the variables that would need to be referenced in the locator strings. This allowed me to run a some setup logic to determine what the variables should be prior to loading the module.

# sys_params.py
class SystemParameters(object):
    homepage_arg1 = "default"

params = SystemParameters()
params.homepage_arg1 = "something"

# locators.py
from sys_params import params

class Locators:
    class HomePage:
        ACTION_BUTTON = f'//button[text()="{params.homepage_arg1}"]'

If you need the module to load a little more dynamically (like I did), then you're faced with having to scope strings inside of functions.

# locators.py
from sys_params import params

class Locators:
    class HomePage:
        @classmethod
        def ACTION_BUTTON(cls): return f'//button[text()="{params.homepage_arg1}"]'

## which gets used slightly differently
driver.click(Locators.HomePage.ACTION_BUTTON())
Related