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
*.swp
.selenium_log
.coverage
coverage.xml
pep8.txt
@ -20,5 +21,5 @@ django-nova-syspanel/src/django_nova_syspanel.egg-info
openstack-dashboard/.dashboard-venv
openstack-dashboard/local/dashboard_openstack.sqlite3
openstack-dashboard/local/local_settings.py
build/
docs/build/
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!
=============================
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::
./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!
================

View File

@ -6,6 +6,7 @@ parts =
openstackx
python-novaclient
python-keystoneclient
seleniumrc
develop = .
versions = versions
@ -33,6 +34,8 @@ eggs =
coverage
glance
quantum
django-nose-selenium
CherryPy
interpreter = python
@ -124,3 +127,7 @@ urls =
#recipe = bazaarrecipe
#urls =
# 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. """
if 'request' not in context:
return {}
current_dashboard = context['request'].horizon.get('dashboard', None)
dashboards = []
for dash in Horizon.get_dashboards():
if callable(dash.nav) and dash.nav(context):
@ -51,7 +52,7 @@ def horizon_main_nav(context):
dashboards.append(dash)
return {'components': dashboards,
'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)

View File

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

View File

@ -86,6 +86,7 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_nose',
'horizon',
'horizon.dashboards.nova',
'horizon.dashboards.syspanel',
@ -137,4 +138,5 @@ if DEBUG:
MIDDLEWARE_CLASSES += (
'debug_toolbar.middleware.DebugToolbarMiddleware',)
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('',
url(r'^$', 'dashboard.views.splash', name='splash'),
url(r'^qunit/$', 'dashboard.views.qunit_tests', name='qunit_tests'),
url(r'', include(horizon.urls)))
# 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
def qunit_tests(request):
return shortcuts.render(request, "qunit.html")
def user_home(user):
if user.admin:
return horizon.get_dashboard('syspanel').get_absolute_url()
@ -42,3 +46,5 @@ def splash(request):
return handled
return shortcuts.render(request, 'splash.html', {'form': form})

View File

@ -10,6 +10,7 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.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'],
'propagate': False,
},
'nose.plugins.manager': {
'handlers': ['console'],
'propagate': False,
}
}
}

View File

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

View File

@ -14,6 +14,7 @@ function usage {
echo " been added."
echo " -p, --pep8 Just run pep8"
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 " openstack-dashboard in the virtual"
echo " environment."
@ -36,6 +37,7 @@ function process_option {
-y|--pylint) let just_pylint=1;;
-f|--force) let force=1;;
-c|--coverage) let with_coverage=1;;
--skip-selenium) let selenium=-1;;
--docs) let just_docs=1;;
--runserver) let runserver=1;;
*) testargs="$testargs $1"
@ -89,6 +91,7 @@ always_venv=0
never_venv=0
force=0
with_coverage=0
selenium=0
testargs=""
django_wrapper=""
dashboard_wrapper=""
@ -143,7 +146,25 @@ then
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 {
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"
${django_wrapper} coverage erase
${django_wrapper} coverage run horizon/bin/test
@ -156,14 +177,22 @@ function run_tests {
cp local/local_settings.py local/local_settings.py.bak
fi
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
cp local/local_settings.py.bak local/local_settings.py
rm local/local_settings.py.bak
fi
cd ..
# get results of the openstack-dashboard tests
DASHBOARD_RESULT=$?
cd ..
if [ $with_coverage -eq 1 ]; then
echo "Generating coverage reports"
${django_wrapper} coverage combine
@ -171,6 +200,18 @@ function run_tests {
${django_wrapper} coverage html -i --omit='/usr*,setup.py,*egg*' -d reports
exit $(($OPENSTACK_RESULT || $DASHBOARD_RESULT))
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