testing.py 5.95 KB
Newer Older
1 2 3 4 5
"""
Utility Mixins for unit tests
"""

import json
6 7 8
import sys

from django.conf import settings
9
from django.core.urlresolvers import clear_url_caches, resolve
10
from django.test import TestCase
11
from mock import patch
12

13
from util.db import CommitOnSuccessManager, OuterAtomic
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30


class UrlResetMixin(object):
    """Mixin to reset urls.py before and after a test

    Django memoizes the function that reads the urls module (whatever module
    urlconf names). The module itself is also stored by python in sys.modules.
    To fully reload it, we need to reload the python module, and also clear django's
    cache of the parsed urls.

    However, the order in which we do this doesn't matter, because neither one will
    get reloaded until the next request

    Doing this is expensive, so it should only be added to tests that modify settings
    that affect the contents of urls.py
    """

31 32
    URLCONF_MODULES = None

33
    def reset_urls(self, urlconf_modules=None):
34
        """Reset `urls.py` for a set of Django apps."""
35 36 37 38 39 40

        if urlconf_modules is None:
            urlconf_modules = [settings.ROOT_URLCONF]
            if self.URLCONF_MODULES is not None:
                urlconf_modules.extend(self.URLCONF_MODULES)

41 42 43
        for urlconf in urlconf_modules:
            if urlconf in sys.modules:
                reload(sys.modules[urlconf])
44 45
        clear_url_caches()

46 47 48
        # Resolve a URL so that the new urlconf gets loaded
        resolve('/')

49
    def setUp(self):
50 51 52
        """Reset Django urls before tests and after tests

        If you need to reset `urls.py` from a particular Django app (or apps),
53
        specify these modules by setting the URLCONF_MODULES class attribute.
54 55 56 57

        Examples:

            # Reload only the root urls.py
58
            URLCONF_MODULES = None
59 60

            # Reload urls from my_app
61
            URLCONF_MODULES = ['myapp.url']
62 63

            # Reload urls from my_app and another_app
64
            URLCONF_MODULES = ['myapp.url', 'another_app.urls']
65 66

        """
67
        super(UrlResetMixin, self).setUp()
68

69 70
        self.reset_urls()
        self.addCleanup(self.reset_urls)
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98


class EventTestMixin(object):
    """
    Generic mixin for verifying that events were emitted during a test.
    """
    def setUp(self, tracker):
        super(EventTestMixin, self).setUp()
        self.tracker = tracker
        patcher = patch(self.tracker)
        self.mock_tracker = patcher.start()
        self.addCleanup(patcher.stop)

    def assert_no_events_were_emitted(self):
        """
        Ensures no events were emitted since the last event related assertion.
        """
        self.assertFalse(self.mock_tracker.emit.called)  # pylint: disable=maybe-no-member

    def assert_event_emitted(self, event_name, **kwargs):
        """
        Verify that an event was emitted with the given parameters.
        """
        self.mock_tracker.emit.assert_any_call(  # pylint: disable=maybe-no-member
            event_name,
            kwargs
        )

99 100 101 102 103 104 105 106 107 108 109
    def assert_event_emission_count(self, event_name, expected_count):
        """
        Verify that the event with the given name was emitted
        a specific number of times.
        """
        actual_count = 0
        for call_args in self.mock_tracker.emit.call_args_list:
            if call_args[0][0] == event_name:
                actual_count += 1
        self.assertEqual(actual_count, expected_count)

110 111 112 113 114
    def reset_tracker(self):
        """
        Reset the mock tracker in order to forget about old events.
        """
        self.mock_tracker.reset_mock()
115

116 117 118 119 120 121
    def get_latest_call_args(self):
        """
        Return the arguments of the latest call to emit.
        """
        return self.mock_tracker.emit.call_args[0]

122 123 124 125 126 127

class PatchMediaTypeMixin(object):
    """
    Generic mixin for verifying unsupported media type in PATCH
    """
    def test_patch_unsupported_media_type(self):
128
        response = self.client.patch(
129 130 131 132 133
            self.url,
            json.dumps({}),
            content_type=self.unsupported_media_type
        )
        self.assertEqual(response.status_code, 415)
134 135


136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
def patch_testcase():
    """
    Disable commit_on_success decorators for tests in TestCase subclasses.

    Since tests in TestCase classes are wrapped in an atomic block, we
    cannot use transaction.commit() or transaction.rollback().
    https://docs.djangoproject.com/en/1.8/topics/testing/tools/#django.test.TransactionTestCase
    """

    def enter_atomics_wrapper(wrapped_func):
        """
        Wrapper for TestCase._enter_atomics
        """
        wrapped_func = wrapped_func.__func__

        def _wrapper(*args, **kwargs):
            """
            Method that performs atomic-entering accounting.
            """
            CommitOnSuccessManager.ENABLED = False
            OuterAtomic.ALLOW_NESTED = True
            if not hasattr(OuterAtomic, 'atomic_for_testcase_calls'):
                OuterAtomic.atomic_for_testcase_calls = 0
            OuterAtomic.atomic_for_testcase_calls += 1
            return wrapped_func(*args, **kwargs)
        return classmethod(_wrapper)

    def rollback_atomics_wrapper(wrapped_func):
        """
        Wrapper for TestCase._rollback_atomics
        """
        wrapped_func = wrapped_func.__func__

        def _wrapper(*args, **kwargs):
            """
            Method that performs atomic-rollback accounting.
            """
            CommitOnSuccessManager.ENABLED = True
            OuterAtomic.ALLOW_NESTED = False
            OuterAtomic.atomic_for_testcase_calls -= 1
            return wrapped_func(*args, **kwargs)
        return classmethod(_wrapper)

    # pylint: disable=protected-access
    TestCase._enter_atomics = enter_atomics_wrapper(TestCase._enter_atomics)
    TestCase._rollback_atomics = rollback_atomics_wrapper(TestCase._rollback_atomics)
182 183 184 185 186 187 188 189


def patch_sessions():
    """
    Override the Test Client's session and login to support safe cookies.
    """
    from openedx.core.djangoapps.safe_sessions.testing import safe_cookie_test_session_patch
    safe_cookie_test_session_patch()