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

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