diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py
index b2a9979f55be7ce45ea2f1ff4d2d3f011bf21eab..ab2a50cee0d9a10cbcefa1e8a75e94afed15d059 100644
--- a/common/djangoapps/course_modes/tests/test_views.py
+++ b/common/djangoapps/course_modes/tests/test_views.py
@@ -13,7 +13,6 @@ import pytz
 from django.conf import settings
 from django.urls import reverse
 from mock import patch
-from nose.plugins.attrib import attr
 
 from course_modes.models import CourseMode, Mode
 from course_modes.tests.factories import CourseModeFactory
@@ -30,13 +29,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
-@attr(shard=5)
 @ddt.ddt
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
 class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTestCase, CourseCatalogServiceMockMixin):
     """
     Course Mode View tests
     """
+    shard = 5
     URLCONF_MODULES = ['course_modes.urls']
 
     @patch.dict(settings.FEATURES, {'MODE_CREATION_FOR_TESTING': True})
diff --git a/common/djangoapps/django_comment_common/tests.py b/common/djangoapps/django_comment_common/tests.py
index db5a9fc656c070b46acdc2ecab605672128a7a6a..13770eece8f53451c6d3fc9c257e61b9461bb031 100644
--- a/common/djangoapps/django_comment_common/tests.py
+++ b/common/djangoapps/django_comment_common/tests.py
@@ -1,11 +1,11 @@
 from django.test import TestCase
-from nose.plugins.attrib import attr
 from opaque_keys.edx.locator import CourseLocator
 from six import text_type
 
 from django_comment_common.models import Role
 from models import CourseDiscussionSettings
 from openedx.core.djangoapps.course_groups.cohorts import CourseCohortsSettings
+from openedx.core.lib.tests import attr
 from student.models import CourseEnrollment, User
 from utils import get_course_discussion_settings, set_course_discussion_settings
 from xmodule.modulestore import ModuleStoreEnum
diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py
index 4bfd820a0ddef49a27c554506fc2476d48cd49fa..6d08f93137e5d61d115dcc53158e5bd9d1c99276 100644
--- a/common/djangoapps/enrollment/tests/test_views.py
+++ b/common/djangoapps/enrollment/tests/test_views.py
@@ -17,7 +17,6 @@ from django.urls import reverse
 from django.test import Client
 from django.test.utils import override_settings
 from mock import patch
-from nose.plugins.attrib import attr
 from rest_framework import status
 from rest_framework.test import APITestCase
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -149,7 +148,6 @@ class EnrollmentTestMixin(object):
         return json.loads(resp.content)
 
 
-@attr(shard=3)
 @override_settings(EDX_API_KEY="i am a key")
 @ddt.ddt
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@@ -157,6 +155,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
     """
     Test user enrollment, especially with different course modes.
     """
+    shard = 3
     USERNAME = "Bob"
     EMAIL = "bob@example.com"
     PASSWORD = "edx"
diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py
index 12ccab74959abc0a4ccf5eb5b939acdf7bf955ba..0574e7e106adf64c15f315917a16095063f8a661 100644
--- a/common/djangoapps/student/tests/test_enrollment.py
+++ b/common/djangoapps/student/tests/test_enrollment.py
@@ -7,7 +7,6 @@ import ddt
 from django.conf import settings
 from django.urls import reverse
 from mock import patch
-from nose.plugins.attrib import attr
 
 from course_modes.models import CourseMode
 from course_modes.tests.factories import CourseModeFactory
@@ -26,7 +25,6 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
-@attr(shard=3)
 @ddt.ddt
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
 class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
@@ -34,6 +32,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
     Test student enrollment, especially with different course modes.
     """
 
+    shard = 3
     USERNAME = "Bob"
     EMAIL = "bob@example.com"
     PASSWORD = "edx"
diff --git a/common/djangoapps/student/tests/test_recent_enrollments.py b/common/djangoapps/student/tests/test_recent_enrollments.py
index 52e85d1924fcf9b8d39a394ca6e5d107ee19aebe..5bb81cda7d7e46533bcf9eef1e23302f07b3b8eb 100644
--- a/common/djangoapps/student/tests/test_recent_enrollments.py
+++ b/common/djangoapps/student/tests/test_recent_enrollments.py
@@ -8,7 +8,6 @@ import ddt
 from django.conf import settings
 from django.urls import reverse
 from django.utils.timezone import now
-from nose.plugins.attrib import attr
 from opaque_keys.edx import locator
 from pytz import UTC
 
@@ -24,13 +23,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
-@attr(shard=3)
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
 @ddt.ddt
 class TestRecentEnrollments(ModuleStoreTestCase, XssTestMixin):
     """
     Unit tests for getting the list of courses for a logged in user
     """
+    shard = 3
     PASSWORD = 'test'
 
     def setUp(self):
diff --git a/common/djangoapps/student/tests/test_verification_status.py b/common/djangoapps/student/tests/test_verification_status.py
index 57da86a9710ccb42fb8800af9c975909e8986ddc..59d39b9e9907f9995d3d0416190dba638bbbe22e 100644
--- a/common/djangoapps/student/tests/test_verification_status.py
+++ b/common/djangoapps/student/tests/test_verification_status.py
@@ -7,7 +7,6 @@ from django.conf import settings
 from django.urls import reverse
 from django.test import override_settings
 from mock import patch
-from nose.plugins.attrib import attr
 from pytz import UTC
 
 from course_modes.tests.factories import CourseModeFactory
@@ -26,13 +25,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
-@attr(shard=3)
 @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
 @ddt.ddt
 class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
     """Tests for per-course verification status on the dashboard. """
 
+    shard = 3
     PAST = 'past'
     FUTURE = 'future'
     DATES = {
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 3255ec723e8e7f4ce470d917d8f6dd6d1f2e091d..3b76b695781ea510680f18b441f47dcbddd6313a 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -17,7 +17,6 @@ from django.test import TestCase, override_settings
 from django.test.client import Client
 from markupsafe import escape
 from mock import Mock, patch
-from nose.plugins.attrib import attr
 from opaque_keys.edx.keys import CourseKey
 from opaque_keys.edx.locations import CourseLocator
 from pyquery import PyQuery as pq
@@ -1063,11 +1062,11 @@ class AnonymousLookupTable(ModuleStoreTestCase):
             self.assertEqual(self.user, user_by_anonymous_id(new_anonymous_id))
 
 
-@attr(shard=3)
 @skip_unless_lms
 @patch('openedx.core.djangoapps.programs.utils.get_programs')
 class RelatedProgramsTests(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
     """Tests verifying that related programs appear on the course dashboard."""
+    shard = 3
     maxDiff = None
     password = 'test'
     related_programs_preface = 'Related Programs'
diff --git a/common/djangoapps/track/views/tests/test_segmentio.py b/common/djangoapps/track/views/tests/test_segmentio.py
index 55a58f18ddaf9970abf6001dc3888351cacbbabf..663070b7ca9b275eeba2d4c9baf10033464c6f9f 100644
--- a/common/djangoapps/track/views/tests/test_segmentio.py
+++ b/common/djangoapps/track/views/tests/test_segmentio.py
@@ -5,7 +5,6 @@ import json
 
 from ddt import ddt, data, unpack
 from mock import sentinel
-from nose.plugins.attrib import attr
 
 from django.contrib.auth.models import User
 from django.test.utils import override_settings
@@ -30,12 +29,12 @@ def expect_failure_with_message(message):
     return test_decorator
 
 
-@attr(shard=3)
 @ddt
 class SegmentIOTrackingTestCase(SegmentIOTrackingTestCaseBase):
     """
     Test processing of Segment events.
     """
+    shard = 3
 
     def test_get_request(self):
         request = self.request_factory.get(SEGMENTIO_TEST_ENDPOINT)
diff --git a/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py b/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py
index 4011fee6bf253e542e87470fa8a8ea66c9be5235..c6ae5e24c4d9719c88b00fd83367af8402dbf8f4 100644
--- a/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py
+++ b/common/lib/capa/capa/safe_exec/tests/test_safe_exec.py
@@ -7,7 +7,7 @@ import random
 import textwrap
 import unittest
 
-from nose.plugins.skip import SkipTest
+import pytest
 from six import text_type
 
 from capa.safe_exec import safe_exec, update_hash
@@ -77,7 +77,7 @@ class TestSafeOrNot(unittest.TestCase):
     def test_cant_do_something_forbidden(self):
         # Can't test for forbiddenness if CodeJail isn't configured for python.
         if not is_configured("python"):
-            raise SkipTest
+            pytest.skip()
 
         g = {}
         with self.assertRaises(SafeExecException) as cm:
diff --git a/common/lib/xmodule/xmodule/modulestore/perf_tests/test_asset_import_export.py b/common/lib/xmodule/xmodule/modulestore/perf_tests/test_asset_import_export.py
index ae29396c467cc503a1fc76ead3e5ee415078518c..087fdcfc2bb517fdc34aed9947ea3b56a2346e9a 100644
--- a/common/lib/xmodule/xmodule/modulestore/perf_tests/test_asset_import_export.py
+++ b/common/lib/xmodule/xmodule/modulestore/perf_tests/test_asset_import_export.py
@@ -9,9 +9,8 @@ from shutil import rmtree
 from bson.code import Code
 import datetime
 import ddt
-#from nose.plugins.attrib import attr
+import pytest
 
-from nose.plugins.skip import SkipTest
 from xmodule.assetstore import AssetMetadata
 from xmodule.modulestore import ModuleStoreEnum
 from xmodule.modulestore.xml_importer import import_course_from_xml
@@ -60,9 +59,6 @@ ASSET_XSD_PATH = PLATFORM_ROOT / "common" / "lib" / "xmodule" / "xmodule" / "ass
 
 
 @ddt.ddt
-# Eventually, exclude this attribute from regular unittests while running *only* tests
-# with this attribute during regular performance tests.
-# @attr("perf_test")
 @unittest.skip
 class CrossStoreXMLRoundtrip(unittest.TestCase):
     """
@@ -89,7 +85,7 @@ class CrossStoreXMLRoundtrip(unittest.TestCase):
         Generate timings for different amounts of asset metadata and different modulestores.
         """
         if CodeBlockTimer is None:
-            raise SkipTest("CodeBlockTimer undefined.")
+            pytest.skip("CodeBlockTimer undefined.")
 
         desc = "XMLRoundTrip:{}->{}:{}".format(
             SHORT_NAME_MAP[source_ms],
@@ -144,9 +140,6 @@ class CrossStoreXMLRoundtrip(unittest.TestCase):
 
 
 @ddt.ddt
-# Eventually, exclude this attribute from regular unittests while running *only* tests
-# with this attribute during regular performance tests.
-# @attr("perf_test")
 @unittest.skip
 class FindAssetTest(unittest.TestCase):
     """
@@ -172,7 +165,7 @@ class FindAssetTest(unittest.TestCase):
         Generate timings for different amounts of asset metadata and different modulestores.
         """
         if CodeBlockTimer is None:
-            raise SkipTest("CodeBlockTimer undefined.")
+            pytest.skip("CodeBlockTimer undefined.")
 
         desc = "FindAssetTest:{}:{}".format(
             SHORT_NAME_MAP[source_ms],
@@ -227,9 +220,6 @@ class FindAssetTest(unittest.TestCase):
 
 
 @ddt.ddt
-# Eventually, exclude this attribute from regular unittests while running *only* tests
-# with this attribute during regular performance tests.
-# @attr("perf_test")
 @unittest.skip
 class TestModulestoreAssetSize(unittest.TestCase):
     """
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
index 624c598f175e4307c415f7538dbc51ea0315c2ee..162928a56cbe8e9824309afd4932c6f64616d4ce 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
@@ -5,12 +5,13 @@ too.
 from datetime import datetime, timedelta
 import ddt
 from django.test import TestCase
-from nose.plugins.attrib import attr
 import pytz
 import unittest
 
 from opaque_keys.edx.keys import CourseKey
 from opaque_keys.edx.locator import CourseLocator
+
+from openedx.core.lib.tests import attr
 from xmodule.assetstore import AssetMetadata
 from xmodule.modulestore import ModuleStoreEnum, SortedAssetList, IncorrectlySortedList
 from xmodule.modulestore.exceptions import ItemNotFoundError
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py b/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
index 2ca9a906a7af9d2738122952f8ce8e5fd77642f8..e8e49f0b5329022c474a682853a679098545fb09 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
@@ -19,9 +19,9 @@ from shutil import rmtree
 from tempfile import mkdtemp
 
 import ddt
-from nose.plugins.attrib import attr
 from mock import patch
 
+from openedx.core.lib.tests import attr
 from xmodule.tests import CourseComparisonTest
 from xmodule.modulestore.xml_importer import import_course_from_xml
 from xmodule.modulestore.xml_exporter import export_course_to_xml
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
index c5fffac093de0a621cbfd748a431cf2379a61de3..06cd3fe0cd63507e5ab815050c5fea5c4539a7bd 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
@@ -15,8 +15,6 @@ from mock import patch, Mock, call
 # before importing the module
 # TODO remove this import and the configuration -- xmodule should not depend on django!
 from django.conf import settings
-# This import breaks this test file when run separately. Needs to be fixed! (PLAT-449)
-from nose.plugins.attrib import attr
 from nose import SkipTest
 import pymongo
 from pytz import UTC
@@ -42,6 +40,7 @@ if not settings.configured:
 
 from opaque_keys.edx.keys import CourseKey
 from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryLocator
+from openedx.core.lib.tests import attr
 from xmodule.exceptions import InvalidVersionError
 from xmodule.modulestore import ModuleStoreEnum
 from xmodule.modulestore.draft_and_published import UnsupportedRevisionError, DIRECT_ONLY_CATEGORIES
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py b/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py
index d990e32b2360f093737b276bf5e8ff34836d8e87..a2af14c60cce9b91bce662268320ac7de8055978 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py
@@ -9,10 +9,10 @@ import unittest
 import uuid
 import xml.etree.ElementTree as ET
 from contextlib import contextmanager
-from nose.plugins.attrib import attr
 from shutil import rmtree
 from tempfile import mkdtemp
 
+from openedx.core.lib.tests import attr
 from xmodule.exceptions import InvalidVersionError
 from xmodule.modulestore import ModuleStoreEnum
 from xmodule.modulestore.exceptions import ItemNotFoundError
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_migrator.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_migrator.py
index b40ab669769cd36628da5e127a4a89fb2938f57a..f683612591efef91fb586bcdf704b41f9c39fbd0 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_migrator.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_migrator.py
@@ -6,8 +6,8 @@ import random
 import uuid
 
 import mock
-from nose.plugins.attrib import attr
 
+from openedx.core.lib.tests import attr
 from xblock.fields import Reference, ReferenceList, ReferenceValueDict, UNIQUE_ID
 from xmodule.modulestore.split_migrator import SplitMigrator
 from xmodule.modulestore.tests.test_split_w_old_mongo import SplitWMongoCourseBootstrapper
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
index 6dbef326a43aeb29c4cc420e9864396ba83bb14f..94a5d7fd789b4a00009904ec686bf5415eb36926 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
@@ -12,10 +12,10 @@ import uuid
 
 import ddt
 from contracts import contract
-from nose.plugins.attrib import attr
 from django.core.cache import caches, InvalidCacheBackendError
 
 from openedx.core.lib import tempdir
+from openedx.core.lib.tests import attr
 from xblock.fields import Reference, ReferenceList, ReferenceValueDict
 from xmodule.course_module import CourseDescriptor
 from xmodule.modulestore import ModuleStoreEnum
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_w_old_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_w_old_mongo.py
index a9e92b121193c91cb0ca4b96a0cf3e062387e7d8..97881b6aaf18324a73ed171290281138fb6b825b 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_w_old_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_w_old_mongo.py
@@ -3,8 +3,8 @@ import random
 import unittest
 import uuid
 
-from nose.plugins.attrib import attr
 import mock
+import pytest
 
 from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
 from xmodule.modulestore import ModuleStoreEnum
@@ -16,7 +16,7 @@ from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOS
 from xmodule.modulestore.tests.utils import MemoryCache
 
 
-@attr('mongo')
+@pytest.mark.mongo
 class SplitWMongoCourseBootstrapper(unittest.TestCase):
     """
     Helper for tests which need to construct split mongo & old mongo based courses to get interesting internal structure.
diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py
index 847f2fc8c2288c56d3213af351de4548c52f229c..5ad3f9a79371e7388ca2a6b6b69c86aa108276e2 100644
--- a/common/test/acceptance/tests/discussion/test_cohort_management.py
+++ b/common/test/acceptance/tests/discussion/test_cohort_management.py
@@ -9,7 +9,6 @@ from datetime import datetime
 
 import unicodecsv
 from bok_choy.promise import EmptyPromise
-from nose.plugins.attrib import attr
 from pytz import UTC, utc
 
 from common.test.acceptance.fixtures.course import CourseFixture
@@ -18,6 +17,7 @@ from common.test.acceptance.pages.lms.instructor_dashboard import DataDownloadPa
 from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage
 from common.test.acceptance.tests.discussion.helpers import CohortTestMixin
 from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest, create_user_partition_json
+from openedx.core.lib.tests import attr
 from xmodule.partitions.partitions import Group
 
 
diff --git a/common/test/acceptance/tests/discussion/test_cohorts.py b/common/test/acceptance/tests/discussion/test_cohorts.py
index 891523fad38f4aa5f25d2013e97db1bd3295fde1..3d98f56fdb62cf08843a3998dfdfc75f07c40441 100644
--- a/common/test/acceptance/tests/discussion/test_cohorts.py
+++ b/common/test/acceptance/tests/discussion/test_cohorts.py
@@ -3,14 +3,13 @@ Tests related to the cohorting feature.
 """
 from uuid import uuid4
 
-from nose.plugins.attrib import attr
-
 from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
 from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
 from common.test.acceptance.pages.lms.courseware import CoursewarePage
 from common.test.acceptance.pages.lms.discussion import DiscussionTabSingleThreadPage, InlineDiscussionPage
 from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, BaseDiscussionTestCase, CohortTestMixin
 from common.test.acceptance.tests.helpers import UniqueCourseTest
+from openedx.core.lib.tests import attr
 
 
 class NonCohortedDiscussionTestMixin(BaseDiscussionMixin):
diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py
index 6805150e3a69d41a955f8961078ff7dbc03341d3..1ab253385cf33683948c3cb27d306c3eb8221767 100644
--- a/common/test/acceptance/tests/discussion/test_discussion.py
+++ b/common/test/acceptance/tests/discussion/test_discussion.py
@@ -6,7 +6,6 @@ import datetime
 from unittest import skip
 from uuid import uuid4
 
-from nose.plugins.attrib import attr
 from nose.tools import nottest
 from pytz import UTC
 
@@ -33,6 +32,7 @@ from common.test.acceptance.pages.lms.learner_profile import LearnerProfilePage
 from common.test.acceptance.pages.lms.tab_nav import TabNavPage
 from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, BaseDiscussionTestCase
 from common.test.acceptance.tests.helpers import UniqueCourseTest, get_modal_alert, skip_if_browser
+from openedx.core.lib.tests import attr
 
 
 THREAD_CONTENT_WITH_LATEX = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
diff --git a/common/test/acceptance/tests/discussion/test_discussion_management.py b/common/test/acceptance/tests/discussion/test_discussion_management.py
index 4775556659fac154d22715836daf5eb98a90db5f..c9abd58049d95ce9c7a98ed1e7a9bc44a6eb7fe7 100644
--- a/common/test/acceptance/tests/discussion/test_discussion_management.py
+++ b/common/test/acceptance/tests/discussion/test_discussion_management.py
@@ -5,8 +5,6 @@ End-to-end tests related to the divided discussion management on the LMS Instruc
 
 import uuid
 
-from nose.plugins.attrib import attr
-
 from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
 from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
 from common.test.acceptance.pages.common.utils import add_enrollment_course_modes
@@ -14,6 +12,7 @@ from common.test.acceptance.pages.lms.discussion import DiscussionTabSingleThrea
 from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage
 from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, CohortTestMixin
 from common.test.acceptance.tests.helpers import UniqueCourseTest
+from openedx.core.lib.tests import attr
 
 
 class BaseDividedDiscussionTest(UniqueCourseTest, CohortTestMixin):
diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py
index fb943f74660b9ebedabe62a927b894aee4424873..9f0b3b52bbc2302e54cf0ae9fc0e98be20fbb65c 100644
--- a/common/test/acceptance/tests/lms/test_lms.py
+++ b/common/test/acceptance/tests/lms/test_lms.py
@@ -7,7 +7,6 @@ from datetime import datetime, timedelta
 from textwrap import dedent
 
 import pytz
-from nose.plugins.attrib import attr
 
 from common.test.acceptance.fixtures.course import CourseFixture, CourseUpdateDesc, XBlockFixtureDesc
 from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
@@ -40,6 +39,7 @@ from common.test.acceptance.tests.helpers import (
     load_data_str,
     select_option_by_text,
 )
+from openedx.core.lib.tests import attr
 
 
 @attr(shard=19)
diff --git a/common/test/acceptance/tests/lms/test_lms_course_home.py b/common/test/acceptance/tests/lms/test_lms_course_home.py
index 56086080b284aac406314a1b69bc425c4e507a39..85d95c327f0ca01ad3f26d8665927f0c2e56db29 100644
--- a/common/test/acceptance/tests/lms/test_lms_course_home.py
+++ b/common/test/acceptance/tests/lms/test_lms_course_home.py
@@ -3,9 +3,8 @@
 End-to-end tests for the LMS that utilize the course home page and course outline.
 """
 
-from nose.plugins.attrib import attr
-
 from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
+from openedx.core.lib.tests import attr
 
 from ...fixtures.course import CourseFixture, XBlockFixtureDesc
 from ...pages.lms.bookmarks import BookmarksPage
diff --git a/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py b/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py
index d91d31f260d5fddf1f016a3d16f69d28b92c0db9..754b227fb45d6e94c5438bc87d27622f68c013c3 100644
--- a/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py
+++ b/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py
@@ -4,8 +4,6 @@ Test courseware search
 
 import json
 
-from nose.plugins.attrib import attr
-
 from common.test.acceptance.fixtures.course import XBlockFixtureDesc
 from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
 from common.test.acceptance.pages.common.logout import LogoutPage
@@ -16,11 +14,11 @@ from common.test.acceptance.tests.studio.base_studio_test import ContainerBase
 from xmodule.partitions.partitions import Group
 
 
-@attr(shard=1)
 class SplitTestCoursewareSearchTest(ContainerBase):
     """
     Test courseware search on Split Test Module.
     """
+    shard = 1
     USERNAME = 'STUDENT_TESTER'
     EMAIL = 'student101@example.com'
 
diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py
index e33a47527bf6ca40b537c21ba9e3f4e96518ca53..169a3a10d15e6f5d669219534620fae5afc33777 100644
--- a/common/test/acceptance/tests/lms/test_problem_types.py
+++ b/common/test/acceptance/tests/lms/test_problem_types.py
@@ -9,7 +9,6 @@ from abc import ABCMeta, abstractmethod
 
 import ddt
 from nose import SkipTest
-from nose.plugins.attrib import attr
 from selenium.webdriver import ActionChains
 
 from capa.tests.response_xml_factory import (
@@ -31,6 +30,7 @@ from common.test.acceptance.fixtures.course import XBlockFixtureDesc
 from common.test.acceptance.pages.lms.problem import ProblemPage
 from common.test.acceptance.tests.helpers import EventsTestMixin, select_option_by_text
 from common.test.acceptance.tests.lms.test_lms_problems import ProblemsTest
+from openedx.core.lib.tests import attr
 
 
 class ProblemTypeTestBaseMeta(ABCMeta):
diff --git a/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_generate_course_overview.py b/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_generate_course_overview.py
index 8988e821f8ddd2fbe022d921cce20f8eff370986..271b7bbb22f53d2c4168dddb166085ec32e0b7ad 100644
--- a/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_generate_course_overview.py
+++ b/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_generate_course_overview.py
@@ -3,7 +3,6 @@ Tests that the generate_course_overview management command actually generates co
 """
 from django.core.management.base import CommandError
 from mock import patch
-from nose.plugins.attrib import attr
 
 from openedx.core.djangoapps.content.course_overviews.management.commands import generate_course_overview
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
@@ -12,11 +11,12 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
-@attr(shard=2)
 class TestGenerateCourseOverview(ModuleStoreTestCase):
     """
     Tests course overview management command.
     """
+    shard = 2
+
     def setUp(self):
         """
         Create courses in modulestore.
diff --git a/openedx/core/djangoapps/course_groups/management/commands/tests/test_post_cohort_membership_fix.py b/openedx/core/djangoapps/course_groups/management/commands/tests/test_post_cohort_membership_fix.py
index 20b4db84e2dbd95eba9fce637c40d07aa335617b..8049bfba3425e8d8d2f996e982daa2258fe3b165 100644
--- a/openedx/core/djangoapps/course_groups/management/commands/tests/test_post_cohort_membership_fix.py
+++ b/openedx/core/djangoapps/course_groups/management/commands/tests/test_post_cohort_membership_fix.py
@@ -3,7 +3,6 @@ Test for the post-migration fix commands that are included with this djangoapp
 """
 from django.core.management import call_command
 from django.test.client import RequestFactory
-from nose.plugins.attrib import attr
 
 from openedx.core.djangoapps.course_groups.views import cohort_handler
 from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_name
@@ -14,11 +13,12 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
-@attr(shard=2)
 class TestPostMigrationFix(ModuleStoreTestCase):
     """
     Base class for testing post-migration fix commands
     """
+    shard = 2
+
     def setUp(self):
         """
         setup course, user and request for tests
diff --git a/openedx/core/lib/tests/__init__.py b/openedx/core/lib/tests/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a79e53e7ef7ab95668b36ec14e902eef53a0aeec 100644
--- a/openedx/core/lib/tests/__init__.py
+++ b/openedx/core/lib/tests/__init__.py
@@ -0,0 +1,23 @@
+"""
+Utility functions for the edx-platform test suite.
+"""
+
+from __future__ import absolute_import
+
+
+def attr(*args, **kwargs):
+    """
+    Set the given attributes on the decorated test class, function or method.
+    Replacement for nose.plugins.attrib.attr, used with pytest-attrib to
+    run tests with particular attributes.
+    """
+    def decorator(test):
+        """
+        Apply the decorator's arguments as arguments to the given test.
+        """
+        for arg in args:
+            setattr(test, arg, True)
+        for key in kwargs:
+            setattr(test, key, kwargs[key])
+        return test
+    return decorator