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]
This commit is contained in:
Calen Pennington
2016-04-14 16:21:54 -04:00
parent 18e1610043
commit 853bfe7a36
47 changed files with 624 additions and 272 deletions

View File

@@ -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/')

View File

@@ -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')

View File

@@ -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})

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 = <settings for the modulestore to test>
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():
<all the cls.setUpClass() setup code that performs modulestore setup...>
@classmethod
def setUpTestData(cls):
<all the setup code that creates Django models per test class...>
<these models can use variables (courses) setup in setUpClass() above>
"""
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.