From 853bfe7a366aa5d569bcdf2640acea87fe015dfe Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 14 Apr 2016 16:21:54 -0400 Subject: [PATCH] Add a TestCase mixin for enabling caches in tests By default, disable all caching in tests, to preserve test independence. In order to enable caching, inherit from CacheSetupMixin, and specify which cache configuration is needed. [EV-32] --- .../commands/tests/test_force_publish.py | 16 +- cms/djangoapps/contentstore/tests/tests.py | 3 +- common/djangoapps/embargo/tests/test_api.py | 7 +- .../djangoapps/embargo/tests/test_models.py | 14 +- common/djangoapps/embargo/tests/test_views.py | 5 +- .../djangoapps/enrollment/tests/test_api.py | 6 +- .../djangoapps/enrollment/tests/test_views.py | 2 + .../external_auth/tests/test_shib.py | 27 ++- common/djangoapps/student/tests/test_login.py | 5 +- .../tests/test_login_registration_forms.py | 2 +- .../student/tests/test_reset_password.py | 5 +- .../tests/test_user_profile_properties.py | 5 +- .../xmodule/modulestore/tests/django_utils.py | 173 ++++++---------- .../bulk_email/tests/test_err_handling.py | 2 + .../tests/test_field_override_performance.py | 2 + lms/djangoapps/ccx/tests/test_views.py | 196 ++++++++++++------ .../certificates/tests/test_views.py | 5 +- lms/djangoapps/course_api/tests/test_api.py | 16 +- lms/djangoapps/course_api/tests/test_views.py | 26 ++- .../tests/test_generate_course_blocks.py | 2 + .../course_blocks/tests/test_signals.py | 1 + .../transformers/tests/helpers.py | 2 + .../courseware/tests/test_course_info.py | 2 + .../courseware/tests/test_grades.py | 2 + .../tests/test_submitting_problems.py | 2 + lms/djangoapps/courseware/tests/test_views.py | 2 + .../commands/tests/test_git_add_course.py | 2 + .../django_comment_client/base/tests.py | 11 +- lms/djangoapps/instructor/tests/test_api.py | 131 ++++++++++-- .../tests/test_tasks_helper.py | 3 + .../mobile_api/tests/test_middleware.py | 7 +- .../shoppingcart/tests/test_views.py | 3 + .../student_account/test/test_views.py | 5 +- .../tests/test_fake_software_secure.py | 2 +- .../verify_student/tests/test_models.py | 5 +- lms/envs/test.py | 15 +- .../djangoapps/bookmarks/tests/test_models.py | 2 + .../course_structures/api/v0/tests_api.py | 2 + .../credentials/tests/test_utils.py | 6 +- .../core/djangoapps/credit/tests/test_api.py | 2 + .../djangoapps/credit/tests/test_partition.py | 1 + .../djangoapps/programs/tests/test_utils.py | 6 +- .../user_api/accounts/tests/test_views.py | 20 +- .../djangoapps/user_api/tests/test_views.py | 15 +- openedx/core/djangolib/testing/__init__.py | 0 openedx/core/djangolib/testing/utils.py | 122 +++++++++++ openedx/core/lib/tests/test_edx_api_utils.py | 6 +- 47 files changed, 624 insertions(+), 272 deletions(-) create mode 100644 openedx/core/djangolib/testing/__init__.py create mode 100644 openedx/core/djangolib/testing/utils.py diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py b/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py index 7358210b31..5fd69318e6 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py @@ -4,7 +4,7 @@ Tests for the force_publish management command import mock from django.core.management import call_command, CommandError from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from contentstore.management.commands.force_publish import Command from contentstore.management.commands.utils import get_course_versions @@ -62,7 +62,19 @@ class TestForcePublish(SharedModuleStoreTestCase): with self.assertRaisesRegexp(CommandError, errstring): call_command('force_publish', unicode(course.id)) - @SharedModuleStoreTestCase.modifies_courseware + +class TestForcePublishModifications(ModuleStoreTestCase): + """ + Tests for the force_publish management command that modify the courseware + during the test. + """ + + def setUp(self): + super(TestForcePublishModifications, self).setUp() + self.course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) + self.test_user_id = ModuleStoreEnum.UserID.test + self.command = Command() + def test_force_publish(self): """ Test 'force_publish' command diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 7bb105d191..8965eb8659 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -88,8 +88,9 @@ class ContentStoreTestCase(ModuleStoreTestCase): class AuthTestCase(ContentStoreTestCase): """Check that various permissions-related things work""" - + CREATE_USER = False + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] def setUp(self): super(AuthTestCase, self).setUp() diff --git a/common/djangoapps/embargo/tests/test_api.py b/common/djangoapps/embargo/tests/test_api.py index 217aab0927..fc4a5c77b4 100644 --- a/common/djangoapps/embargo/tests/test_api.py +++ b/common/djangoapps/embargo/tests/test_api.py @@ -46,6 +46,8 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) class EmbargoCheckAccessApiTests(ModuleStoreTestCase): """Test the embargo API calls to determine whether a user has access. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(EmbargoCheckAccessApiTests, self).setUp() self.course = CourseFactory.create() @@ -239,16 +241,13 @@ class EmbargoMessageUrlApiTests(UrlResetMixin, ModuleStoreTestCase): """Test the embargo API calls for retrieving the blocking message URLs. """ URLCONF_MODULES = ['embargo'] + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] @patch.dict(settings.FEATURES, {'EMBARGO': True}) def setUp(self): super(EmbargoMessageUrlApiTests, self).setUp() self.course = CourseFactory.create() - def tearDown(self): - super(EmbargoMessageUrlApiTests, self).tearDown() - cache.clear() - @ddt.data( ('enrollment', '/embargo/blocked-message/enrollment/embargo/'), ('courseware', '/embargo/blocked-message/courseware/embargo/') diff --git a/common/djangoapps/embargo/tests/test_models.py b/common/djangoapps/embargo/tests/test_models.py index 340aabe829..5fc93bb868 100644 --- a/common/djangoapps/embargo/tests/test_models.py +++ b/common/djangoapps/embargo/tests/test_models.py @@ -8,9 +8,14 @@ from embargo.models import ( Country, CountryAccessRule, CourseAccessRuleHistory ) +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase -class EmbargoModelsTest(TestCase): + +class EmbargoModelsTest(CacheIsolationTestCase): """Test each of the 3 models in embargo.models""" + + ENABLED_CACHES = ['default'] + def test_course_embargo(self): course_id = CourseLocator('abc', '123', 'doremi') # Test that course is not authorized by default @@ -101,9 +106,11 @@ class EmbargoModelsTest(TestCase): self.assertFalse('1.2.0.0' in cblacklist) -class RestrictedCourseTest(TestCase): +class RestrictedCourseTest(CacheIsolationTestCase): """Test RestrictedCourse model. """ + ENABLED_CACHES = ['default'] + def test_unicode_values(self): course_id = CourseLocator('abc', '123', 'doremi') restricted_course = RestrictedCourse.objects.create(course_key=course_id) @@ -162,8 +169,9 @@ class CountryTest(TestCase): self.assertEquals(unicode(country), "New Zealand (NZ)") -class CountryAccessRuleTest(TestCase): +class CountryAccessRuleTest(CacheIsolationTestCase): """Test CountryAccessRule model. """ + ENABLED_CACHES = ['default'] def test_unicode_values(self): course_id = CourseLocator('abc', '123', 'doremi') diff --git a/common/djangoapps/embargo/tests/test_views.py b/common/djangoapps/embargo/tests/test_views.py index 09b3a48781..6ef81f6618 100644 --- a/common/djangoapps/embargo/tests/test_views.py +++ b/common/djangoapps/embargo/tests/test_views.py @@ -10,11 +10,12 @@ import ddt from util.testing import UrlResetMixin from embargo import messages +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.ddt -class CourseAccessMessageViewTest(UrlResetMixin, TestCase): +class CourseAccessMessageViewTest(CacheIsolationTestCase, UrlResetMixin): """Tests for the courseware access message view. These end-points serve static content. @@ -32,6 +33,8 @@ class CourseAccessMessageViewTest(UrlResetMixin, TestCase): """ + ENABLED_CACHES = ['default'] + URLCONF_MODULES = ['embargo'] @patch.dict(settings.FEATURES, {'EMBARGO': True}) diff --git a/common/djangoapps/enrollment/tests/test_api.py b/common/djangoapps/enrollment/tests/test_api.py index 95b441d157..f941db37e9 100644 --- a/common/djangoapps/enrollment/tests/test_api.py +++ b/common/djangoapps/enrollment/tests/test_api.py @@ -15,22 +15,24 @@ from course_modes.models import CourseMode from enrollment import api from enrollment.errors import EnrollmentApiLoadError, EnrollmentNotFoundError, CourseModeNotFoundError from enrollment.tests import fake_data_api +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @ddt.ddt @override_settings(ENROLLMENT_DATA_API="enrollment.tests.fake_data_api") @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class EnrollmentTest(TestCase): +class EnrollmentTest(CacheIsolationTestCase): """ Test student enrollment, especially with different course modes. """ USERNAME = "Bob" COURSE_ID = "some/great/course" + ENABLED_CACHES = ['default'] + def setUp(self): super(EnrollmentTest, self).setUp() fake_data_api.reset() - cache.clear() @ddt.data( # Default (no course modes in the database) diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index 722709bc29..d20c31a0aa 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -141,6 +141,8 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): OTHER_USERNAME = "Jane" OTHER_EMAIL = "jane@example.com" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create a course and user, then log in. """ super(EnrollmentTest, self).setUp() diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index ee1e066ba0..6d58bd16a8 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -23,11 +23,12 @@ from mock import patch from nose.plugins.attrib import attr from urllib import urlencode +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.views import create_account, change_enrollment from student.models import UserProfile, CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import CourseFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore import ModuleStoreEnum @@ -76,11 +77,14 @@ def gen_all_identities(): @attr('shard_3') @ddt @override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache') -class ShibSPTest(SharedModuleStoreTestCase): +class ShibSPTest(CacheIsolationTestCase): """ Tests for the Shibboleth SP, which communicates via request.META (Apache environment variables set by mod_shib) """ + + ENABLED_CACHES = ['default'] + request_factory = RequestFactory() def setUp(self): @@ -377,8 +381,23 @@ class ShibSPTest(SharedModuleStoreTestCase): self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) self.assertEqual(profile.name, identity.get('displayName').decode('utf-8')) + +@ddt +@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache') +class ShibSPTestModifiedCourseware(ModuleStoreTestCase): + """ + Tests for the Shibboleth SP which modify the courseware + """ + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + + request_factory = RequestFactory() + + def setUp(self): + super(ShibSPTestModifiedCourseware, self).setUp() + self.test_user_id = ModuleStoreEnum.UserID.test + @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") - @SharedModuleStoreTestCase.modifies_courseware @data(None, "", "shib:https://idp.stanford.edu/") def test_course_specific_login_and_reg(self, domain): """ @@ -457,7 +476,6 @@ class ShibSPTest(SharedModuleStoreTestCase): '&enrollment_action=enroll') @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") - @SharedModuleStoreTestCase.modifies_courseware def test_enrollment_limit_by_domain(self): """ Tests that the enrollmentDomain setting is properly limiting enrollment to those who have @@ -525,7 +543,6 @@ class ShibSPTest(SharedModuleStoreTestCase): self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") - @SharedModuleStoreTestCase.modifies_courseware def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py index 1a80873a78..2c0bce87cf 100644 --- a/common/djangoapps/student/tests/test_login.py +++ b/common/djangoapps/student/tests/test_login.py @@ -17,6 +17,7 @@ from mock import patch from social.apps.django_app.default.models import UserSocialAuth from external_auth.models import ExternalAuthMap +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory from student.views import login_oauth_token from third_party_auth.tests.utils import ( @@ -28,11 +29,13 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -class LoginTest(TestCase): +class LoginTest(CacheIsolationTestCase): ''' Test student.views.login_user() view ''' + ENABLED_CACHES = ['default'] + def setUp(self): super(LoginTest, self).setUp() # Create one user and save it to the database diff --git a/common/djangoapps/student/tests/test_login_registration_forms.py b/common/djangoapps/student/tests/test_login_registration_forms.py index d7f8713069..ada18b7db7 100644 --- a/common/djangoapps/student/tests/test_login_registration_forms.py +++ b/common/djangoapps/student/tests/test_login_registration_forms.py @@ -158,7 +158,7 @@ class RegisterFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, SharedModuleStore """Test rendering of the registration form. """ URLCONF_MODULES = ['lms.urls'] - + @classmethod def setUpClass(cls): super(RegisterFormTest, cls).setUpClass() diff --git a/common/djangoapps/student/tests/test_reset_password.py b/common/djangoapps/student/tests/test_reset_password.py index ac07a0c4e0..63e77e780f 100644 --- a/common/djangoapps/student/tests/test_reset_password.py +++ b/common/djangoapps/student/tests/test_reset_password.py @@ -19,6 +19,7 @@ from django.utils.http import urlsafe_base64_encode, base36_to_int, int_to_base3 from mock import Mock, patch import ddt +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.views import password_reset, password_reset_confirm_wrapper, SETTING_CHANGE_INITIATED from student.tests.factories import UserFactory from student.tests.test_email import mock_render_to_string @@ -28,11 +29,13 @@ from .test_microsite import fake_microsite_get_value @ddt.ddt -class ResetPasswordTests(EventTestMixin, TestCase): +class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): """ Tests that clicking reset password sends email, and doesn't activate the user """ request_factory = RequestFactory() + ENABLED_CACHES = ['default'] + def setUp(self): super(ResetPasswordTests, self).setUp('student.views.tracker') self.user = UserFactory.create() diff --git a/common/djangoapps/student/tests/test_user_profile_properties.py b/common/djangoapps/student/tests/test_user_profile_properties.py index 3c7b835d54..ace966e726 100644 --- a/common/djangoapps/student/tests/test_user_profile_properties.py +++ b/common/djangoapps/student/tests/test_user_profile_properties.py @@ -7,14 +7,17 @@ from django.test import TestCase from student.models import UserProfile from student.tests.factories import UserFactory from django.core.cache import cache +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @ddt.ddt -class UserProfilePropertiesTest(TestCase): +class UserProfilePropertiesTest(CacheIsolationTestCase): """Unit tests for age, gender_display, and level_of_education_display properties .""" password = "test" + ENABLED_CACHES = ['default'] + def setUp(self): super(UserProfilePropertiesTest, self).setUp() self.user = UserFactory.create(password=self.password) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 0b3108bdb9..6bebed9acc 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -13,7 +13,6 @@ from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase from django.test.utils import override_settings -from request_cache.middleware import RequestCache from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error from openedx.core.lib.tempdir import mkdtemp_clean @@ -25,6 +24,7 @@ from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOS from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK from openedx.core.djangoapps.bookmarks.signals import trigger_update_xblocks_cache_task +from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase class StoreConstructors(object): @@ -170,23 +170,64 @@ TEST_DATA_SPLIT_MODULESTORE = mixed_store_config( ) -def clear_all_caches(): - """Clear all caches so that cache info doesn't leak across test cases.""" - # This will no longer be necessary when Django adds (in Django 1.10?): - # https://code.djangoproject.com/ticket/11505 - for cache in django.core.cache.caches.all(): - cache.clear() +class ModuleStoreIsolationMixin(CacheIsolationMixin): + """ + A mixin to be used by TestCases that want to isolate their use of the + Modulestore. - RequestCache().clear_request_cache() + How to use:: + + class MyTestCase(ModuleStoreMixin, TestCase): + + MODULESTORE = + + def my_test(self): + self.start_modulestore_isolation() + self.addCleanup(self.end_modulestore_isolation) + + modulestore.create_course(...) + ... + + """ + + MODULESTORE = mixed_store_config(mkdtemp_clean(), {}) + ENABLED_CACHES = ['mongo_metadata_inheritance', 'loc_cache'] + + @classmethod + def start_modulestore_isolation(cls): + """ + Isolate uses of the modulestore after this call. Once + :py:meth:`end_modulestore_isolation` is called, this modulestore will + be flushed (all content will be deleted). + """ + cls.start_cache_isolation() + cls.__settings_override = override_settings( + MODULESTORE=cls.MODULESTORE, + ) + cls.__settings_override.__enter__() + XMODULE_FACTORY_LOCK.enable() + clear_existing_modulestores() + cls.store = modulestore() + + @classmethod + def end_modulestore_isolation(cls): + """ + Delete all content in the Modulestore, and reset the Modulestore + settings from before :py:meth:`start_modulestore_isolation` was + called. + """ + drop_mongo_collections() # pylint: disable=no-value-for-parameter + XMODULE_FACTORY_LOCK.disable() + cls.__settings_override.__exit__(None, None, None) + cls.end_cache_isolation() -class SharedModuleStoreTestCase(TestCase): +class SharedModuleStoreTestCase(ModuleStoreIsolationMixin, CacheIsolationTestCase): """ Subclass for any test case that uses a ModuleStore that can be shared between individual tests. This class ensures that the ModuleStore is cleaned before/after the entire test case has run. Use this class if your tests - set up one or a small number of courses that individual tests do not modify - (or modify extermely rarely -- see @modifies_courseware). + set up one or a small number of courses that individual tests do not modify. If your tests modify contents in the ModuleStore, you should use ModuleStoreTestCase instead. @@ -218,41 +259,26 @@ class SharedModuleStoreTestCase(TestCase): In Django 1.8, we will be able to use setUpTestData() to do class level init for Django ORM models that will get cleaned up properly. """ - MODULESTORE = mixed_store_config(mkdtemp_clean(), {}) # Tell Django to clean out all databases, not just default multi_db = True - @classmethod - def _setUpModuleStore(cls): # pylint: disable=invalid-name - """ - Set up the modulestore for an entire test class. - """ - cls._settings_override = override_settings(MODULESTORE=cls.MODULESTORE) - cls._settings_override.__enter__() - XMODULE_FACTORY_LOCK.enable() - clear_existing_modulestores() - cls.store = modulestore() - @classmethod @contextmanager def setUpClassAndTestData(cls): # pylint: disable=invalid-name """ For use when the test class has a setUpTestData() method that uses variables that are setup during setUpClass() of the same test class. - Use it like so: - @classmethod def setUpClass(cls): with super(MyTestClass, cls).setUpClassAndTestData(): - @classmethod def setUpTestData(cls): """ - cls._setUpModuleStore() + cls.start_modulestore_isolation() # Now yield to allow the test class to run its setUpClass() setup code. yield # Now call the base class, which calls back into the test class's setUpTestData(). @@ -261,19 +287,15 @@ class SharedModuleStoreTestCase(TestCase): @classmethod def setUpClass(cls): """ - For use when the test class has no setUpTestData() method -or- - when that method does not use variable set up in setUpClass(). + Start modulestore isolation, and then load modulestore specific + test data. """ super(SharedModuleStoreTestCase, cls).setUpClass() - cls._setUpModuleStore() + cls.start_modulestore_isolation() @classmethod def tearDownClass(cls): - drop_mongo_collections() # pylint: disable=no-value-for-parameter - clear_all_caches() - XMODULE_FACTORY_LOCK.disable() - cls._settings_override.__exit__(None, None, None) - + cls.end_modulestore_isolation() super(SharedModuleStoreTestCase, cls).tearDownClass() def setUp(self): @@ -282,69 +304,8 @@ class SharedModuleStoreTestCase(TestCase): OverrideFieldData.provider_classes = None super(SharedModuleStoreTestCase, self).setUp() - def tearDown(self): - """Reset caches.""" - clear_all_caches() - super(SharedModuleStoreTestCase, self).tearDown() - def reset(self): - """ - Manually run tearDownClass/setUpClass again. - - This is so that if you have a mostly read-only course that you're just - modifying in one test, you can write `self.reset()` at the - end of that test and reset the state of the world for other tests in - the class. - """ - self.tearDownClass() - self.setUpClass() - - @staticmethod - def modifies_courseware(f): - """ - Decorator to place around tests that modify course content. - - For performance reasons, SharedModuleStoreTestCase intentionally does - not reset the modulestore between individual tests. However, sometimes - you might have a test case where the vast majority of tests treat a - course as read-only, but one or two want to modify it. In that case, you - can do this: - - class MyTestCase(SharedModuleStoreTestCase): - # ... - @SharedModuleStoreTestCase.modifies_courseware - def test_that_edits_modulestore(self): - do_something() - - This is equivalent to calling `self.reset()` at the end of - your test. - - If you find yourself using this functionality a lot, it might indicate - that you should be using ModuleStoreTestCase instead, or that you should - break up your tests into different TestCases. - """ - @functools.wraps(f) - def wrapper(*args, **kwargs): - """Call the object method, and reset the test case afterwards.""" - try: - # Attempt execution of the test. - return_val = f(*args, **kwargs) - except: - # If the test raises an exception, re-raise it. - raise - else: - # Otherwise, return the test's return value. - return return_val - finally: - # In either case, call SharedModuleStoreTestCase.reset() "on the way out." - # For more, see here: https://docs.python.org/2/tutorial/errors.html#defining-clean-up-actions. - obj = args[0] - obj.reset() - - return wrapper - - -class ModuleStoreTestCase(TestCase): +class ModuleStoreTestCase(ModuleStoreIsolationMixin, TestCase): """ Subclass for any test case that uses a ModuleStore. Ensures that the ModuleStore is cleaned before/after each test. @@ -382,8 +343,6 @@ class ModuleStoreTestCase(TestCase): your `setUp()` method. """ - MODULESTORE = mixed_store_config(mkdtemp_clean(), {}) - CREATE_USER = True # Tell Django to clean out all databases, not just default @@ -394,20 +353,9 @@ class ModuleStoreTestCase(TestCase): Creates a test User if `self.CREATE_USER` is True. Sets the password as self.user_password. """ - settings_override = override_settings(MODULESTORE=self.MODULESTORE) - settings_override.__enter__() - self.addCleanup(settings_override.__exit__, None, None, None) + self.start_modulestore_isolation() - # Clear out any existing modulestores, - # which will cause them to be re-created - clear_existing_modulestores() - - self.addCleanup(drop_mongo_collections) - self.addCleanup(clear_all_caches) - - # Enable XModuleFactories for the space of this test (and its setUp). - self.addCleanup(XMODULE_FACTORY_LOCK.disable) - XMODULE_FACTORY_LOCK.enable() + self.addCleanup(self.end_modulestore_isolation) # When testing CCX, we should make sure that # OverrideFieldData.provider_classes is always reset to `None` so @@ -436,7 +384,6 @@ class ModuleStoreTestCase(TestCase): self.user.is_staff = True self.user.save() - def create_non_staff_user(self): """ Creates a non-staff test user. diff --git a/lms/djangoapps/bulk_email/tests/test_err_handling.py b/lms/djangoapps/bulk_email/tests/test_err_handling.py index 04547d7e9c..d6788e6817 100644 --- a/lms/djangoapps/bulk_email/tests/test_err_handling.py +++ b/lms/djangoapps/bulk_email/tests/test_err_handling.py @@ -44,6 +44,8 @@ class TestEmailErrors(ModuleStoreTestCase): Test that errors from sending email are handled properly. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(TestEmailErrors, self).setUp() course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ" diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 92f32e773f..82f8f3467f 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -52,6 +52,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, # TEST_DATA must be overridden by subclasses TEST_DATA = None + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create a test client, course, and user. diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py index 507a8d66f2..e7839c1a1b 100644 --- a/lms/djangoapps/ccx/tests/test_views.py +++ b/lms/djangoapps/ccx/tests/test_views.py @@ -68,6 +68,8 @@ from lms.djangoapps.ccx.tests.utils import ( from lms.djangoapps.ccx.utils import is_email from lms.djangoapps.ccx.views import get_date +from xmodule.modulestore.django import modulestore + def intercept_renderer(path, context): """ @@ -184,10 +186,6 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): Tests for Custom Courses views. """ - @classmethod - def setUpClass(cls): - super(TestCoachDashboard, cls).setUpClass() - def setUp(self): """ Set up tests @@ -206,31 +204,6 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): allow_access(self.course, instructor, 'instructor') self.assertTrue(CourseInstructorRole(self.course.id).has_user(instructor)) - def assert_elements_in_schedule(self, url, n_chapters=2, n_sequentials=4, n_verticals=8): - """ - Helper function to count visible elements in the schedule - """ - response = self.client.get(url) - # the schedule contains chapters - chapters = json.loads(response.mako_context['schedule']) # pylint: disable=no-member - sequentials = flatten([chapter.get('children', []) for chapter in chapters]) - verticals = flatten([sequential.get('children', []) for sequential in sequentials]) - # check that the numbers of nodes at different level are the expected ones - self.assertEqual(n_chapters, len(chapters)) - self.assertEqual(n_sequentials, len(sequentials)) - self.assertEqual(n_verticals, len(verticals)) - # extract the locations of all the nodes - all_elements = chapters + sequentials + verticals - return [elem['location'] for elem in all_elements if 'location' in elem] - - def hide_node(self, node): - """ - Helper function to set the node `visible_to_staff_only` property - to True and save the change - """ - node.visible_to_staff_only = True - self.mstore.update_item(node, self.coach.id) - def test_not_a_coach(self): """ User is not a coach, should get Forbidden response. @@ -352,43 +325,6 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): self.assertEqual(get_date(ccx, unit, 'start', parent_node=subsection), self.mooc_start) self.assertEqual(get_date(ccx, unit, 'due', parent_node=subsection), self.mooc_due) - @SharedModuleStoreTestCase.modifies_courseware - @patch('ccx.views.render_to_response', intercept_renderer) - @patch('ccx.views.TODAY') - def test_get_ccx_schedule(self, today): - """ - Gets CCX schedule and checks number of blocks in it. - Hides nodes at a different depth and checks that these nodes - are not in the schedule. - """ - today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) - self.make_coach() - ccx = self.make_ccx() - url = reverse( - 'ccx_coach_dashboard', - kwargs={ - 'course_id': CCXLocator.from_course_locator( - self.course.id, ccx.id) - } - ) - # all the elements are visible - self.assert_elements_in_schedule(url) - # hide a vertical - vertical = self.verticals[0] - self.hide_node(vertical) - locations = self.assert_elements_in_schedule(url, n_verticals=7) - self.assertNotIn(unicode(vertical.location), locations) - # hide a sequential - sequential = self.sequentials[0] - self.hide_node(sequential) - locations = self.assert_elements_in_schedule(url, n_sequentials=3, n_verticals=6) - self.assertNotIn(unicode(sequential.location), locations) - # hide a chapter - chapter = self.chapters[0] - self.hide_node(chapter) - locations = self.assert_elements_in_schedule(url, n_chapters=1, n_sequentials=2, n_verticals=4) - self.assertNotIn(unicode(chapter.location), locations) - @patch('ccx.views.render_to_response', intercept_renderer) @patch('ccx.views.TODAY') def test_edit_schedule(self, today): @@ -842,6 +778,134 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ) +@attr('shard_1') +class TestCoachDashboardSchedule(CcxTestCase, LoginEnrollmentTestCase, ModuleStoreTestCase): + """ + Tests of the CCX Coach Dashboard which need to modify the course content. + """ + + ENABLED_CACHES = ['default', 'mongo_inheritance_cache', 'loc_cache'] + + def setUp(self): + super(TestCoachDashboardSchedule, self).setUp() + self.course = course = CourseFactory.create() + + # Create a course outline + self.mooc_start = start = datetime.datetime( + 2010, 5, 12, 2, 42, tzinfo=pytz.UTC + ) + self.mooc_due = due = datetime.datetime( + 2010, 7, 7, 0, 0, tzinfo=pytz.UTC + ) + + self.chapters = [ + ItemFactory.create(start=start, parent=course) for _ in xrange(2) + ] + self.sequentials = flatten([ + [ + ItemFactory.create(parent=chapter) for _ in xrange(2) + ] for chapter in self.chapters + ]) + self.verticals = flatten([ + [ + ItemFactory.create( + start=start, due=due, parent=sequential, graded=True, format='Homework', category=u'vertical' + ) for _ in xrange(2) + ] for sequential in self.sequentials + ]) + + # Trying to wrap the whole thing in a bulk operation fails because it + # doesn't find the parents. But we can at least wrap this part... + with self.store.bulk_operations(course.id, emit_signals=False): + blocks = flatten([ # pylint: disable=unused-variable + [ + ItemFactory.create(parent=vertical) for _ in xrange(2) + ] for vertical in self.verticals + ]) + + # Create instructor account + self.coach = UserFactory.create() + # create an instance of modulestore + self.mstore = modulestore() + + # Login with the instructor account + self.client.login(username=self.coach.username, password="test") + + # adding staff to master course. + staff = UserFactory() + allow_access(self.course, staff, 'staff') + self.assertTrue(CourseStaffRole(self.course.id).has_user(staff)) + + # adding instructor to master course. + instructor = UserFactory() + allow_access(self.course, instructor, 'instructor') + self.assertTrue(CourseInstructorRole(self.course.id).has_user(instructor)) + + self.assertTrue(modulestore().has_course(self.course.id)) + + def assert_elements_in_schedule(self, url, n_chapters=2, n_sequentials=4, n_verticals=8): + """ + Helper function to count visible elements in the schedule + """ + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + # the schedule contains chapters + chapters = json.loads(response.mako_context['schedule']) # pylint: disable=no-member + sequentials = flatten([chapter.get('children', []) for chapter in chapters]) + verticals = flatten([sequential.get('children', []) for sequential in sequentials]) + # check that the numbers of nodes at different level are the expected ones + self.assertEqual(n_chapters, len(chapters)) + self.assertEqual(n_sequentials, len(sequentials)) + self.assertEqual(n_verticals, len(verticals)) + # extract the locations of all the nodes + all_elements = chapters + sequentials + verticals + return [elem['location'] for elem in all_elements if 'location' in elem] + + def hide_node(self, node): + """ + Helper function to set the node `visible_to_staff_only` property + to True and save the change + """ + node.visible_to_staff_only = True + self.mstore.update_item(node, self.coach.id) + + @patch('ccx.views.render_to_response', intercept_renderer) + @patch('ccx.views.TODAY') + def test_get_ccx_schedule(self, today): + """ + Gets CCX schedule and checks number of blocks in it. + Hides nodes at a different depth and checks that these nodes + are not in the schedule. + """ + today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) + self.make_coach() + ccx = self.make_ccx() + url = reverse( + 'ccx_coach_dashboard', + kwargs={ + 'course_id': CCXLocator.from_course_locator( + self.course.id, ccx.id) + } + ) + # all the elements are visible + self.assert_elements_in_schedule(url) + # hide a vertical + vertical = self.verticals[0] + self.hide_node(vertical) + locations = self.assert_elements_in_schedule(url, n_verticals=7) + self.assertNotIn(unicode(vertical.location), locations) + # hide a sequential + sequential = self.sequentials[0] + self.hide_node(sequential) + locations = self.assert_elements_in_schedule(url, n_sequentials=3, n_verticals=6) + self.assertNotIn(unicode(sequential.location), locations) + # hide a chapter + chapter = self.chapters[0] + self.hide_node(chapter) + locations = self.assert_elements_in_schedule(url, n_chapters=1, n_sequentials=2, n_verticals=4) + self.assertNotIn(unicode(chapter.location), locations) + + GET_CHILDREN = XModuleMixin.get_children diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 6824a1ff07..85dc913d31 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -12,6 +12,7 @@ from django.test.client import Client from django.test.utils import override_settings from nose.plugins.attrib import attr from opaque_keys.edx.locator import CourseLocator +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from certificates.api import get_certificate_url from certificates.models import ( @@ -38,7 +39,7 @@ FEATURES_WITH_CUSTOM_CERTS_ENABLED.update(FEATURES_WITH_CERTS_ENABLED) @attr('shard_1') @ddt.ddt -class UpdateExampleCertificateViewTest(TestCase): +class UpdateExampleCertificateViewTest(CacheIsolationTestCase): """Tests for the XQueue callback that updates example certificates. """ COURSE_KEY = CourseLocator(org='test', course='test', run='test') @@ -48,6 +49,8 @@ class UpdateExampleCertificateViewTest(TestCase): DOWNLOAD_URL = 'http://www.example.com' ERROR_REASON = 'Kaboom!' + ENABLED_CACHES = ['default'] + def setUp(self): super(UpdateExampleCertificateViewTest, self).setUp() self.cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY) diff --git a/lms/djangoapps/course_api/tests/test_api.py b/lms/djangoapps/course_api/tests/test_api.py index 5c447ece2c..d8e375f291 100644 --- a/lms/djangoapps/course_api/tests/test_api.py +++ b/lms/djangoapps/course_api/tests/test_api.py @@ -144,13 +144,24 @@ class TestGetCourseList(CourseListTestMixin, SharedModuleStoreTestCase): with self.assertRaises(PermissionDenied): self._make_api_call(anonuser, self.staff_user) - @SharedModuleStoreTestCase.modifies_courseware + +class TestGetCourseListMultipleCourses(CourseListTestMixin, ModuleStoreTestCase): + """ + Test the behavior of the `list_courses` api function (with tests that + modify the courseware). + """ + + def setUp(self): + super(TestGetCourseListMultipleCourses, self).setUp() + self.course = self.create_course() + self.staff_user = self.create_user("staff", is_staff=True) + self.honor_user = self.create_user("honor", is_staff=False) + def test_multiple_courses(self): self.create_course(course='second') courses = self._make_api_call(self.honor_user, self.honor_user) self.assertEqual(len(courses), 2) - @SharedModuleStoreTestCase.modifies_courseware def test_filter_by_org(self): """Verify that courses are filtered by the provided org key.""" # Create a second course to be filtered out of queries. @@ -173,7 +184,6 @@ class TestGetCourseList(CourseListTestMixin, SharedModuleStoreTestCase): all(course.org == self.course.org for course in filtered_courses) ) - @SharedModuleStoreTestCase.modifies_courseware def test_filter(self): # Create a second course to be filtered out of queries. alternate_course = self.create_course(course='mobile', mobile_available=True) diff --git a/lms/djangoapps/course_api/tests/test_views.py b/lms/djangoapps/course_api/tests/test_views.py index 6bf79bbec0..104e8f1713 100644 --- a/lms/djangoapps/course_api/tests/test_views.py +++ b/lms/djangoapps/course_api/tests/test_views.py @@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse from django.test import RequestFactory from nose.plugins.attrib import attr -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from .mixins import CourseApiFactoryMixin, TEST_PASSWORD from ..views import CourseDetailView @@ -91,7 +91,24 @@ class CourseListViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): response_to_missing_username = self.verify_response(expected_status_code=200) self.assertIsNotNone(response_to_missing_username.data) # pylint: disable=no-member - @SharedModuleStoreTestCase.modifies_courseware + def test_not_logged_in(self): + self.client.logout() + self.verify_response() + + +class CourseListViewTestCaseMultipleCourses(CourseApiTestViewMixin, ModuleStoreTestCase): + """ + Test responses returned from CourseListView (with tests that modify the + courseware). + """ + + def setUp(self): + super(CourseListViewTestCaseMultipleCourses, self).setUp() + self.course = self.create_course() + self.url = reverse('course-list') + self.staff_user = self.create_user(username='staff', is_staff=True) + self.honor_user = self.create_user(username='honor', is_staff=False) + def test_filter_by_org(self): """Verify that CourseOverviews are filtered by the provided org key.""" self.setup_user(self.staff_user) @@ -116,7 +133,6 @@ class CourseListViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): all(course['org'] == self.course.org for course in filtered_response.data['results']) # pylint: disable=no-member ) - @SharedModuleStoreTestCase.modifies_courseware def test_filter(self): self.setup_user(self.staff_user) @@ -139,10 +155,6 @@ class CourseListViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): "testing course_api.views.CourseListView with filter_={}".format(filter_), ) - def test_not_logged_in(self): - self.client.logout() - self.verify_response() - class CourseDetailViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase): """ diff --git a/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py b/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py index 9c384be15f..c0459a5e21 100644 --- a/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py +++ b/lms/djangoapps/course_blocks/management/commands/tests/test_generate_course_blocks.py @@ -15,6 +15,8 @@ class TestGenerateCourseBlocks(ModuleStoreTestCase): """ Tests generate course blocks management command. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create courses in modulestore. diff --git a/lms/djangoapps/course_blocks/tests/test_signals.py b/lms/djangoapps/course_blocks/tests/test_signals.py index 9f63279b31..9887087024 100644 --- a/lms/djangoapps/course_blocks/tests/test_signals.py +++ b/lms/djangoapps/course_blocks/tests/test_signals.py @@ -15,6 +15,7 @@ class CourseBlocksSignalTest(EnableTransformerRegistryMixin, ModuleStoreTestCase """ Tests for the Course Blocks signal """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] def setUp(self): super(CourseBlocksSignalTest, self).setUp() diff --git a/lms/djangoapps/course_blocks/transformers/tests/helpers.py b/lms/djangoapps/course_blocks/transformers/tests/helpers.py index af8b5afa77..bdffb4f3ea 100644 --- a/lms/djangoapps/course_blocks/transformers/tests/helpers.py +++ b/lms/djangoapps/course_blocks/transformers/tests/helpers.py @@ -36,6 +36,8 @@ class CourseStructureTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase) """ Helper for test cases that need to build course structures. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Create users. diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index 069547e506..76281d5b97 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -36,6 +36,7 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase): """ Tests for the Course Info page """ + @classmethod def setUpClass(cls): super(CourseInfoTestCase, cls).setUpClass() @@ -263,6 +264,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest """ Tests for the info page of self-paced courses. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] @classmethod def setUpClass(cls): diff --git a/lms/djangoapps/courseware/tests/test_grades.py b/lms/djangoapps/courseware/tests/test_grades.py index c62657347e..f68c7400a6 100644 --- a/lms/djangoapps/courseware/tests/test_grades.py +++ b/lms/djangoapps/courseware/tests/test_grades.py @@ -151,6 +151,8 @@ class TestMaxScoresCache(SharedModuleStoreTestCase): Tests for the MaxScoresCache """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + @classmethod def setUpClass(cls): super(TestMaxScoresCache, cls).setUpClass() diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 0892ef6e89..c163c66dbe 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -127,6 +127,8 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl COURSE_SLUG = "100" COURSE_NAME = "test_course" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(TestSubmittingProblems, self).setUp() diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 016a98c5e6..7cde020765 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -970,6 +970,8 @@ class ProgressPageTests(ModuleStoreTestCase): Tests that verify that the progress page works correctly. """ + ENABLED_CACHES = ['default', 'mongo_modulestore_inheritance', 'loc_cache'] + def setUp(self): super(ProgressPageTests, self).setUp() self.request_factory = RequestFactory() diff --git a/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py b/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py index 555743f0e9..f5550cdfbf 100644 --- a/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py +++ b/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py @@ -51,6 +51,8 @@ class TestGitAddCourse(SharedModuleStoreTestCase): TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx') GIT_REPO_DIR = settings.GIT_REPO_DIR + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def assertCommandFailureRegexp(self, regex, *args): """ Convenience function for testing command failures diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 43c0e56aaf..7b33deeb94 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -10,7 +10,6 @@ from django.test.client import RequestFactory from django.contrib.auth.models import User from django.core.management import call_command from django.core.urlresolvers import reverse -from request_cache.middleware import RequestCache from mock import patch, ANY, Mock from nose.tools import assert_true, assert_equal from nose.plugins.attrib import attr @@ -24,8 +23,9 @@ from django_comment_client.tests.utils import CohortedTestCase from django_comment_client.tests.unicode import UnicodeTestMixin from django_comment_common.models import Role from django_comment_common.utils import seed_permissions_roles, ThreadContext -from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseAccessRoleFactory + from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory +from student.tests.factories import CourseEnrollmentFactory, UserFactory, CourseAccessRoleFactory from util.testing import UrlResetMixin from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase @@ -351,17 +351,12 @@ class ViewsTestCaseMixin(object): class ViewsQueryCountTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin, ViewsTestCaseMixin): CREATE_USER = False + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): super(ViewsQueryCountTestCase, self).setUp() - def clear_caches(self): - """Clears caches so that query count numbers are accurate.""" - for cache in settings.CACHES: - caches[cache].clear() - RequestCache.clear_request_cache() - def count_queries(func): # pylint: disable=no-self-argument """ Decorates test methods to count mongo and SQL calls for a diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index b5abbbb299..d1a095d869 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -51,7 +51,7 @@ from student.models import ( ) from student.tests.factories import UserFactory, CourseModeFactory, AdminFactory from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole, CourseInstructorRole -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.fields import Date @@ -3968,27 +3968,6 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): }) self.assertEqual(response.status_code, 400, response.content) - @SharedModuleStoreTestCase.modifies_courseware - def test_reset_extension_to_deleted_date(self): - """ - Test that we can delete a due date extension after deleting the normal - due date, without causing an error. - """ - self.test_change_due_date() - self.week1.due = None - self.week1 = self.store.update_item(self.week1, self.user1.id) - # Now, week1's normal due date is deleted but the extension still exists. - url = reverse('reset_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) - response = self.client.get(url, { - 'student': self.user1.username, - 'url': self.week1.location.to_deprecated_string(), - }) - self.assertEqual(response.status_code, 200, response.content) - self.assertEqual( - None, - get_extended_due(self.course, self.week1, self.user1) - ) - def test_show_unit_extensions(self): self.test_change_due_date() url = reverse('show_unit_extensions', @@ -4017,6 +3996,114 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase): self.user1.profile.name, self.user1.username)}) +@attr('shard_1') +class TestDueDateExtensionsDeletedDate(ModuleStoreTestCase, LoginEnrollmentTestCase): + def setUp(self): + """ + Fixtures. + """ + super(TestDueDateExtensionsDeletedDate, self).setUp() + + self.course = CourseFactory.create() + self.due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) + + with self.store.bulk_operations(self.course.id, emit_signals=False): + self.week1 = ItemFactory.create(due=self.due) + self.week2 = ItemFactory.create(due=self.due) + self.week3 = ItemFactory.create() # No due date + self.course.children = [ + self.week1.location.to_deprecated_string(), + self.week2.location.to_deprecated_string(), + self.week3.location.to_deprecated_string() + ] + self.homework = ItemFactory.create( + parent_location=self.week1.location, + due=self.due + ) + self.week1.children = [self.homework.location.to_deprecated_string()] + + user1 = UserFactory.create() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.week1.location).save() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.week2.location).save() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.week3.location).save() + StudentModule( + state='{}', + student_id=user1.id, + course_id=self.course.id, + module_state_key=self.homework.location).save() + + user2 = UserFactory.create() + StudentModule( + state='{}', + student_id=user2.id, + course_id=self.course.id, + module_state_key=self.week1.location).save() + StudentModule( + state='{}', + student_id=user2.id, + course_id=self.course.id, + module_state_key=self.homework.location).save() + + user3 = UserFactory.create() + StudentModule( + state='{}', + student_id=user3.id, + course_id=self.course.id, + module_state_key=self.week1.location).save() + StudentModule( + state='{}', + student_id=user3.id, + course_id=self.course.id, + module_state_key=self.homework.location).save() + + self.user1 = user1 + self.user2 = user2 + self.instructor = InstructorFactory(course_key=self.course.id) + self.client.login(username=self.instructor.username, password='test') + + def test_reset_extension_to_deleted_date(self): + """ + Test that we can delete a due date extension after deleting the normal + due date, without causing an error. + """ + + url = reverse('change_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, { + 'student': self.user1.username, + 'url': self.week1.location.to_deprecated_string(), + 'due_datetime': '12/30/2013 00:00' + }) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(datetime.datetime(2013, 12, 30, 0, 0, tzinfo=utc), + get_extended_due(self.course, self.week1, self.user1)) + + self.week1.due = None + self.week1 = self.store.update_item(self.week1, self.user1.id) + # Now, week1's normal due date is deleted but the extension still exists. + url = reverse('reset_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, { + 'student': self.user1.username, + 'url': self.week1.location.to_deprecated_string(), + }) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual( + None, + get_extended_due(self.course, self.week1, self.user1) + ) + + @attr('shard_1') class TestCourseIssuedCertificatesData(SharedModuleStoreTestCase): """ diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index a6238a824b..d5021027fd 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -1683,6 +1683,9 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): """ Test certificate generation task works. """ + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(TestCertificateGeneration, self).setUp() self.initialize_course() diff --git a/lms/djangoapps/mobile_api/tests/test_middleware.py b/lms/djangoapps/mobile_api/tests/test_middleware.py index 7ffbbdce50..86a675e495 100644 --- a/lms/djangoapps/mobile_api/tests/test_middleware.py +++ b/lms/djangoapps/mobile_api/tests/test_middleware.py @@ -10,18 +10,21 @@ import mock from pytz import UTC from mobile_api.middleware import AppVersionUpgrade from mobile_api.models import AppVersionConfig +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase @ddt.ddt -class TestAppVersionUpgradeMiddleware(TestCase): +class TestAppVersionUpgradeMiddleware(CacheIsolationTestCase): """ Tests for version based app upgrade middleware """ + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestAppVersionUpgradeMiddleware, self).setUp() self.middleware = AppVersionUpgrade() self.set_app_version_config() - cache.clear() def set_app_version_config(self): """ Creates configuration data for platform versions """ diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 175fce6622..15e2727077 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -1707,6 +1707,9 @@ class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase): """ Test suite for RegistrationCodeRedemption Course Enrollments """ + + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + @classmethod def setUpClass(cls): super(RegistrationCodeRedemptionCourseEnrollment, cls).setUpClass() diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 782b18b307..4482d54341 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -22,6 +22,7 @@ from course_modes.models import CourseMode from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH from openedx.core.djangolib.js_utils import dump_js_escaped_json +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory from student_account.views import account_settings_context from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin @@ -31,7 +32,7 @@ from openedx.core.djangoapps.theming.test_util import with_edx_domain_context @ddt.ddt -class StudentAccountUpdateTest(UrlResetMixin, TestCase): +class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin): """ Tests for the student account views that update the user's account information. """ USERNAME = u"heisenberg" @@ -64,6 +65,8 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase): URLCONF_MODULES = ['student_accounts.urls'] + ENABLED_CACHES = ['default'] + def setUp(self): super(StudentAccountUpdateTest, self).setUp() diff --git a/lms/djangoapps/verify_student/tests/test_fake_software_secure.py b/lms/djangoapps/verify_student/tests/test_fake_software_secure.py index f33b69a2a9..a9c2b4b8dc 100644 --- a/lms/djangoapps/verify_student/tests/test_fake_software_secure.py +++ b/lms/djangoapps/verify_student/tests/test_fake_software_secure.py @@ -14,7 +14,7 @@ class SoftwareSecureFakeViewTest(UrlResetMixin, TestCase): """ Base class to test the fake software secure view. """ - + URLCONF_MODULES = ['verify_student.urls'] def setUp(self, **kwargs): diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 349cf7d144..b67732df0a 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -18,6 +18,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from opaque_keys.edx.keys import CourseKey +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from lms.djangoapps.verify_student.models import ( SoftwareSecurePhotoVerification, @@ -844,11 +845,13 @@ class SkippedReverificationTest(ModuleStoreTestCase): ) -class VerificationDeadlineTest(TestCase): +class VerificationDeadlineTest(CacheIsolationTestCase): """ Tests for the VerificationDeadline model. """ + ENABLED_CACHES = ['default'] + def test_caching(self): deadlines = { CourseKey.from_string("edX/DemoX/Fall"): datetime.now(pytz.UTC), diff --git a/lms/envs/test.py b/lms/envs/test.py index ba43f30574..99d2524735 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -203,9 +203,7 @@ CACHES = { # This is the cache used for most things. # In staging/prod envs, the sessions also live here. 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'edx_loc_mem_cache', - 'KEY_FUNCTION': 'util.memcache.safe_key', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, # The general cache is what you get if you use our util.cache. It's used for @@ -215,20 +213,13 @@ CACHES = { # push process. 'general': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', - 'KEY_PREFIX': 'general', - 'VERSION': 4, - 'KEY_FUNCTION': 'util.memcache.safe_key', }, 'mongo_metadata_inheritance': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': os.path.join(tempfile.gettempdir(), 'mongo_metadata_inheritance'), - 'TIMEOUT': 300, - 'KEY_FUNCTION': 'util.memcache.safe_key', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, 'loc_cache': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'edx_location_mem_cache', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, 'course_structure_cache': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', diff --git a/openedx/core/djangoapps/bookmarks/tests/test_models.py b/openedx/core/djangoapps/bookmarks/tests/test_models.py index a2b762a2bb..e452a5a92e 100644 --- a/openedx/core/djangoapps/bookmarks/tests/test_models.py +++ b/openedx/core/djangoapps/bookmarks/tests/test_models.py @@ -40,6 +40,8 @@ class BookmarksTestsBase(ModuleStoreTestCase): STORE_TYPE = ModuleStoreEnum.Type.mongo TEST_PASSWORD = 'test' + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): super(BookmarksTestsBase, self).setUp() diff --git a/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py b/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py index 927c3698c1..ce1093dbea 100644 --- a/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py +++ b/openedx/core/djangoapps/content/course_structures/api/v0/tests_api.py @@ -16,6 +16,8 @@ class CourseStructureApiTests(ModuleStoreTestCase): """ MOCK_CACHE = "openedx.core.djangoapps.content.course_structures.api.v0.api.cache" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + def setUp(self): """ Test setup diff --git a/openedx/core/djangoapps/credentials/tests/test_utils.py b/openedx/core/djangoapps/credentials/tests/test_utils.py index 8de680f79f..0d68827e9c 100644 --- a/openedx/core/djangoapps/credentials/tests/test_utils.py +++ b/openedx/core/djangoapps/credentials/tests/test_utils.py @@ -16,16 +16,20 @@ from openedx.core.djangoapps.credentials.utils import ( ) from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin from openedx.core.djangoapps.programs.models import ProgramsApiConfig +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @attr('shard_2') class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin, CredentialsDataMixin, - ProgramsDataMixin, TestCase): + ProgramsDataMixin, CacheIsolationTestCase): """ Tests covering the retrieval of user credentials from the Credentials service. """ + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestCredentialsRetrieval, self).setUp() diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index d68fe77b6a..99f4b363c6 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -55,6 +55,8 @@ class CreditApiTestBase(ModuleStoreTestCase): Base class for test cases of the credit API. """ + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] + PROVIDER_ID = "hogwarts" PROVIDER_NAME = "Hogwarts School of Witchcraft and Wizardry" PROVIDER_URL = "https://credit.example.com/request" diff --git a/openedx/core/djangoapps/credit/tests/test_partition.py b/openedx/core/djangoapps/credit/tests/test_partition.py index 96fe676ac1..2397f20764 100644 --- a/openedx/core/djangoapps/credit/tests/test_partition.py +++ b/openedx/core/djangoapps/credit/tests/test_partition.py @@ -31,6 +31,7 @@ class ReverificationPartitionTest(ModuleStoreTestCase): SUBMITTED = "submitted" APPROVED = "approved" DENIED = "denied" + ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] def setUp(self): super(ReverificationPartitionTest, self).setUp() diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 19a6638c80..245a9703b2 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -17,6 +17,7 @@ from openedx.core.djangoapps.programs import utils from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.tests import factories from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory, CourseEnrollmentFactory @@ -26,8 +27,11 @@ UTILS_MODULE = 'openedx.core.djangoapps.programs.utils' @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @attr('shard_2') class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, - CredentialsApiConfigMixin, TestCase): + CredentialsApiConfigMixin, CacheIsolationTestCase): """Tests covering the retrieval of programs from the Programs service.""" + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestProgramRetrieval, self).setUp() diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index 2cd04ea5d2..5ebe13efe5 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -25,6 +25,7 @@ from student.tests.factories import UserFactory from student.models import UserProfile, LanguageProficiency, PendingEmailChange from openedx.core.djangoapps.user_api.accounts import ACCOUNT_VISIBILITY_PREF_KEY from openedx.core.djangoapps.user_api.preferences.api import set_user_preference +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from .. import PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 01, tzinfo=UTC) @@ -121,10 +122,13 @@ class UserAPITestCase(APITestCase): clear=True ) @attr('shard_2') -class TestAccountAPI(UserAPITestCase): +class TestAccountAPI(CacheIsolationTestCase, UserAPITestCase): """ Unit tests for the Account API. """ + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestAccountAPI, self).setUp() @@ -244,7 +248,7 @@ class TestAccountAPI(UserAPITestCase): """ self.different_client.login(username=self.different_user.username, password=self.test_password) self.create_mock_profile(self.user) - with self.assertNumQueries(11): + with self.assertNumQueries(14): response = self.send_get(self.different_client) self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY) @@ -259,7 +263,7 @@ class TestAccountAPI(UserAPITestCase): """ self.different_client.login(username=self.different_user.username, password=self.test_password) self.create_mock_profile(self.user) - with self.assertNumQueries(11): + with self.assertNumQueries(14): response = self.send_get(self.different_client) self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY) @@ -307,11 +311,11 @@ class TestAccountAPI(UserAPITestCase): Test that a client (logged in) can get her own account information (using default legacy profile information, as created by the test UserFactory). """ - def verify_get_own_information(): + def verify_get_own_information(queries): """ Internal helper to perform the actual assertions """ - with self.assertNumQueries(9): + with self.assertNumQueries(queries): response = self.send_get(self.client) data = response.data self.assertEqual(17, len(data)) @@ -333,12 +337,12 @@ class TestAccountAPI(UserAPITestCase): self.assertEqual(False, data["accomplishments_shared"]) self.client.login(username=self.user.username, password=self.test_password) - verify_get_own_information() + verify_get_own_information(12) # Now make sure that the user can get the same information, even if not active self.user.is_active = False self.user.save() - verify_get_own_information() + verify_get_own_information(9) def test_get_account_empty_string(self): """ @@ -352,7 +356,7 @@ class TestAccountAPI(UserAPITestCase): legacy_profile.save() self.client.login(username=self.user.username, password=self.test_password) - with self.assertNumQueries(9): + with self.assertNumQueries(12): response = self.send_get(self.client) for empty_field in ("level_of_education", "gender", "country", "bio"): self.assertIsNone(response.data[empty_field]) diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index ad9177e19c..fb28687896 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -20,6 +20,7 @@ from social.apps.django_app.default.models import UserSocialAuth from django_comment_common import models from openedx.core.lib.api.test_utils import ApiTestCase, TEST_API_KEY +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin from third_party_auth.tests.utils import ( @@ -334,12 +335,14 @@ class UserViewSetTest(UserApiTestCase): ) -class UserPreferenceViewSetTest(UserApiTestCase): +class UserPreferenceViewSetTest(CacheIsolationTestCase, UserApiTestCase): """ Test cases covering the User Preference DRF view class and its various behaviors """ LIST_URI = USER_PREFERENCE_LIST_URI + ENABLED_CACHES = ['default'] + def setUp(self): super(UserPreferenceViewSetTest, self).setUp() self.detail_uri = self.get_uri_for_pref(self.prefs[0]) @@ -1725,12 +1728,16 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): @httpretty.activate @ddt.ddt -class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin): +class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin, CacheIsolationTestCase): """ Tests for the User API registration endpoint with 3rd party authentication. """ CREATE_USER = False + ENABLED_CACHES = ['default'] + + __test__ = False + def setUp(self): super(ThirdPartyRegistrationTestMixin, self).setUp() self.url = reverse('user_api_registration') @@ -1847,6 +1854,8 @@ class TestFacebookRegistrationView( ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase ): """Tests the User API registration endpoint with Facebook authentication.""" + __test__ = True + def test_social_auth_exception(self): """ According to the do_auth method in social.backends.facebook.py, @@ -1863,6 +1872,8 @@ class TestGoogleRegistrationView( ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase ): """Tests the User API registration endpoint with Google authentication.""" + __test__ = True + pass diff --git a/openedx/core/djangolib/testing/__init__.py b/openedx/core/djangolib/testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangolib/testing/utils.py b/openedx/core/djangolib/testing/utils.py new file mode 100644 index 0000000000..4feb1749c3 --- /dev/null +++ b/openedx/core/djangolib/testing/utils.py @@ -0,0 +1,122 @@ +from django.core.cache import caches +from django.test import TestCase, override_settings +from django.conf import settings +from django.contrib import sites + +from request_cache.middleware import RequestCache + + +class CacheIsolationMixin(object): + """ + This class can be used to enable specific django caches for + specific the TestCase that it's mixed into. + + Usage: + + Use the ENABLED_CACHES to list the names of caches that should + be enabled in the context of this TestCase. These caches will + use a loc_mem_cache with the default settings. + + Set the class variable CACHES to explicitly specify the cache settings + that should be overridden. This class will insert those values into + django.conf.settings, and will reset all named caches before each + test. + + If both CACHES and ENABLED_CACHES are not None, raises an error. + """ + + CACHES = None + ENABLED_CACHES = None + __settings_override = None + + @classmethod + def start_cache_isolation(cls): + """ + Start cache isolation by overriding the settings.CACHES and + flushing the cache. + """ + cache_settings = None + if cls.CACHES is not None and cls.ENABLED_CACHES is not None: + raise Exception( + "Use either CACHES or ENABLED_CACHES, but not both" + ) + + if cls.CACHES is not None: + cache_settings = cls.CACHES + elif cls.ENABLED_CACHES is not None: + cache_settings = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } + } + + cache_settings.update({ + cache_name: { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': cache_name, + 'KEY_FUNCTION': 'util.memcache.safe_key', + } for cache_name in cls.ENABLED_CACHES + }) + + if cache_settings is None: + return + + cls.__settings_override = override_settings(CACHES=cache_settings) + cls.__settings_override.__enter__() + + # Start with empty caches + cls.clear_caches() + + @classmethod + def end_cache_isolation(cls): + """ + End cache isolation by flushing the cache and then returning + settings.CACHES to its original state. + """ + # Make sure that cache contents don't leak out after the isolation is ended + cls.clear_caches() + + if cls.__settings_override is not None: + cls.__settings_override.__exit__(None, None, None) + cls.__settings_override = None + + @classmethod + def clear_caches(cls): + """ + Clear all of the caches defined in settings.CACHES. + """ + # N.B. As of 2016-04-20, Django won't return any caches + # from django.core.cache.caches.all() that haven't been + # accessed using caches[name] previously, so we loop + # over our list of overridden caches, instead. + for cache in settings.CACHES: + caches[cache].clear() + + # The sites framework caches in a module-level dictionary. + # Clear that. + sites.models.SITE_CACHE.clear() + + RequestCache.clear_request_cache() + + +class CacheIsolationTestCase(CacheIsolationMixin, TestCase): + """ + A TestCase that isolates caches (as described in + :py:class:`CacheIsolationMixin`) at class setup, and flushes the cache + between every test. + """ + @classmethod + def setUpClass(cls): + super(CacheIsolationTestCase, cls).setUpClass() + cls.start_cache_isolation() + + @classmethod + def tearDownClass(cls): + cls.end_cache_isolation() + super(CacheIsolationTestCase, cls).tearDownClass() + + def setUp(self): + super(CacheIsolationTestCase, self).setUp() + + self.clear_caches() + self.addCleanup(self.clear_caches) diff --git a/openedx/core/lib/tests/test_edx_api_utils.py b/openedx/core/lib/tests/test_edx_api_utils.py index 1570c0b4bb..76b6b8b563 100644 --- a/openedx/core/lib/tests/test_edx_api_utils.py +++ b/openedx/core/lib/tests/test_edx_api_utils.py @@ -15,6 +15,7 @@ from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.lib.edx_api_utils import get_edx_api_data from student.tests.factories import UserFactory @@ -24,8 +25,11 @@ LOGGER_NAME = 'openedx.core.lib.edx_api_utils' @attr('shard_2') class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, ProgramsApiConfigMixin, ProgramsDataMixin, - TestCase): + CacheIsolationTestCase): """Test utility for API data retrieval.""" + + ENABLED_CACHES = ['default'] + def setUp(self): super(TestApiDataRetrieval, self).setUp() ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)