diff --git a/.gitignore b/.gitignore
index e038b53ce1..8b413ce11d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
 *.swp
 *~
 .openstackclient-venv
+.testrepository
 .tox
 .venv
 AUTHORS
diff --git a/.testr.conf b/.testr.conf
new file mode 100644
index 0000000000..2109af6ce0
--- /dev/null
+++ b/.testr.conf
@@ -0,0 +1,4 @@
+[DEFAULT]
+test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION
+test_id_option=--load-list $IDFILE
+test_list_option=--list
diff --git a/HACKING b/HACKING
index 1218e5f712..e9bcb7eaf4 100644
--- a/HACKING
+++ b/HACKING
@@ -112,3 +112,11 @@ Text encoding
     returntext = do_some_magic_with(mytext)
     returnstring = returntext.encode('utf-8')
     outfile.write(returnstring)
+
+Running Tests
+-------------
+The testing system is based on a combination of tox and testr. If you just
+want to run the whole suite, run `tox` and all will be fine. However, if
+you'd like to dig in a bit more, you might want to learn some things about
+testr itself. A basic walkthrough for OpenStack can be found at
+http://wiki.openstack.org/testr
diff --git a/run_tests.sh b/run_tests.sh
index 4700c1164a..ff5f83ec4e 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -33,8 +33,8 @@ function process_option {
     -p|--pep8) just_pep8=1;;
     -P|--no-pep8) no_pep8=1;;
     -c|--coverage) coverage=1;;
-    -*) noseopts="$noseopts $1";;
-    *) noseargs="$noseargs $1"
+    -*) testropts="$testropts $1";;
+    *) testrargs="$testrargs $1"
   esac
 }
 
@@ -45,34 +45,62 @@ never_venv=0
 force=0
 no_site_packages=0
 installvenvopts=
-noseargs=
-noseopts=
+testrargs=
+testropts=
 wrapper=""
 just_pep8=0
 no_pep8=0
 coverage=0
 
+LANG=en_US.UTF-8
+LANGUAGE=en_US:en
+LC_ALL=C
+
 for arg in "$@"; do
   process_option $arg
 done
 
-# If enabled, tell nose to collect coverage data
-if [ $coverage -eq 1 ]; then
-    noseopts="$noseopts --with-coverage --cover-package=openstackclient"
-fi
-
 if [ $no_site_packages -eq 1 ]; then
   installvenvopts="--no-site-packages"
 fi
 
+function init_testr {
+  if [ ! -d .testrepository ]; then
+    ${wrapper} testr init
+  fi
+}
+
 function run_tests {
+  # Cleanup *.pyc
+  ${wrapper} find . -type f -name "*.pyc" -delete
+
+  if [ $coverage -eq 1 ]; then
+    # Do not test test_coverage_ext when gathering coverage.
+    if [ "x$testrargs" = "x" ]; then
+      testrargs = "^(?!.*test_coverage_ext).*$"
+    fi
+    export PYTHON="${wrapper} coverage run --source novaclient --parallel-mode"
+  fi
   # Just run the test suites in current environment
-  ${wrapper} $NOSETESTS
-  # If we get some short import error right away, print the error log directly
+  set +e
+  TESTRTESTS="$TESTRTESTS $testrargs"
+  echo "Running \`${wrapper} $TESTRTESTS\`"
+  ${wrapper} $TESTRTESTS
   RESULT=$?
+  set -e
+
+  copy_subunit_log
+
   return $RESULT
 }
 
+function copy_subunit_log {
+  LOGNAME=`cat .testrepository/next-stream`
+  LOGNAME=$(($LOGNAME - 1))
+  LOGNAME=".testrepository/${LOGNAME}"
+  cp $LOGNAME subunit.log
+}
+
 function run_pep8 {
   echo "Running pep8 ..."
   srcfiles="openstackclient tests"
@@ -96,7 +124,7 @@ function run_pep8 {
   ${wrapper} pep8 ${pep8_opts} ${srcfiles}
 }
 
-NOSETESTS="nosetests $noseopts $noseargs"
+TESTRTESTS="testr run --parallel $testropts"
 
 if [ $never_venv -eq 0 ]
 then
@@ -134,13 +162,14 @@ if [ $just_pep8 -eq 1 ]; then
     exit
 fi
 
+init_testr
 run_tests
 
 # NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
 # not when we're running tests individually. To handle this, we need to
 # distinguish between options (noseopts), which begin with a '-', and
-# arguments (noseargs).
-if [ -z "$noseargs" ]; then
+# arguments (testrargs).
+if [ -z "$testrargs" ]; then
   if [ $no_pep8 -eq 0 ]; then
     run_pep8
   fi
@@ -148,5 +177,6 @@ fi
 
 if [ $coverage -eq 1 ]; then
     echo "Generating coverage report in covhtml/"
-    ${wrapper} coverage html -d covhtml -i
+    ${wrapper} cverage combine
+    ${wrapper} coverage html --include='novaclient/*' --omit='novaclient/openstack/common/*' -d covhtml -i
 fi
diff --git a/setup.cfg b/setup.cfg
index 27d2986511..11c72013c2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,11 +1,3 @@
-[nosetests]
-cover-package = openstackclient
-cover-html = true
-cover-erase = true
-cover-inclusive = true
-verbosity=2
-detailed-errors=1
-
 [build_sphinx]
 source-dir = doc/source
 build-dir = doc/build
diff --git a/setup.py b/setup.py
index df9fefffe7..fe0c2ebdb8 100644
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,6 @@ setuptools.setup(
     install_requires=requires,
     dependency_links=dependency_links,
     cmdclass=setup.get_cmdclass(),
-    test_suite="nose.collector",
     entry_points={
         'console_scripts': ['openstack=openstackclient.shell:main'],
         'openstack.cli': [
diff --git a/tests/utils.py b/tests/utils.py
index 633442c92f..9027472578 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,17 +1,20 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-import time
+import os
 
+import fixtures
 import testtools
 
 
 class TestCase(testtools.TestCase):
-
     def setUp(self):
         super(TestCase, self).setUp()
-        self._original_time = time.time
-        time.time = lambda: 1234
+        if (os.environ.get("OS_STDOUT_NOCAPTURE") == "True" and
+                os.environ.get("OS_STDOUT_NOCAPTURE") == "1"):
+            stdout = self.useFixture(fixtures.StringStream("stdout")).stream
+            self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout))
+        if (os.environ.get("OS_STDERR_NOCAPTURE") == "True" and
+                os.environ.get("OS_STDERR_NOCAPTURE") == "1"):
+            stderr = self.useFixture(fixtures.StringStream("stderr")).stream
+            self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr))
 
     def tearDown(self):
-        time.time = self._original_time
         super(TestCase, self).tearDown()
diff --git a/tools/test-requires b/tools/test-requires
index d96d52b60d..7fb687d25c 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -1,12 +1,11 @@
 distribute>=0.6.24
 
-fixtures
+coverage
+discover
+fixtures>=0.3.12
 mock
-nose
-nose-exclude
-nosexcover
-nosehtmloutput
 openstack.nose_plugin
 pep8==1.1
 sphinx>=1.1.2
-testtools>=0.9.22
+testrepository>=0.0.13
+testtools>=0.9.26
diff --git a/tox.ini b/tox.ini
index f562534181..6b4a96dbdb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,14 +3,12 @@ envlist = py26,py27,pep8
 
 [testenv]
 setenv = VIRTUAL_ENV={envdir}
-         NOSE_WITH_OPENSTACK=1
-         NOSE_OPENSTACK_COLOR=1
-         NOSE_OPENSTACK_RED=0.05
-         NOSE_OPENSTACK_YELLOW=0.025
-         NOSE_OPENSTACK_SHOW_ELAPSED=1
+         LANG=en_US.UTF-8
+         LANGUAGE=en_US:en
+         LC_ALL=C
 deps = -r{toxinidir}/tools/pip-requires
        -r{toxinidir}/tools/test-requires
-commands = nosetests
+commands = python setup.py testr --testr-args='{posargs}'
 
 [testenv:pep8]
 deps = pep8==1.1
@@ -20,27 +18,7 @@ commands = pep8 --repeat --show-source openstackclient setup.py
 commands = {posargs}
 
 [testenv:cover]
-commands = nosetests --cover-erase --cover-package=openstackclient --with-xcoverage
+commands = python setup.py testr --coverage --testr-args='{posargs}'
 
 [tox:jenkins]
 downloadcache = ~/cache/pip
-
-[testenv:jenkins26]
-basepython = python2.6
-setenv = NOSE_WITH_XUNIT=1
-deps = file://{toxinidir}/.cache.bundle
-
-[testenv:jenkins27]
-basepython = python2.7
-setenv = NOSE_WITH_XUNIT=1
-deps = file://{toxinidir}/.cache.bundle
-
-[testenv:jenkinscover]
-deps = file://{toxinidir}/.cache.bundle
-setenv = NOSE_WITH_XUNIT=1
-commands = nosetests --cover-erase --cover-package=openstackclient --with-xcoverage
-
-[testenv:jenkinsvenv]
-deps = file://{toxinidir}/.cache.bundle
-setenv = NOSE_WITH_XUNIT=1
-commands = {posargs}