Testing with Behave Framework & Selenium
White Owl Diamond
Stumptown Cold Brew
Behave Framework
This is a very simple test using the Behave Framework and Selenium. I chose to start with a login
test since the bot must always have login capabilities (aka just in case Twitter decides to block my automation).
I want to specifically point out my directory structure to give a better idea on how your tests can scale. Lets start with the Feature
file.
See code on Github
Feature File
# regression/features/login_to_twitter.feature
# Created by churt at 2/16/19
@twitter
Feature: Login to Twitter
The Bot should always be able to post.
It is important that the login always works.
Run this test all damn day and let me know if it fails
cuz that means my API key is likely revoked!
Scenario Outline: GlobalEntry Bot logs in to twitter
Given twitters homepage
When "<user>" logs in
Then the twitter timeline appears
Examples: users
| user |
| GlobalEntry |
I'm using the Scenario Outline
function in order to iterate the Examples
table. This allows me to execute the scenario
with multiple users
(aka usernames).
Steps
# /regression/lib/steps/login_to_twitter.py
@given("twitters homepage")
def step_load_twitter(context):
"""
:type context: behave.runner.Context
"""
context.login_page = LoginPage(context)
assert_that(context.login_page.browser.current_url, contains_string(URL.TWITTER))
assert_that(context.login_page.username.get_attribute('autocomplete'), equal_to(Constants.USERNAME))
time.sleep(4)
@when('"(?P<user>.+)" logs in')
def step_login(context, user):
"""
:type context: behave.runner.Context
:type user: str
"""
context.timeline_page = context.login_page.login(username=context.twitter_username, password=context.twitter_password)
time.sleep(3)
assert_that(context.timeline_page.avatar.get_attribute('title'), equal_to("Profile and settings"))
@then("the twitter timeline appears")
def step_impl(context):
"""
:type context: behave.runner.Context
"""
context.timeline_page.view_latest_tweets(search=True)
time.sleep(3)
assert_that(context.timeline_page.browser.current_url, contains_string(Constants.TWEETS))
Step functions are decorated with given, when, then, or step (....wtf?). Notice each function's decorator is either given
, when
, or then
and corresponds with steps found in my Feature
file. Steps should drive the webdriver
and then make assertions
to ensure tests pass.
Page Objects
# regression/lib/page_objects/login.py
class LoginPage(BasePage):
def __init__(self, context):
BasePage.__init__(self,
context.browser,
)
self.context = context
self.base_url = URL.TWITTER
self.visit(url=self.base_url)
locator_dictionary = LoginLocators.__dict__
def login(self, username='', password=''):
self.username.send_keys(username)
self.password.send_keys(password)
self.submit_btn.click()
return TwitterTimeline(self.context)
Page objects are designed to provide methods for quick page actions. For example, my LoginPage
object has attributes (aka elements) of Twitter's login page, so the login()
method is called in my step definition to log in. The other option would have been to write the login logic within my step definition, but IMO this is easier for refactoring.
Base Page Object
# regression/lib/page_objects/base_page.py
class BasePage(object):
"""
Really the only method used here is '__getattr__' now
"""
def __init__(self, browser, base_url='', **kwargs):
self.browser = browser
self.base_url = base_url
self.timeout = 10
@contextlib.contextmanager
def wait_for_page_load(self, timeout=10):
old_page = self.find_element_by_tage_name('html')
yield
WebDriverWait(self, timeout).until(staleness_of(old_page))
def find_element(self, *loc):
return self.browser.find_element(*loc)
def find_elements(self, *loc):
return self.browser.find_elements(*loc)
def visit(self, url='', route=''):
if not url:
url = self.base_url
self.browser.get(url + route)
def hover(self, element):
ActionChains(self.browser).move_to_element(element).perform()
# I don't like this but hover is sensitive and needs some sleep time
time.sleep(5)
def __getattr__(self, what):
try:
if what in self.locator_dictionary.keys():
try:
element = WebDriverWait(self.browser, self.timeout).until(
EC.presence_of_element_located(self.locator_dictionary[what])
)
except(TimeoutException, StaleElementReferenceException):
traceback.print_exc()
try:
element = WebDriverWait(self.browser, self.timeout).until(
EC.visibility_of_element_located(self.locator_dictionary[what])
)
except(TimeoutException, StaleElementReferenceException):
traceback.print_exc()
# I could have returned element, however because of lazy loading, I am seeking the element before return
return self.find_element(*self.locator_dictionary[what])
except AttributeError:
super(BasePage, self).__getattribute__("method_missing")(what)
def method_missing(self, what):
print "No %s here!" % what
Pointing out my BasePage
object that is inherited by every page object. The __getattr__
allows for simple syntax... it is the reason why login()
's logic is written plainly as self.username.send_keys(username)
.
Locators
# regression/lib/locators/login.py
class LoginLocators:
username = (By.NAME, "session[username_or_email]")
password = (By.NAME, "session[password]")
submit_btn = (By.CLASS_NAME, "js-submit")
A Locator is a Class that contain the elements a page object needs for its methods. Notice my LoginLocators
class contains the same attributes used in LoginPage
, and LoginPage
has an attribute named locator_dictionary
that has a value of LoginLocators.__dict__
.
Register Steps
# regression/lib/steps/__all_steps__.py
from regression.lib.steps.login_to_twitter import *
# regression/lib/steps/__init__.py
__all__ = ["__all_steps__",]
regression/features/steps/__init__.py
from regression.lib.steps import __all_steps__
Notice the location of each file. It is very important to register
steps in order for features
to execute.
Environment Settings
regression/features/environment.py
use_step_matcher("re")
def before_all(context):
context.logger = logging
# import config settings
with open(os.getcwd() + os.path.sep + Path.CONFIG, 'r') as ymlfile:
context.config = yaml.safe_load(ymlfile)
context.browser = context.config.get('env')['browser']
if context.browser == 'chrome':
context.browser = webdriver.Chrome()
else:
raise RuntimeError("Please run with Chrome")
if not os.path.exists(Path.FAILED_SCREENSHOT):
os.makedirs(Path.FAILED_SCREENSHOT)
context.twitter_username = context.config.get('userdata')['username']
if not context.twitter_username:
context.twitter_username = os.environ.get("TWITTER_USERNAME")
context.twitter_password = context.config.get('userdata')['password']
if not context.twitter_password:
context.twitter_password = os.environ.get("TWITTER_PASSWORD")
def after_scenario(context, scenario):
if scenario.status == "failed":
print("Failed: {}".format(Path.FAILED_SCREENSHOT + scenario.name + "_failed.png"))
context.logger.info("Failed: {}".format(Path.FAILED_SCREENSHOT + scenario.name + "_failed.png"))
context.browser.save_screenshot(Path.FAILED_SCREENSHOT + scenario.name + "_failed.png")
context.browser.delete_all_cookies()
def after_feature(context, feature):
context.browser.quit()
A few minimal things I like to do here:
- Initiate
logging
- Load
configs
fromyml
file (top-level directory) - Above step can be loaded from
behave.ini
(userdata section) as well - Pass variables to
context
duringbefore_all()
hook - Create screenshot directory
- Take screenshot if test fails in
after_scenario
hook
There are more hooks available for the environment.py
file. I suggest reading behave's documentation
for advanced topics.
Run Tests
$ behave