Make feature test app teardown more reliable, and tests faster

- don't spin up app and chromedriver between each test
    - catching signals also tears down the app
    - do layout reset between tests, but assume that tests will not leave a modal opened.

 Use selenium built-in waiting function and fix flakiness around clicking the alertify OK button

    - we think the OK button does not have its event bound when it is created.

If you see more flakiness around clicking the alertify OK button, let us know. The element is clickable but we have to arbitrarily wait for the event to be bound and that timing may vary system to system.

The feature tests are about 7 seconds faster now.

Tira & Joao
pull/3/head
Atira Odhner 2017-03-01 13:20:06 +00:00 committed by Dave Page
parent 59c6be534d
commit e89c54c15d
7 changed files with 71 additions and 50 deletions

View File

@ -40,7 +40,6 @@ class ConnectsToServerFeatureTest(BaseFeatureTest):
def tearDown(self): def tearDown(self):
self.page.remove_server(self.server) self.page.remove_server(self.server)
self.app_starter.stop_app()
connection = test_utils.get_db_connection(self.server['db'], connection = test_utils.get_db_connection(self.server['db'],
self.server['username'], self.server['username'],

View File

@ -1,4 +1,3 @@
from selenium import webdriver
from selenium.webdriver import ActionChains from selenium.webdriver import ActionChains
from regression import test_utils from regression import test_utils
@ -44,7 +43,6 @@ class TemplateSelectionFeatureTest(BaseFeatureTest):
def tearDown(self): def tearDown(self):
self.page.find_by_xpath("//button[contains(.,'Cancel')]").click() self.page.find_by_xpath("//button[contains(.,'Cancel')]").click()
self.page.remove_server(self.server) self.page.remove_server(self.server)
self.app_starter.stop_app()
connection = test_utils.get_db_connection(self.server['db'], connection = test_utils.get_db_connection(self.server['db'],
self.server['username'], self.server['username'],
self.server['db_password'], self.server['db_password'],

View File

@ -96,3 +96,7 @@ class BaseTestGenerator(unittest.TestCase):
@classmethod @classmethod
def setTestClient(cls, test_client): def setTestClient(cls, test_client):
cls.tester = test_client cls.tester = test_client
@classmethod
def setDriver(cls, driver):
cls.driver = driver

View File

@ -1,8 +1,5 @@
from selenium import webdriver
import config as app_config import config as app_config
from pgadmin.utils.route import BaseTestGenerator from pgadmin.utils.route import BaseTestGenerator
from regression.feature_utils.app_starter import AppStarter
from regression.feature_utils.pgadmin_page import PgadminPage from regression.feature_utils.pgadmin_page import PgadminPage
@ -12,11 +9,11 @@ class BaseFeatureTest(BaseTestGenerator):
self.skipTest("Currently, config is set to start pgadmin in server mode. " self.skipTest("Currently, config is set to start pgadmin in server mode. "
"This test doesn't know username and password so doesn't work in server mode") "This test doesn't know username and password so doesn't work in server mode")
driver = webdriver.Chrome() self.page = PgadminPage(self.driver, app_config)
self.app_starter = AppStarter(driver, app_config)
self.page = PgadminPage(driver, app_config)
self.app_starter.start_app()
self.page.wait_for_app() self.page.wait_for_app()
self.page.wait_for_spinner_to_disappear()
self.page.reset_layout()
self.page.wait_for_spinner_to_disappear()
def failureException(self, *args, **kwargs): def failureException(self, *args, **kwargs):
self.page.driver.save_screenshot('/tmp/feature_test_failure.png') self.page.driver.save_screenshot('/tmp/feature_test_failure.png')

View File

@ -1,21 +1,33 @@
import time import time
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException, WebDriverException
from selenium.webdriver import ActionChains from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
class PgadminPage: class PgadminPage:
""" """
Helper class for interacting with the page, given a selenium driver Helper class for interacting with the page, given a selenium driver
""" """
def __init__(self, driver, app_config): def __init__(self, driver, app_config):
self.driver = driver self.driver = driver
self.app_config = app_config self.app_config = app_config
self.timeout = 10
def reset_layout(self):
self.click_element(self.find_by_partial_link_text("File"))
self.find_by_partial_link_text("Reset Layout").click()
self.click_modal_ok()
def click_modal_ok(self):
time.sleep(0.1)
self.click_element(self.find_by_xpath("//button[contains(.,'OK')]"))
def add_server(self, server_config): def add_server(self, server_config):
self.wait_for_spinner_to_disappear()
self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click()
self.driver.find_element_by_link_text("Object").click() self.driver.find_element_by_link_text("Object").click()
ActionChains(self.driver) \ ActionChains(self.driver) \
@ -37,24 +49,36 @@ class PgadminPage:
self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click()
self.find_by_partial_link_text("Object").click() self.find_by_partial_link_text("Object").click()
self.find_by_partial_link_text("Delete/Drop").click() self.find_by_partial_link_text("Delete/Drop").click()
time.sleep(0.5) self.click_modal_ok()
self.find_by_xpath("//button[contains(.,'OK')]").click()
def toggle_open_tree_item(self, tree_item_text): def toggle_open_tree_item(self, tree_item_text):
self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click() self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "']/../*[@class='aciTreeButton']").click()
def find_by_xpath(self, xpath): def find_by_xpath(self, xpath):
return self.wait_for_element(lambda: self.driver.find_element_by_xpath(xpath)) return self.wait_for_element(lambda (driver): driver.find_element_by_xpath(xpath))
def find_by_id(self, element_id): def find_by_id(self, element_id):
return self.wait_for_element(lambda: self.driver.find_element_by_id(element_id)) return self.wait_for_element(lambda (driver): driver.find_element_by_id(element_id))
def find_by_partial_link_text(self, link_text): def find_by_partial_link_text(self, link_text):
return self.wait_for_element(lambda: self.driver.find_element_by_partial_link_text(link_text)) return self._wait_for(
'link with text "#{0}"'.format(link_text),
EC.element_to_be_clickable((By.PARTIAL_LINK_TEXT, link_text))
)
def click_element(self, element):
def click_succeeded(driver):
try:
element.click()
return True
except WebDriverException:
return False
return self._wait_for("clicking the element not to throw an exception", click_succeeded)
def fill_input_by_field_name(self, field_name, field_content): def fill_input_by_field_name(self, field_name, field_content):
field = self.find_by_xpath("//input[@name='" + field_name + "']") field = self.find_by_xpath("//input[@name='" + field_name + "']")
backspaces = [Keys.BACKSPACE]*len(field.get_attribute('value')) backspaces = [Keys.BACKSPACE] * len(field.get_attribute('value'))
field.click() field.click()
field.send_keys(backspaces) field.send_keys(backspaces)
@ -70,8 +94,8 @@ class PgadminPage:
self.find_by_xpath("//*[contains(@class,'wcPanelTab') and contains(.,'" + tab_name + "')]").click() self.find_by_xpath("//*[contains(@class,'wcPanelTab') and contains(.,'" + tab_name + "')]").click()
def wait_for_input_field_content(self, field_name, content): def wait_for_input_field_content(self, field_name, content):
def input_field_has_content(): def input_field_has_content(driver):
element = self.driver.find_element_by_xpath( element = driver.find_element_by_xpath(
"//input[@name='" + field_name + "']") "//input[@name='" + field_name + "']")
return str(content) == element.get_attribute('value') return str(content) == element.get_attribute('value')
@ -79,10 +103,10 @@ class PgadminPage:
return self._wait_for("field to contain '" + str(content) + "'", input_field_has_content) return self._wait_for("field to contain '" + str(content) + "'", input_field_has_content)
def wait_for_element(self, find_method_with_args): def wait_for_element(self, find_method_with_args):
def element_if_it_exists(): def element_if_it_exists(driver):
try: try:
element = find_method_with_args() element = find_method_with_args(driver)
if element.is_displayed() & element.is_enabled(): if element.is_displayed() and element.is_enabled():
return element return element
except NoSuchElementException: except NoSuchElementException:
return False return False
@ -90,9 +114,9 @@ class PgadminPage:
return self._wait_for("element to exist", element_if_it_exists) return self._wait_for("element to exist", element_if_it_exists)
def wait_for_spinner_to_disappear(self): def wait_for_spinner_to_disappear(self):
def spinner_has_disappeared(): def spinner_has_disappeared(driver):
try: try:
self.driver.find_element_by_id("pg-spinner") driver.find_element_by_id("pg-spinner")
return False return False
except NoSuchElementException: except NoSuchElementException:
return True return True
@ -100,25 +124,15 @@ class PgadminPage:
self._wait_for("spinner to disappear", spinner_has_disappeared) self._wait_for("spinner to disappear", spinner_has_disappeared)
def wait_for_app(self): def wait_for_app(self):
def page_shows_app(): def page_shows_app(driver):
if self.driver.title == self.app_config.APP_NAME: if driver.title == self.app_config.APP_NAME:
return True return True
else: else:
self.driver.refresh() driver.refresh()
return False return False
self._wait_for("app to start", page_shows_app) self._wait_for("app to start", page_shows_app)
def _wait_for(self, waiting_for_message, condition_met_function): def _wait_for(self, waiting_for_message, condition_met_function):
timeout = 10 return WebDriverWait(self.driver, self.timeout, 0.01).until(condition_met_function,
time_waited = 0 "Timed out waiting for " + waiting_for_message)
sleep_time = 0.01
while time_waited < timeout:
result = condition_met_function()
if result:
return result
time_waited += sleep_time
time.sleep(sleep_time)
raise AssertionError("timed out waiting for " + waiting_for_message)

View File

@ -10,14 +10,17 @@
""" This file collect all modules/files present in tests directory and add """ This file collect all modules/files present in tests directory and add
them to TestSuite. """ them to TestSuite. """
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import os
import sys
import signal
import atexit import atexit
import logging import logging
import os
import signal
import sys
import traceback import traceback
from selenium import webdriver
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
import unittest2 as unittest import unittest2 as unittest
else: else:
@ -40,6 +43,7 @@ if sys.path[0] != root:
from pgadmin import create_app from pgadmin import create_app
import config import config
from regression import test_setup from regression import test_setup
from regression.feature_utils.app_starter import AppStarter
# Delete SQLite db file if exists # Delete SQLite db file if exists
if os.path.isfile(config.TEST_SQLITE_PATH): if os.path.isfile(config.TEST_SQLITE_PATH):
@ -88,7 +92,10 @@ config.CONSOLE_LOG_LEVEL = WARNING
app = create_app() app = create_app()
app.config['WTF_CSRF_ENABLED'] = False app.config['WTF_CSRF_ENABLED'] = False
test_client = app.test_client() test_client = app.test_client()
drop_objects = test_utils.get_cleanup_handler(test_client) driver = webdriver.Chrome()
app_starter = AppStarter(driver, config)
app_starter.start_app()
handle_cleanup = test_utils.get_cleanup_handler(test_client, app_starter)
def get_suite(module_list, test_server, test_app_client): def get_suite(module_list, test_server, test_app_client):
@ -118,6 +125,7 @@ def get_suite(module_list, test_server, test_app_client):
obj.setApp(app) obj.setApp(app)
obj.setTestClient(test_app_client) obj.setTestClient(test_app_client)
obj.setTestServer(test_server) obj.setTestServer(test_server)
obj.setDriver(driver)
scenario = generate_scenarios(obj) scenario = generate_scenarios(obj)
pgadmin_suite.addTests(scenario) pgadmin_suite.addTests(scenario)
@ -180,7 +188,7 @@ def add_arguments():
def sig_handler(signo, frame): def sig_handler(signo, frame):
drop_objects() handle_cleanup()
def get_tests_result(test_suite): def get_tests_result(test_suite):
@ -242,7 +250,7 @@ if __name__ == '__main__':
test_result = dict() test_result = dict()
# Register cleanup function to cleanup on exit # Register cleanup function to cleanup on exit
atexit.register(drop_objects) atexit.register(handle_cleanup)
# Set signal handler for cleanup # Set signal handler for cleanup
signal_list = dir(signal) signal_list = dir(signal)
required_signal_list = ['SIGTERM', 'SIGABRT', 'SIGQUIT', 'SIGINT'] required_signal_list = ['SIGTERM', 'SIGABRT', 'SIGQUIT', 'SIGINT']

View File

@ -365,7 +365,7 @@ def remove_db_file():
os.remove(config.TEST_SQLITE_PATH) os.remove(config.TEST_SQLITE_PATH)
def _drop_objects(tester): def _cleanup(tester, app_starter):
"""This function use to cleanup the created the objects(servers, databases, """This function use to cleanup the created the objects(servers, databases,
schemas etc) during the test suite run""" schemas etc) during the test suite run"""
try: try:
@ -404,11 +404,12 @@ def _drop_objects(tester):
logout_tester_account(tester) logout_tester_account(tester)
# Remove SQLite db file # Remove SQLite db file
remove_db_file() remove_db_file()
app_starter.stop_app()
def get_cleanup_handler(tester): def get_cleanup_handler(tester, app_starter):
"""This function use to bind variable to drop_objects function""" """This function use to bind variable to drop_objects function"""
return partial(_drop_objects, tester) return partial(_cleanup, tester, app_starter)
class Database: class Database: