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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user