Adds selenium and qunit integration into Django test suite.

Implements blueprint frontend-testing.
Implements blueprint javascript-unit-tests.

Adds selenium to buildout script and uses django-nose-selenium to integrate with Django's unit test machinery. Includes proof-of-implementation tests with both selenium and qunit.

Change-Id: Ic7db4994be398c633a78dca7369359602c7d8f57
This commit is contained in:
Gabriel Hurley 2011-11-08 16:38:03 -08:00
parent 67a979ae99
commit 2532d25c08
15 changed files with 1936 additions and 8 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.pyc *.pyc
*.swp *.swp
.selenium_log
.coverage .coverage
coverage.xml coverage.xml
pep8.txt pep8.txt
@ -20,5 +21,5 @@ django-nova-syspanel/src/django_nova_syspanel.egg-info
openstack-dashboard/.dashboard-venv openstack-dashboard/.dashboard-venv
openstack-dashboard/local/dashboard_openstack.sqlite3 openstack-dashboard/local/dashboard_openstack.sqlite3
openstack-dashboard/local/local_settings.py openstack-dashboard/local/local_settings.py
build/ docs/build/
docs/source/sourcecode docs/source/sourcecode

View File

@ -28,12 +28,18 @@ environments will be necessary but not necessarily as time consuming.
I just want to run the tests! I just want to run the tests!
============================= =============================
Running both sets of unit tests quickly and easily is the main goal of this Running the full set of unit tests quickly and easily is the main goal of this
script. All you need to do is:: script. All you need to do is::
./run_tests.sh ./run_tests.sh
Yep, that's it. Everything else the script can do is optional. Yep, that's it. However, for a quicker test run you can skip the Selenium
tests by using the ``--skip-selenium`` flag::
./run_tests.sh --skip-selenium
This isn't recommended, but can be a timesaver when you only need to run
the code tests and not the frontend tests during development.
Give me metrics! Give me metrics!
================ ================

View File

@ -6,6 +6,7 @@ parts =
openstackx openstackx
python-novaclient python-novaclient
python-keystoneclient python-keystoneclient
seleniumrc
develop = . develop = .
versions = versions versions = versions
@ -33,6 +34,8 @@ eggs =
coverage coverage
glance glance
quantum quantum
django-nose-selenium
CherryPy
interpreter = python interpreter = python
@ -124,3 +127,7 @@ urls =
#recipe = bazaarrecipe #recipe = bazaarrecipe
#urls = #urls =
# https://launchpad.net/~hudson-openstack/glance/trunk/ glance # https://launchpad.net/~hudson-openstack/glance/trunk/ glance
[seleniumrc]
recipe=collective.recipe.seleniumrc

View File

@ -43,6 +43,7 @@ def horizon_main_nav(context):
""" Generates top-level dashboard navigation entries. """ """ Generates top-level dashboard navigation entries. """
if 'request' not in context: if 'request' not in context:
return {} return {}
current_dashboard = context['request'].horizon.get('dashboard', None)
dashboards = [] dashboards = []
for dash in Horizon.get_dashboards(): for dash in Horizon.get_dashboards():
if callable(dash.nav) and dash.nav(context): if callable(dash.nav) and dash.nav(context):
@ -51,7 +52,7 @@ def horizon_main_nav(context):
dashboards.append(dash) dashboards.append(dash)
return {'components': dashboards, return {'components': dashboards,
'user': context['request'].user, 'user': context['request'].user,
'current': context['request'].horizon['dashboard'].slug} 'current': getattr(current_dashboard, 'slug', None)}
@register.inclusion_tag('horizon/_subnav_list.html', takes_context=True) @register.inclusion_tag('horizon/_subnav_list.html', takes_context=True)

View File

@ -26,11 +26,13 @@ TESTSERVER = 'http://testserver'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/tmp/horizon.db'}} 'NAME': '/tmp/horizon.db',
'TEST_NAME': '/tmp/test_horizon.db',}}
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django_nose',
'horizon', 'horizon',
'horizon.tests', 'horizon.tests',
'horizon.dashboards.nova', 'horizon.dashboards.nova',
@ -80,6 +82,8 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--nocapture', NOSE_ARGS = ['--nocapture',
'--cover-package=horizon', '--cover-package=horizon',
'--cover-inclusive'] '--cover-inclusive']
# For nose-selenium integration
LIVE_SERVER_PORT = 8000
# django-mailer uses a different config attribute # django-mailer uses a different config attribute
# even though it just wraps django.core.mail # even though it just wraps django.core.mail

View File

@ -86,6 +86,7 @@ INSTALLED_APPS = (
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_nose',
'horizon', 'horizon',
'horizon.dashboards.nova', 'horizon.dashboards.nova',
'horizon.dashboards.syspanel', 'horizon.dashboards.syspanel',
@ -137,4 +138,5 @@ if DEBUG:
MIDDLEWARE_CLASSES += ( MIDDLEWARE_CLASSES += (
'debug_toolbar.middleware.DebugToolbarMiddleware',) 'debug_toolbar.middleware.DebugToolbarMiddleware',)
except ImportError: except ImportError:
logging.info('Running in debug mode without debug_toolbar.') _logger = logging.getLogger(__name__)
_logger.debug('Running in debug mode without debug_toolbar.')

View File

@ -0,0 +1,226 @@
/**
* QUnit 1.2.0pre - A JavaScript Unit Testing Framework
*
* http://docs.jquery.com/QUnit
*
* Copyright (c) 2011 John Resig, Jörn Zaefferer
* Dual licensed under the MIT (MIT-LICENSE.txt)
* or GPL (GPL-LICENSE.txt) licenses.
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699a4;
background-color: #0d3349;
font-size: 1.5em;
line-height: 1em;
font-weight: normal;
border-radius: 15px 15px 0 0;
-moz-border-radius: 15px 15px 0 0;
-webkit-border-top-right-radius: 15px;
-webkit-border-top-left-radius: 15px;
}
#qunit-header a {
text-decoration: none;
color: #c2ccd1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #fff;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #eee;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2b81af;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #fff;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #c2ccd1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests ol {
margin-top: 0.5em;
padding: 0.5em;
background-color: #fff;
border-radius: 15px;
-moz-border-radius: 15px;
-webkit-border-radius: 15px;
box-shadow: inset 0px 2px 13px #999;
-moz-box-shadow: inset 0px 2px 13px #999;
-webkit-box-shadow: inset 0px 2px 13px #999;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: .2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 .5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #e0f2be;
color: #374e0c;
text-decoration: none;
}
#qunit-tests ins {
background-color: #ffcaca;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: black; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
margin: 0.5em;
padding: 0.4em 0.5em 0.4em 0.5em;
background-color: #fff;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #5E740B;
background-color: #fff;
border-left: 26px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #fff;
border-left: 26px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 15px 15px;
-moz-border-radius: 0 0 15px 15px;
-webkit-border-bottom-right-radius: 15px;
-webkit-border-bottom-left-radius: 15px;
}
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: green; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2b81af;
background-color: #D2E0E6;
border-bottom: 1px solid white;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="{{ STATIC_URL }}qunit/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="{{ STATIC_URL }}qunit/qunit.js"></script>
</head>
<body>
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup</div>
</body>
</html>

View File

@ -0,0 +1,12 @@
from django import test
from noseselenium.cases import SeleniumTestCaseMixin
class SeleniumTests(test.TestCase, SeleniumTestCaseMixin):
def test_splash(self):
self.selenium.open("/")
self.failUnless(self.selenium.is_text_present("User Name"))
def test_qunit(self):
self.selenium.open("/qunit/")
self.selenium.wait_for_page_to_load("2000")
self.failUnless(self.selenium.is_text_present("0 failed"))

View File

@ -32,6 +32,7 @@ import horizon
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', 'dashboard.views.splash', name='splash'), url(r'^$', 'dashboard.views.splash', name='splash'),
url(r'^qunit/$', 'dashboard.views.qunit_tests', name='qunit_tests'),
url(r'', include(horizon.urls))) url(r'', include(horizon.urls)))
# Development static app and project media serving using the staticfiles app. # Development static app and project media serving using the staticfiles app.

View File

@ -29,6 +29,10 @@ import horizon
from horizon.views import auth as auth_views from horizon.views import auth as auth_views
def qunit_tests(request):
return shortcuts.render(request, "qunit.html")
def user_home(user): def user_home(user):
if user.admin: if user.admin:
return horizon.get_dashboard('syspanel').get_absolute_url() return horizon.get_dashboard('syspanel').get_absolute_url()
@ -42,3 +46,5 @@ def splash(request):
return handled return handled
return shortcuts.render(request, 'splash.html', {'form': form}) return shortcuts.render(request, 'splash.html', {'form': form})

View File

@ -10,6 +10,7 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(LOCAL_PATH, 'dashboard_openstack.sqlite3'), 'NAME': os.path.join(LOCAL_PATH, 'dashboard_openstack.sqlite3'),
'TEST_NAME': os.path.join(LOCAL_PATH, 'test.sqlite3'),
}, },
} }
@ -94,6 +95,10 @@ LOGGING = {
'handlers': ['console'], 'handlers': ['console'],
'propagate': False, 'propagate': False,
}, },
'nose.plugins.manager': {
'handlers': ['console'],
'propagate': False,
}
} }
} }

View File

@ -1,7 +1,9 @@
coverage coverage
CherryPy
Django==1.3 Django==1.3
django-mailer django-mailer
django-nose==0.1.2 django-nose==0.1.2
django-nose-selenium
django-registration==0.7 django-registration==0.7
eventlet eventlet
glance glance

View File

@ -14,6 +14,7 @@ function usage {
echo " been added." echo " been added."
echo " -p, --pep8 Just run pep8" echo " -p, --pep8 Just run pep8"
echo " -y, --pylint Just run pylint" echo " -y, --pylint Just run pylint"
echo " --skip-selenium Run unit tests but skip Selenium tests"
echo " --runserver Run the Django development server for" echo " --runserver Run the Django development server for"
echo " openstack-dashboard in the virtual" echo " openstack-dashboard in the virtual"
echo " environment." echo " environment."
@ -36,6 +37,7 @@ function process_option {
-y|--pylint) let just_pylint=1;; -y|--pylint) let just_pylint=1;;
-f|--force) let force=1;; -f|--force) let force=1;;
-c|--coverage) let with_coverage=1;; -c|--coverage) let with_coverage=1;;
--skip-selenium) let selenium=-1;;
--docs) let just_docs=1;; --docs) let just_docs=1;;
--runserver) let runserver=1;; --runserver) let runserver=1;;
*) testargs="$testargs $1" *) testargs="$testargs $1"
@ -89,6 +91,7 @@ always_venv=0
never_venv=0 never_venv=0
force=0 force=0
with_coverage=0 with_coverage=0
selenium=0
testargs="" testargs=""
django_wrapper="" django_wrapper=""
dashboard_wrapper="" dashboard_wrapper=""
@ -143,7 +146,25 @@ then
fi fi
fi fi
function wait_for_selenium {
# Selenium can sometimes take several seconds to start.
STARTED=`grep -irn "Started SocketListener on 0.0.0.0:4444" .selenium_log`
if [ $? -eq 0 ]; then
echo "Selenium server started."
else
echo -n "."
sleep 1
wait_for_selenium
fi
}
function run_tests { function run_tests {
if [ $selenium -eq 0 ]; then
echo "Starting Selenium server..."
${django_wrapper} horizon/bin/seleniumrc > .selenium_log &
wait_for_selenium
fi
echo "Running Horizon application tests" echo "Running Horizon application tests"
${django_wrapper} coverage erase ${django_wrapper} coverage erase
${django_wrapper} coverage run horizon/bin/test ${django_wrapper} coverage run horizon/bin/test
@ -156,14 +177,22 @@ function run_tests {
cp local/local_settings.py local/local_settings.py.bak cp local/local_settings.py local/local_settings.py.bak
fi fi
cp local/local_settings.py.example local/local_settings.py cp local/local_settings.py.example local/local_settings.py
${dashboard_wrapper} coverage run dashboard/manage.py test
if [ $selenium -eq 0 ]; then
${dashboard_wrapper} coverage run dashboard/manage.py test --with-selenium --with-cherrypyliveserver
else
${dashboard_wrapper} coverage run dashboard/manage.py test
fi
if [ -f local/local_settings.py.bak ]; then if [ -f local/local_settings.py.bak ]; then
cp local/local_settings.py.bak local/local_settings.py cp local/local_settings.py.bak local/local_settings.py
rm local/local_settings.py.bak rm local/local_settings.py.bak
fi fi
cd ..
# get results of the openstack-dashboard tests # get results of the openstack-dashboard tests
DASHBOARD_RESULT=$? DASHBOARD_RESULT=$?
cd ..
if [ $with_coverage -eq 1 ]; then if [ $with_coverage -eq 1 ]; then
echo "Generating coverage reports" echo "Generating coverage reports"
${django_wrapper} coverage combine ${django_wrapper} coverage combine
@ -171,6 +200,18 @@ function run_tests {
${django_wrapper} coverage html -i --omit='/usr*,setup.py,*egg*' -d reports ${django_wrapper} coverage html -i --omit='/usr*,setup.py,*egg*' -d reports
exit $(($OPENSTACK_RESULT || $DASHBOARD_RESULT)) exit $(($OPENSTACK_RESULT || $DASHBOARD_RESULT))
fi fi
if [ $selenium -eq 0 ]; then
echo "Stopping Selenium server..."
SELENIUM_JOB=`ps -elf | grep "selenium" | grep -v grep`
if [ $? -eq 0 ]; then
kill `echo "${SELENIUM_JOB}" | awk '{print $4}'`
echo "Selenium process stopped."
else
echo "Selenium process not found. This may require manual claenup."
fi
rm -f .selenium_log
fi
} }
if [ $just_docs -eq 1 ]; then if [ $just_docs -eq 1 ]; then