There are a number of Django Signals that are on the modulestore's
SignalHandler class, such as SignalHandler.course_published. These
signals can trigger very expensive processes to occur, such as course
overview or block structures generation. Most of the time, the test
author doesn't care about these side-effects.
This commit does a few things:
* Converts the signals on SignalHandler to be instances of a new
SwitchedSignal class, that allows signal sending to be disabled.
* Creates a SignalIsolationMixin helper similar in spirit to the
CacheIsolationMixin, and adds it to the ModuleStoreIsolationMixin
(and thus to ModuleStoreTestCase and SharedModuleStoreTestCase).
* Converts our various tests to use this new mechanism. In some cases,
this means adjusting query counts downwards because they no longer
have to account for publishing listener actions.
Modulestore generated signals are now muted by default during test runs.
Calls to send() them will result in no-ops. You can choose to enable
specific signals for a given subclass of ModuleStoreTestCase or
SharedModuleStoreTestCase by specifying an ENABLED_SIGNALS class
attribute, like the following example:
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class MyPublishTestCase(ModuleStoreTestCase):
ENABLED_SIGNALS = ['course_published', 'pre_publish']
You should take great care when disabling signals outside of a
ModuleStoreTestCase or SharedModuleStoreTestCase, since they can leak
out into other tests. Be sure to always clean up, and never disable
signals outside of testing. Because signals are essentially process
globals, it can have a lot of unpleasant side-effects if we start
mucking around with them during live requests.
Overall, this change has cut the total test execution time for
edx-platform by a bit over a third, though we still spend a lot in
pre-test setup during our test builds.
[PERF-413]
107 lines
4.5 KiB
Python
107 lines
4.5 KiB
Python
"""
|
|
Tests for celery tasks defined in tasks module
|
|
"""
|
|
|
|
from mock_django import mock_signal_receiver
|
|
|
|
from lms.djangoapps.ccx.tests.factories import CcxFactory
|
|
from student.roles import CourseCcxCoachRole
|
|
from student.tests.factories import (
|
|
AdminFactory,
|
|
)
|
|
from xmodule.modulestore.django import SignalHandler
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
from xmodule.modulestore.tests.django_utils import (
|
|
ModuleStoreTestCase,
|
|
TEST_DATA_SPLIT_MODULESTORE)
|
|
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
|
|
from ccx_keys.locator import CCXLocator
|
|
|
|
from lms.djangoapps.ccx.tasks import send_ccx_course_published
|
|
|
|
|
|
class TestSendCCXCoursePublished(ModuleStoreTestCase):
|
|
"""unit tests for the send ccx course published task
|
|
"""
|
|
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
|
|
|
ENABLED_SIGNALS = ['course_published']
|
|
|
|
def setUp(self):
|
|
"""
|
|
Set up tests
|
|
"""
|
|
super(TestSendCCXCoursePublished, self).setUp()
|
|
course = self.course = CourseFactory.create(org="edX", course="999", display_name="Run 666")
|
|
course2 = self.course2 = CourseFactory.create(org="edX", course="999a", display_name="Run 667")
|
|
coach = AdminFactory.create()
|
|
role = CourseCcxCoachRole(course.id)
|
|
role.add_users(coach)
|
|
self.ccx = CcxFactory(course_id=course.id, coach=coach)
|
|
self.ccx2 = CcxFactory(course_id=course.id, coach=coach)
|
|
self.ccx3 = CcxFactory(course_id=course.id, coach=coach)
|
|
self.ccx4 = CcxFactory(course_id=course2.id, coach=coach)
|
|
|
|
def call_fut(self, course_key):
|
|
"""Call the function under test
|
|
"""
|
|
send_ccx_course_published(unicode(course_key))
|
|
|
|
def test_signal_not_sent_for_ccx(self):
|
|
"""Check that course published signal is not sent when course key is for a ccx
|
|
"""
|
|
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
|
|
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
|
self.call_fut(course_key)
|
|
self.assertEqual(receiver.call_count, 0)
|
|
|
|
def test_signal_sent_for_ccx(self):
|
|
"""Check that course published signal is sent when course key is not for a ccx.
|
|
We have 4 ccx's, but only 3 are derived from the course id used here, so call
|
|
count must be 3 to confirm that all derived courses and no more got the signal.
|
|
"""
|
|
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
|
self.call_fut(self.course.id)
|
|
self.assertEqual(receiver.call_count, 3)
|
|
|
|
def test_course_structure_generated(self):
|
|
"""Check that course structure is generated after course published signal is sent
|
|
"""
|
|
ccx_structure = {
|
|
u"blocks": {
|
|
u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course": {
|
|
u"block_type": u"course",
|
|
u"graded": False,
|
|
u"format": None,
|
|
u"usage_key": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course",
|
|
u"children": [
|
|
],
|
|
u"display_name": u"Run 666"
|
|
}
|
|
},
|
|
u"root": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course"
|
|
}
|
|
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
|
|
structure = CourseStructure.objects.filter(course_id=course_key)
|
|
# no structure exists before signal is called
|
|
self.assertEqual(len(structure), 0)
|
|
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
|
self.call_fut(self.course.id)
|
|
self.assertEqual(receiver.call_count, 3)
|
|
structure = CourseStructure.objects.get(course_id=course_key)
|
|
self.assertEqual(structure.structure, ccx_structure)
|
|
|
|
def test_course_overview_cached(self):
|
|
"""Check that course overview is cached after course published signal is sent
|
|
"""
|
|
course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
|
|
overview = CourseOverview.objects.filter(id=course_key)
|
|
self.assertEqual(len(overview), 0)
|
|
with mock_signal_receiver(SignalHandler.course_published) as receiver:
|
|
self.call_fut(self.course.id)
|
|
self.assertEqual(receiver.call_count, 3)
|
|
overview = CourseOverview.objects.filter(id=course_key)
|
|
self.assertEqual(len(overview), 1)
|