jQuery-style syntactic sugar for highly reliable automated browser testing in Python
- Chainable methods with obvious names
- Easy to read
- Concise to write
- Built-in fault tolerance
For an introduction to why you'd want to use Elementium, take a look at the following post.
Before & After
# From http://selenium-python.readthedocs.org/en/latest/getting-started.html from selenium import webdriver from selenium.webdriver.common.keys import Keys driver = webdriver.Firefox() driver.get("http://www.python.org") assert "Python" in driver.title elem = driver.find_element_by_name("q") elem.send_keys("selenium") elem.send_keys(Keys.RETURN) driver.close()
from selenium import webdriver from selenium.webdriver.common.keys import Keys from elementium.drivers.se import SeElements se = SeElements(webdriver.Firefox()) se.navigate("http://www.python.org").insist(lambda e: "Python" in e.title) se.find("q").write("selenium" + Keys.RETURN)
The easy way
pip install elementium
The developer way
git clone firstname.lastname@example.org:actmd/elementium.git cd elementium python setup.py install
Elementium has been tested for Python 2.6, 3.4, 3.5, 3.7, and pypy using Travis CI
Elementium includes by default a wrapper for the Selenium Python Bindings. As such, all of the usage examples make use of the Selenium driver.
Wrap the browser with an Elementium object
from selenium import webdriver from elementium.drivers.se import SeElements se = SeElements(webdriver.Firefox())
Navigating to a web page
Finding DOM elements
Elementium simplifies most of Selenium's many
# Find by ID se.find("#foo") # Find by name se.find("[name='foo']") # Find by tag name se.find("input") # Find by class name se.find(".cssClass") # Find by link text se.find_link("Click me") # Find by partial link text se.find_link("Click", exact=False)
find_link() will also return multiple elements, if present, so you can forget about all the additional
find_elements_... methods, too:
<div>...</div> <div>...</div> <div>...</div>
len(se.find("div")) # == 3
Under the hood,
find() returns a new
SeElements object containing a list of all of the items that matched. (These individual items are also of type
Getting specific items
get method lets you pull out a specific item in a chainable manner.
<button>Foo</button> <button>Bar</button> <button>Baz</button>
# Get the second button se.find("button").get(1)
Accessing the raw object
If you would rather get the raw object (e.g.
SeleniumWebElement) that is returned by the underlying driver, use
# Find elements on a page for a given class buttons = se.find("button") for button in buttons.items: print type(button)
item alias will return the first raw item:
<input value="blerg" />
se.find("input").value() # returns 'blerg'
<input type="checkbox" value="check1"> <input type="checkbox" value="check2"> <input type="checkbox" value="check3">
# Click all three checkboxes se.find("input[type='checkbox']").click()
<input type="text" />
se.find("input").write("If not now, when?")
<select> <option value="cb">Corned Beef</option> <option value="ps">Pastrami</option> </select>
# Select by visible text se.find("select").select(text="Corned Beef") # Select by value se.find("select").select(value="cb") # Select by index se.find("select").select(i=0)
If manipulating a multiple select, you may use the
deselect() method in a similar manner:
<select multiple> <option value="h">Hummus</option> <option value="t">Tahina</option> <option value="c">Chips</option> <option value="a">Amba</option> </select>
# Deselect by visible text se.find("select").deselect(text="Chips") # Deelect by value se.find("select").deselect(value="c") # Deselect by index se.find("select").deselect(i=2) # Deselect all se.find("select").deselect()
So far, we haven't taken any huge leaps from off-the-shelf Selenium, though we're certainly typing less!
One of the big issues with Selenium is waiting for pages to load completely and all of the retry logic that may have to be used to have tests that work well. A common solution is to wrap your code with "retry" functions.
For example, a naive way of retrying might have been:
browser = webdriver.Firefox() els = None while not els: els = browser.find_element_by_tag_name('button') time.sleep(0.5)
With Elementium, just tell
find() to wait:
# Retry until we find a button on the page (up to 20 seconds by default) se.find('button', wait=True)
Have a more complex success condition? Use
# Retry until we find 3 buttons on the page (up to 20 seconds by default) se.find('button').until(lambda e: len(e) == 3) # Retry for 60 seconds se.find('button').until(lambda e: len(e) == 3, ttl=60)
Both of the above methods will raise a
elementium.elements.TimeoutError if the element is not found in the specified period of time.
Basically all methods that are part of the
SeElements object will be automatically retried for you. Under the hood, each selector (e.g. '.foo' or '#foo') is stored as a callback function (similar to something like
lambda: selenium.find_element_by_id('foo')). This way, when any of the calls to any of the methods of an element has an expected error (
StaleElementException, etc.) it will recall this function. If you perform chaining, this will actually propagate that refresh (called
update()) up the entire chain to ensure that all parts of the call are valid. Cool!
(Look at the code for more detail.)
se.find('input').insist(lambda e: e.value() == 'Pilkington')
This works exactly like
until() above, only it raises an
Other useful methods
See the full Elementium documentation for more details on the following methods.
The following are simply more reliable versions of their Selenium counterparts. Some have been renamed for ease of use.
There are a couple examples in the aptly named examples directory. Take a look there for fully functioning source code.
Running the Tests
First, make sure to install the testing requirements
pip install -r requirements-tests.txt
Then run the tests via nose
Running the tests across multiple python versions in parallel
If you don't trust our Travis CI badge above, you can run all of the tests across multiple python versions by using pyenv and detox. A good writeup for what you need to do to set this up can be found here. If you are using OS X and installed pyenv with brew, make sure to follow these instructions as well.
# Assuming OS X with Homebrew installed brew update brew install pyenv
# Install tox and detox pip install tox pip install detox
You'll want to make sure that you have all of the different python versions are installed so that they can be tested:
# Install the pyenv versions pyenv install 2.7.13 pyenv install 3.4.7 pyenv install 3.5.4 pyenv install 3.6.0 pyenv install 3.7.3 # Set all these to be global versions pyenv global system 2.7.13 3.4.7 3.5.4 3.6.0 3.7.3 # Make sure that they are all there (they should all have a * next to them) pyenv versions
Note, if you are running in to issues installing python due to a
zlib issue, then take a look at the solution in this thread which can be summarized by saying that you should install your pyenv versions as follows
CFLAGS="-I$(xcrun --show-sdk-path)/usr/include" before your pyenv install ...
Once you get everything installed, you can run the tests across the different versions as follows.
Note this assumes that you have detox installed globally.
Assuming all goes well, you should see a result akin to
py27-1.7: commands succeeded py27-1.8: commands succeeded py27-1.9: commands succeeded py27-master: commands succeeded py34-1.7: commands succeeded py34-1.8: commands succeeded py34-1.9: commands succeeded py34-master: commands succeeded py35-1.8: commands succeeded py35-1.9: commands succeeded py35-master: commands succeeded congratulations :)
If you run in to an issue with running detox, make sure that you have the latest version of pip as there are some issues with pyenv and older versions of pip.
There are several features planned for the future to improve Elementium and they will be rolled out as they pass through our internal scrutiny. If you have great ideas, you can be part of Elementium's future as well!
If you would like to contribute to this project, you will need to use git flow. This way, any and all changes happen on the development branch and not on the master branch. As such, after you have git-flow-ified your elementium git repo, create a pull request for your branch, and we'll take it from there.
Elementium has been a collaborative effort of ACT.md.