From 42412491192fe498de0a44769ac6c1f0a136b3cb Mon Sep 17 00:00:00 2001
From: Jeremy Bowman <jbowman@edx.org>
Date: Mon, 30 Jul 2018 17:16:38 -0400
Subject: [PATCH] TE-2524 Stop using nose.plugins

---
 .../course_modes/tests/test_views.py          |  3 +--
 .../djangoapps/django_comment_common/tests.py |  2 +-
 .../djangoapps/enrollment/tests/test_views.py |  3 +--
 .../student/tests/test_enrollment.py          |  3 +--
 .../student/tests/test_recent_enrollments.py  |  3 +--
 .../student/tests/test_verification_status.py |  3 +--
 common/djangoapps/student/tests/tests.py      |  3 +--
 .../track/views/tests/test_segmentio.py       |  3 +--
 .../capa/safe_exec/tests/test_safe_exec.py    |  4 ++--
 .../perf_tests/test_asset_import_export.py    | 16 +++----------
 .../modulestore/tests/test_assetstore.py      |  3 ++-
 .../test_cross_modulestore_import_export.py   |  2 +-
 .../tests/test_mixed_modulestore.py           |  3 +--
 .../xmodule/modulestore/tests/test_publish.py |  2 +-
 .../modulestore/tests/test_split_migrator.py  |  2 +-
 .../tests/test_split_modulestore.py           |  2 +-
 .../tests/test_split_w_old_mongo.py           |  4 ++--
 .../discussion/test_cohort_management.py      |  2 +-
 .../tests/discussion/test_cohorts.py          |  3 +--
 .../tests/discussion/test_discussion.py       |  2 +-
 .../discussion/test_discussion_management.py  |  3 +--
 common/test/acceptance/tests/lms/test_lms.py  |  2 +-
 .../tests/lms/test_lms_course_home.py         |  3 +--
 .../test_lms_split_test_courseware_search.py  |  4 +---
 .../tests/lms/test_problem_types.py           |  2 +-
 .../tests/test_generate_course_overview.py    |  4 ++--
 .../tests/test_post_cohort_membership_fix.py  |  4 ++--
 openedx/core/lib/tests/__init__.py            | 23 +++++++++++++++++++
 28 files changed, 57 insertions(+), 56 deletions(-)

diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py
index b2a9979f55b..ab2a50cee0d 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 db5a9fc656c..13770eece8f 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 4bfd820a0dd..6d08f93137e 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 12ccab74959..0574e7e106a 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 52e85d1924f..5bb81cda7d7 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 57da86a9710..59d39b9e990 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 3255ec723e8..3b76b695781 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 55a58f18dda..663070b7ca9 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 4011fee6bf2..c6ae5e24c4d 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 ae29396c467..087fdcfc2bb 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 624c598f175..162928a56cb 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 2ca9a906a7a..e8e49f0b532 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 c5fffac093d..06cd3fe0cd6 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 d990e32b236..a2af14c60cc 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 b40ab669769..f683612591e 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 6dbef326a43..94a5d7fd789 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 a9e92b12119..97881b6aaf1 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 847f2fc8c22..5ad3f9a7937 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 891523fad38..3d98f56fdb6 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 6805150e3a6..1ab253385cf 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 4775556659f..c9abd58049d 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 fb943f74660..9f0b3b52bbc 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 56086080b28..85d95c327f0 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 d91d31f260d..754b227fb45 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 e33a47527bf..169a3a10d15 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 8988e821f8d..271b7bbb22f 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 20b4db84e2d..8049bfba342 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 e69de29bb2d..a79e53e7ef7 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
-- 
GitLab