diff --git a/common/test/acceptance/.coveragerc b/common/test/acceptance/.coveragerc
index 975bb71929747115c0fd9a5866e7f9f76db946c3..9d19a5b09ada6857b10b85d103d03183241d0a0c 100644
--- a/common/test/acceptance/.coveragerc
+++ b/common/test/acceptance/.coveragerc
@@ -10,21 +10,32 @@ source =
 omit =
     lms/envs/*
     cms/envs/*
+    cms/manage.py
+    cms/djangoapps/contentstore/views/dev.py
     common/djangoapps/terrain/*
     common/djangoapps/*/migrations/*
+    openedx/core/djangoapps/debug/*
     openedx/core/djangoapps/*/migrations/*
     */test*
     */management/*
     */urls*
     */wsgi*
+    lms/debug/*
+    lms/djangoapps/*/features/*
     lms/djangoapps/*/migrations/*
+    cms/djangoapps/*/features/*
     cms/djangoapps/*/migrations/*
 
+concurrency = multiprocessing
 parallel = True
 
 [report]
 ignore_errors = True
 
+exclude_lines =
+   pragma: no cover
+   raise NotImplementedError
+
 [html]
 title = Bok Choy Test Coverage Report
 directory = reports/bok_choy/cover
diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py
index cd8648a74df7b43cfb9de573e4471851ee71a304..cda7583a94660cc9938603d93c2022a612f56e26 100644
--- a/common/test/acceptance/tests/discussion/test_discussion.py
+++ b/common/test/acceptance/tests/discussion/test_discussion.py
@@ -1226,6 +1226,7 @@ class DiscussionUserProfileTest(UniqueCourseTest):
         self.profiled_user_id = self.setup_user(username=self.PROFILED_USERNAME)
         # now create a second user who will view the profile.
         self.user_id = self.setup_user()
+        UserProfileViewFixture([]).push()
 
     def setup_course(self):
         """
diff --git a/docs/testing.rst b/docs/testing.rst
index f03ac03e29ac8a3d2fc159a8b394194ad4864f52..a97c42d960182315dff8207a4b157e4097e3d674 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -437,14 +437,14 @@ To test only a certain feature, specify the file and the testcase class.
 
 ::
 
-    paver test_bokchoy -t studio/test_studio_bad_data.py:BadComponentTest
+    paver test_bokchoy -t studio/test_studio_bad_data.py::BadComponentTest
 
 To execute only a certain test case, specify the file name, class, and
 test case method.
 
 ::
 
-    paver test_bokchoy -t lms/test_lms.py:RegistrationTest.test_register
+    paver test_bokchoy -t lms/test_lms.py::RegistrationTest::test_register
 
 During acceptance test execution, log files and also screenshots of
 failed tests are captured in test\_root/log.
@@ -454,7 +454,7 @@ If you check this in, your tests will hang on jenkins.
 
 ::
 
-    from nose.tools import set_trace; set_trace()
+    import pdb; pdb.set_trace()
 
 By default, all bokchoy tests are run with the 'split' ModuleStore. To
 override the modulestore that is used, use the default\_store option.
@@ -506,7 +506,7 @@ relative to the ``common/test/acceptance/tests`` directory. This is an example f
 
 ::
 
-    paver test_a11y -t lms/test_lms_dashboard.py:LmsDashboardA11yTest.test_dashboard_course_listings_a11y
+    paver test_a11y -t lms/test_lms_dashboard.py::LmsDashboardA11yTest::test_dashboard_course_listings_a11y
 
 **Coverage**:
 
@@ -644,7 +644,7 @@ Running Tests on Paver Scripts
 
 To run tests on the scripts that power the various Paver commands, use the following command::
 
-  nosetests paver
+  nosetests pavelib
 
 
 Testing internationalization with dummy translations
diff --git a/pavelib/paver_tests/test_paver_bok_choy_cmds.py b/pavelib/paver_tests/test_paver_bok_choy_cmds.py
index fdd2a4263e2830ab32c2c836f8d27bd429546a15..c04534ad0a36635f1a35d3ccb0855d7f8248e847 100644
--- a/pavelib/paver_tests/test_paver_bok_choy_cmds.py
+++ b/pavelib/paver_tests/test_paver_bok_choy_cmds.py
@@ -11,6 +11,7 @@ import ddt
 from mock import Mock, call, patch
 from paver.easy import BuildFailure, call_task, environment
 
+from pavelib.utils.envs import Env
 from pavelib.utils.test.suites import BokChoyTestSuite, Pa11yCrawler
 from pavelib.utils.test.suites.bokchoy_suite import DEMO_COURSE_IMPORT_DIR, DEMO_COURSE_TAR_GZ
 
@@ -40,10 +41,15 @@ class TestPaverBokChoyCmd(unittest.TestCase):
             ),
             "SELENIUM_DRIVER_LOG_DIR='{}/test_root/log{}'".format(REPO_DIR, shard_str),
             "VERIFY_XSS='{}'".format(verify_xss),
-            "nosetests",
+            "coverage",
+            "run",
+            "--rcfile={}".format(Env.BOK_CHOY_COVERAGERC),
+            "-m",
+            "pytest",
             "{}/common/test/acceptance/{}".format(REPO_DIR, name),
-            "--xunit-file={}/reports/bok_choy{}/xunit.xml".format(REPO_DIR, shard_str),
-            "--verbosity=2",
+            "--durations=20",
+            "--junitxml={}/reports/bok_choy{}/xunit.xml".format(REPO_DIR, shard_str),
+            "--verbose",
         ]
         return expected_statement
 
@@ -122,11 +128,11 @@ class TestPaverBokChoyCmd(unittest.TestCase):
         Using 1 process means paver should ask for the traditional xunit plugin for plugin results
         """
         expected_verbosity_command = [
-            "--xunit-file={repo_dir}/reports/bok_choy{shard_str}/xunit.xml".format(
+            "--junitxml={repo_dir}/reports/bok_choy{shard_str}/xunit.xml".format(
                 repo_dir=REPO_DIR,
                 shard_str='/shard_' + self.shard if self.shard else ''
             ),
-            "--verbosity=2",
+            "--verbose",
         ]
         suite = BokChoyTestSuite('', num_processes=1)
         self.assertEqual(suite.verbosity_processes_command, expected_verbosity_command)
@@ -138,13 +144,13 @@ class TestPaverBokChoyCmd(unittest.TestCase):
         """
         process_count = 2
         expected_verbosity_command = [
-            "--xunitmp-file={repo_dir}/reports/bok_choy{shard_str}/xunit.xml".format(
+            "--junitxml={repo_dir}/reports/bok_choy{shard_str}/xunit.xml".format(
                 repo_dir=REPO_DIR,
                 shard_str='/shard_' + self.shard if self.shard else '',
             ),
-            "--processes={}".format(process_count),
-            "--no-color",
-            "--process-timeout=1200",
+            "-n {}".format(process_count),
+            "--color=no",
+            "--verbose",
         ]
         suite = BokChoyTestSuite('', num_processes=process_count)
         self.assertEqual(suite.verbosity_processes_command, expected_verbosity_command)
@@ -155,27 +161,17 @@ class TestPaverBokChoyCmd(unittest.TestCase):
         """
         process_count = 3
         expected_verbosity_command = [
-            "--xunitmp-file={repo_dir}/reports/bok_choy{shard_str}/xunit.xml".format(
+            "--junitxml={repo_dir}/reports/bok_choy{shard_str}/xunit.xml".format(
                 repo_dir=REPO_DIR,
                 shard_str='/shard_' + self.shard if self.shard else '',
             ),
-            "--processes={}".format(process_count),
-            "--no-color",
-            "--process-timeout=1200",
+            "-n {}".format(process_count),
+            "--color=no",
+            "--verbose",
         ]
         suite = BokChoyTestSuite('', num_processes=process_count)
         self.assertEqual(suite.verbosity_processes_command, expected_verbosity_command)
 
-    def test_invalid_verbosity_and_processes(self):
-        """
-        If an invalid combination of verbosity and number of processors is passed in, a
-        BuildFailure should be raised
-        """
-        suite = BokChoyTestSuite('', num_processes=2, verbosity=3)
-        with self.assertRaises(BuildFailure):
-            # pylint: disable=pointless-statement
-            suite.verbosity_processes_command
-
 
 @ddt.ddt
 class TestPaverPa11yCrawlerCmd(unittest.TestCase):
diff --git a/pavelib/paver_tests/test_utils.py b/pavelib/paver_tests/test_utils.py
index 65bdc966a651af578e86315312111ee77c62eaff..cccd922f74cba283949184e476e71fca0e99f51b 100644
--- a/pavelib/paver_tests/test_utils.py
+++ b/pavelib/paver_tests/test_utils.py
@@ -6,9 +6,11 @@ import unittest
 
 from mock import patch
 
+from pavelib.utils.envs import Env
 from pavelib.utils.test.utils import MINIMUM_FIREFOX_VERSION, check_firefox_version
 
 
+@unittest.skipIf(Env.USING_DOCKER, 'Firefox version check works differently under Docker Devstack')
 class TestUtils(unittest.TestCase):
     """
     Test utils.py under pavelib/utils/test
diff --git a/pavelib/utils/test/bokchoy_options.py b/pavelib/utils/test/bokchoy_options.py
index 8631da945f792e66e04e3af0e9e5e41e0ebff328..48ec319a9434615c3f16d12a59a9e6fa996d33c9 100644
--- a/pavelib/utils/test/bokchoy_options.py
+++ b/pavelib/utils/test/bokchoy_options.py
@@ -19,8 +19,16 @@ BOKCHOY_DEFAULT_STORE_DEPR = make_option(
     default=os.environ.get('DEFAULT_STORE', 'split'),
     help='deprecated in favor of default-store'
 )
-BOKCHOY_FASTTEST = make_option('-a', '--fasttest', action='store_true', help='Skip some setup')
-BOKCHOY_COVERAGERC = make_option('--coveragerc', help='coveragerc file to use during this test')
+BOKCHOY_EVAL_ATTR = make_option(
+    "-a", "--eval-attr",
+    dest="eval_attr", help="Only run tests matching given attribute expression."
+)
+BOKCHOY_FASTTEST = make_option('--fasttest', action='store_true', help='Skip some setup')
+BOKCHOY_COVERAGERC = make_option(
+    '--coveragerc',
+    default=Env.BOK_CHOY_COVERAGERC,
+    help='coveragerc file to use during this test'
+)
 
 BOKCHOY_OPTS = [
     ('test-spec=', 't', 'Specific test to run'),
@@ -28,7 +36,9 @@ BOKCHOY_OPTS = [
     ('skip-clean', 'C', 'Skip cleaning repository before running tests'),
     make_option('-r', '--serversonly', action='store_true', help='Prepare suite and leave servers running'),
     make_option('-o', '--testsonly', action='store_true', help='Assume servers are running and execute tests only'),
+    BOKCHOY_COVERAGERC,
     BOKCHOY_DEFAULT_STORE,
+    BOKCHOY_EVAL_ATTR,
     make_option(
         '-d', '--test-dir',
         default='tests',
diff --git a/pavelib/utils/test/suites/bokchoy_suite.py b/pavelib/utils/test/suites/bokchoy_suite.py
index 6c05afe93c141ba77fd6637fd10f2516ebf69f7e..453e4dc19f93d193cb24d17c7e1858acc6ccacc6 100644
--- a/pavelib/utils/test/suites/bokchoy_suite.py
+++ b/pavelib/utils/test/suites/bokchoy_suite.py
@@ -179,10 +179,11 @@ class BokChoyTestSuite(TestSuite):
       testsonly - assume servers are running (as per above) and run tests with no setup or cleaning of environment
       test_spec - when set, specifies test files, classes, cases, etc. See platform doc.
       default_store - modulestore to use when running tests (split or draft)
+      eval_attr - only run tests matching given attribute expression
       num_processes - number of processes or threads to use in tests. Recommendation is that this
       is less than or equal to the number of available processors.
       verify_xss - when set, check for XSS vulnerabilities in the page HTML.
-      See nosetest documentation: http://nose.readthedocs.org/en/latest/usage.html
+      See pytest documentation: https://docs.pytest.org/en/latest/
     """
     def __init__(self, *args, **kwargs):
         super(BokChoyTestSuite, self).__init__(*args, **kwargs)
@@ -196,6 +197,7 @@ class BokChoyTestSuite(TestSuite):
         self.testsonly = kwargs.get('testsonly', False)
         self.test_spec = kwargs.get('test_spec', None)
         self.default_store = kwargs.get('default_store', None)
+        self.eval_attr = kwargs.get('eval_attr', None)
         self.verbosity = kwargs.get('verbosity', DEFAULT_VERBOSITY)
         self.num_processes = kwargs.get('num_processes', DEFAULT_NUM_PROCESSES)
         self.verify_xss = kwargs.get('verify_xss', os.environ.get('VERIFY_XSS', True))
@@ -203,7 +205,7 @@ class BokChoyTestSuite(TestSuite):
         self.har_dir = self.log_dir / 'hars'
         self.a11y_file = Env.BOK_CHOY_A11Y_CUSTOM_RULES_FILE
         self.imports_dir = kwargs.get('imports_dir', None)
-        self.coveragerc = kwargs.get('coveragerc', None)
+        self.coveragerc = kwargs.get('coveragerc', Env.BOK_CHOY_COVERAGERC)
         self.save_screenshots = kwargs.get('save_screenshots', False)
 
     def __enter__(self):
@@ -269,29 +271,22 @@ class BokChoyTestSuite(TestSuite):
     @property
     def verbosity_processes_command(self):
         """
-        Multiprocessing, xunit, color, and verbosity do not work well together. We need to construct
-        the proper combination for use with nosetests.
+        Construct the proper combination of multiprocessing, XUnit XML file, color, and verbosity for use with pytest.
         """
-        command = []
-
-        if self.verbosity != DEFAULT_VERBOSITY and self.num_processes != DEFAULT_NUM_PROCESSES:
-            msg = 'Cannot pass in both num_processors and verbosity. Quitting'
-            raise BuildFailure(msg)
+        command = ["--junitxml={}".format(self.xunit_report)]
 
         if self.num_processes != 1:
-            # Construct "multiprocess" nosetest command
-            command = [
-                "--xunitmp-file={}".format(self.xunit_report),
-                "--processes={}".format(self.num_processes),
-                "--no-color",
-                "--process-timeout=1200",
-            ]
-
-        else:
-            command = [
-                "--xunit-file={}".format(self.xunit_report),
-                "--verbosity={}".format(self.verbosity),
+            # Construct "multiprocess" pytest command
+            command += [
+                "-n {}".format(self.num_processes),
+                "--color=no",
             ]
+        if self.verbosity < 1:
+            command.append("--quiet")
+        elif self.verbosity > 1:
+            command.append("--verbose")
+        if self.eval_attr:
+            command.append("-a '{}'".format(self.eval_attr))
 
         return command
 
@@ -300,7 +295,7 @@ class BokChoyTestSuite(TestSuite):
         Infinite loop. Servers will continue to run in the current session unless interrupted.
         """
         print 'Bok-choy servers running. Press Ctrl-C to exit...\n'
-        print 'Note: pressing Ctrl-C multiple times can corrupt noseid files and system state. Just press it once.\n'
+        print 'Note: pressing Ctrl-C multiple times can corrupt system state. Just press it once.\n'
 
         while True:
             try:
@@ -312,7 +307,7 @@ class BokChoyTestSuite(TestSuite):
     @property
     def cmd(self):
         """
-        This method composes the nosetests command to send to the terminal. If nosetests aren't being run,
+        This method composes the pytest command to send to the terminal. If pytest isn't being run,
          the command returns None.
         """
         # Default to running all tests if no specific test is specified
@@ -321,12 +316,12 @@ class BokChoyTestSuite(TestSuite):
         else:
             test_spec = self.test_dir / self.test_spec
 
-        # Skip any additional commands (such as nosetests) if running in
+        # Skip any additional commands (such as pytest) if running in
         # servers only mode
         if self.serversonly:
             return None
 
-        # Construct the nosetests command, specifying where to save
+        # Construct the pytest command, specifying where to save
         # screenshots and XUnit XML reports
         cmd = [
             "DEFAULT_STORE={}".format(self.default_store),
@@ -335,11 +330,21 @@ class BokChoyTestSuite(TestSuite):
             "BOKCHOY_A11Y_CUSTOM_RULES_FILE='{}'".format(self.a11y_file),
             "SELENIUM_DRIVER_LOG_DIR='{}'".format(self.log_dir),
             "VERIFY_XSS='{}'".format(self.verify_xss),
-            "nosetests",
+        ]
+        if self.save_screenshots:
+            cmd.append("NEEDLE_SAVE_BASELINE=True")
+        cmd += [
+            "coverage",
+            "run",
+        ]
+        if self.coveragerc:
+            cmd.append("--rcfile={}".format(self.coveragerc))
+        cmd += [
+            "-m",
+            "pytest",
             test_spec,
+            "--durations=20",
         ] + self.verbosity_processes_command
-        if self.save_screenshots:
-            cmd.append("--with-save-baseline")
         if self.extra_args:
             cmd.append(self.extra_args)
         cmd.extend(self.passthrough_options)
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 248bdd8c261f67edff5d7fc346e6825b7cc658a6..767d019b8bda47109e646b84f4966f3a2a5e24cd 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -9,4 +9,11 @@
 #   * @edx/testeng - to discuss it's impact on test infrastructure
 #   * @edx/devops - to check system requirements
 
+execnet==1.4.1
+py==1.4.34
 pysqlite==2.8.3
+pytest==3.1.3
+pytest-attrib==0.1.3
+pytest-catchlog==1.2.2
+pytest-django==3.1.2
+pytest-xdist==1.18.1
diff --git a/scripts/accessibility-tests.sh b/scripts/accessibility-tests.sh
index 6597cb21fc49ae90aff5356d77f87be33656c9cb..41dbb8e0ca3cdde4b3297311878f53f7f55f6531 100755
--- a/scripts/accessibility-tests.sh
+++ b/scripts/accessibility-tests.sh
@@ -6,7 +6,7 @@ echo "Setting up for accessibility tests..."
 source scripts/jenkins-common.sh
 
 echo "Running explicit accessibility tests..."
-SELENIUM_BROWSER=phantomjs paver test_a11y --with-xunitmp
+SELENIUM_BROWSER=phantomjs paver test_a11y
 
 echo "Generating coverage report..."
 paver a11y_coverage
diff --git a/scripts/generic-ci-tests.sh b/scripts/generic-ci-tests.sh
index 392d8374926fb397ee0f71177ab5a59da52ea22a..ea87a18ebd2b12034d83f429afc88523e939969a 100755
--- a/scripts/generic-ci-tests.sh
+++ b/scripts/generic-ci-tests.sh
@@ -162,7 +162,7 @@ case "$TEST_SUITE" in
 
     "bok-choy")
 
-        PAVER_ARGS="-n $NUMBER_OF_BOKCHOY_THREADS --with-flaky --with-xunit"
+        PAVER_ARGS="-n $NUMBER_OF_BOKCHOY_THREADS"
 
         case "$SHARD" in
 
@@ -171,11 +171,11 @@ case "$TEST_SUITE" in
                 ;;
 
             [1-9]|10)
-                paver test_bokchoy --attr="shard=$SHARD" $PAVER_ARGS
+                paver test_bokchoy --eval-attr="shard==$SHARD" $PAVER_ARGS
                 ;;
 
             11|"noshard")
-                paver test_bokchoy --attr='!shard,a11y=False' $PAVER_ARGS
+                paver test_bokchoy --eval-attr='not shard and not a11y' $PAVER_ARGS
                 ;;
 
             # Default case because if we later define another bok-choy shard on Jenkins
@@ -190,7 +190,7 @@ case "$TEST_SUITE" in
                 # May be unnecessary if we changed the "Skip if there are no test files"
                 # option to True in the jenkins job definitions.
                 mkdir -p reports/bok_choy
-                emptyxunit "bok_choy/nosetests"
+                emptyxunit "bok_choy/xunit"
                 ;;
         esac
         ;;
diff --git a/setup.cfg b/setup.cfg
index 673bf03b017eacfb82302ab3ccafde56dd5fdad6..9123e9f69a64066c5aede8c584058f66631c8902 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,6 +14,9 @@ process-timeout=300
 #nocapture=1
 #pdb=1
 
+[tool:pytest]
+norecursedirs = .git conf node_modules test_root cms/envs lms/envs
+
 [pep8]
 # error codes: http://pep8.readthedocs.org/en/latest/intro.html#error-codes
 # E501: line too long