diff --git a/lti_consumer/plugin/compat.py b/lti_consumer/plugin/compat.py
new file mode 100644
index 0000000000000000000000000000000000000000..68994bf45994b3d7558e468eb2c02a513097d95f
--- /dev/null
+++ b/lti_consumer/plugin/compat.py
@@ -0,0 +1,21 @@
+"""
+Compatibility layer to isolate core-platform method calls from implementation.
+"""
+
+
+def run_xblock_handler(*args, **kwargs):
+    """
+    Import and run `handle_xblock_callback` from LMS
+    """
+    # pylint: disable=import-error,import-outside-toplevel
+    from lms.djangoapps.courseware.module_render import handle_xblock_callback
+    return handle_xblock_callback(*args, **kwargs)
+
+
+def run_xblock_handler_noauth(*args, **kwargs):
+    """
+    Import and run `handle_xblock_callback_noauth` from LMS
+    """
+    # pylint: disable=import-error,import-outside-toplevel
+    from lms.djangoapps.courseware.module_render import handle_xblock_callback_noauth
+    return handle_xblock_callback_noauth(*args, **kwargs)
diff --git a/lti_consumer/plugin/views.py b/lti_consumer/plugin/views.py
index 755a559c8d1c62ebf935a28090759fb32ec82b3a..78174a50090221e8debafd443219ed350145ef90 100644
--- a/lti_consumer/plugin/views.py
+++ b/lti_consumer/plugin/views.py
@@ -7,9 +7,9 @@ from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_http_methods
 
 from opaque_keys.edx.keys import UsageKey
-from lms.djangoapps.courseware.module_render import (  # pylint: disable=import-error
-    handle_xblock_callback,
-    handle_xblock_callback_noauth,
+from lti_consumer.plugin.compat import (
+    run_xblock_handler,
+    run_xblock_handler_noauth,
 )
 
 
@@ -25,13 +25,13 @@ def public_keyset_endpoint(request, usage_id=None):
     try:
         usage_key = UsageKey.from_string(usage_id)
 
-        return handle_xblock_callback_noauth(
+        return run_xblock_handler_noauth(
             request=request,
             course_id=str(usage_key.course_key),
             usage_id=str(usage_key),
             handler='public_keyset_endpoint'
         )
-    except:  # pylint: disable=bare-except
+    except Exception:  # pylint: disable=broad-except
         return HttpResponse(status=404)
 
 
@@ -49,14 +49,14 @@ def launch_gate_endpoint(request, suffix):
             request.GET.get('login_hint')
         )
 
-        return handle_xblock_callback(
+        return run_xblock_handler(
             request=request,
             course_id=str(usage_key.course_key),
             usage_id=str(usage_key),
             handler='lti_1p3_launch_callback',
             suffix=suffix
         )
-    except:  # pylint: disable=bare-except
+    except Exception:  # pylint: disable=broad-except
         return HttpResponse(status=404)
 
 
@@ -69,11 +69,11 @@ def access_token_endpoint(request, usage_id=None):
     try:
         usage_key = UsageKey.from_string(usage_id)
 
-        return handle_xblock_callback_noauth(
+        return run_xblock_handler_noauth(
             request=request,
             course_id=str(usage_key.course_key),
             usage_id=str(usage_key),
             handler='lti_1p3_access_token'
         )
-    except:  # pylint: disable=bare-except
+    except Exception:  # pylint: disable=broad-except
         return HttpResponse(status=404)
diff --git a/lti_consumer/tests/unit/plugin/__init__.py b/lti_consumer/tests/unit/plugin/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lti_consumer/tests/unit/plugin/test_views.py b/lti_consumer/tests/unit/plugin/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..21345f973626a1ce5161b4e880b9423c6bfca2a3
--- /dev/null
+++ b/lti_consumer/tests/unit/plugin/test_views.py
@@ -0,0 +1,133 @@
+"""
+Tests for LTI 1.3 endpoint views.
+"""
+from mock import patch
+
+from django.http import HttpResponse
+from django.test.testcases import TestCase
+
+
+class TestLti1p3KeysetEndpoint(TestCase):
+    """
+    Test `public_keyset_endpoint` method.
+    """
+    def setUp(self):
+        super(TestLti1p3KeysetEndpoint, self).setUp()
+
+        self.location = 'block-v1:course+test+2020+type@problem+block@test'
+        self.url = '/lti_consumer/v1/public_keysets/{}'.format(self.location)
+
+        # Patch settings calls to LMS method
+        xblock_handler_patcher = patch(
+            'lti_consumer.plugin.views.run_xblock_handler_noauth',
+            return_value=HttpResponse()
+        )
+        self.addCleanup(xblock_handler_patcher.stop)
+        self._mock_xblock_handler = xblock_handler_patcher.start()
+
+    def test_public_keyset_endpoint(self):
+        """
+        Check that the keyset endpoint maps correctly to the
+        `public_keyset_endpoint` XBlock handler endpoint.
+        """
+        response = self.client.get(self.url)
+
+        # Check response
+        self.assertEqual(response.status_code, 200)
+        # Check function call arguments
+        self._mock_xblock_handler.assert_called_once()
+        kwargs = self._mock_xblock_handler.call_args.kwargs
+        self.assertEqual(kwargs['usage_id'], self.location)
+        self.assertEqual(kwargs['handler'], 'public_keyset_endpoint')
+
+    def test_invalid_usage_key(self):
+        """
+        Check invalid methods yield HTTP code 404.
+        """
+        response = self.client.get('/lti_consumer/v1/public_keysets/invalid-key')
+        self.assertEqual(response.status_code, 404)
+
+
+class TestLti1p3LaunchGateEndpoint(TestCase):
+    """
+    Test `launch_gate_endpoint` method.
+    """
+    def setUp(self):
+        super(TestLti1p3LaunchGateEndpoint, self).setUp()
+
+        self.location = 'block-v1:course+test+2020+type@problem+block@test'
+        self.url = '/lti_consumer/v1/launch/'
+        self.request = {'login_hint': self.location}
+
+        # Patch settings calls to LMS method
+        xblock_handler_patcher = patch(
+            'lti_consumer.plugin.views.run_xblock_handler',
+            return_value=HttpResponse()
+        )
+        self.addCleanup(xblock_handler_patcher.stop)
+        self._mock_xblock_handler = xblock_handler_patcher.start()
+
+    def test_launch_gate(self):
+        """
+        Check that the launch endpoint correctly maps to the
+        `lti_1p3_launch_callback` XBlock handler.
+        """
+        response = self.client.get(self.url, self.request)
+
+        # Check response
+        self.assertEqual(response.status_code, 200)
+        # Check function call arguments
+        self._mock_xblock_handler.assert_called_once()
+        kwargs = self._mock_xblock_handler.call_args.kwargs
+        self.assertEqual(kwargs['usage_id'], self.location)
+        self.assertEqual(kwargs['handler'], 'lti_1p3_launch_callback')
+
+    def test_invalid_usage_key(self):
+        """
+        Check that passing a invalid login_hint yields HTTP code 404.
+        """
+        self._mock_xblock_handler.side_effect = Exception()
+        response = self.client.get(self.url, self.request)
+        self.assertEqual(response.status_code, 404)
+
+
+class TestLti1p3AccessTokenEndpoint(TestCase):
+    """
+    Test `access_token_endpoint` method.
+    """
+    def setUp(self):
+        super(TestLti1p3AccessTokenEndpoint, self).setUp()
+
+        self.location = 'block-v1:course+test+2020+type@problem+block@test'
+        self.url = '/lti_consumer/v1/token/{}'.format(self.location)
+
+        # Patch settings calls to LMS method
+        xblock_handler_patcher = patch(
+            'lti_consumer.plugin.views.run_xblock_handler_noauth',
+            return_value=HttpResponse()
+        )
+        self.addCleanup(xblock_handler_patcher.stop)
+        self._mock_xblock_handler = xblock_handler_patcher.start()
+
+    def test_access_token_endpoint(self):
+        """
+        Check that the keyset endpoint is correctly mapping to the
+        `lti_1p3_access_token` XBlock handler.
+        """
+        response = self.client.post(self.url)
+
+        # Check response
+        self.assertEqual(response.status_code, 200)
+        # Check function call arguments
+        self._mock_xblock_handler.assert_called_once()
+        kwargs = self._mock_xblock_handler.call_args.kwargs
+        self.assertEqual(kwargs['usage_id'], self.location)
+        self.assertEqual(kwargs['handler'], 'lti_1p3_access_token')
+
+    def test_invalid_usage_key(self):
+        """
+        Check invalid methods yield HTTP code 404.
+        """
+        self._mock_xblock_handler.side_effect = Exception()
+        response = self.client.post(self.url)
+        self.assertEqual(response.status_code, 404)
diff --git a/test.py b/test.py
index efce05d858e922d0186df9fe3db7e6dd2baaec28..eb32b0414d31a1f1ff415be54bc7f526c47f4d52 100644
--- a/test.py
+++ b/test.py
@@ -8,7 +8,7 @@ import os
 import sys
 
 if __name__ == '__main__':
-    os.environ.setdefault('DJANGO_SETTINGS_MODULE', u'workbench.settings')
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', u'test_settings')
 
     try:
         from django.conf import settings  # pylint: disable=wrong-import-position
diff --git a/test_settings.py b/test_settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6e2f7fa10b6833d1007b576207f09dcc60e54bf
--- /dev/null
+++ b/test_settings.py
@@ -0,0 +1,11 @@
+"""
+Custom testing settings for testing views
+"""
+from workbench.settings import *
+
+
+# Usage id pattern (from edx-platform)
+USAGE_ID_PATTERN = r'(?P<usage_id>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
+
+# Keep settings, use different ROOT_URLCONF
+ROOT_URLCONF = 'test_urls'
diff --git a/test_urls.py b/test_urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..66f5f749dd579ff0866ae9391bbee5191546f3b4
--- /dev/null
+++ b/test_urls.py
@@ -0,0 +1,9 @@
+"""
+Custom URL patterns for testing
+"""
+from django.conf.urls import include, re_path
+
+urlpatterns = [
+    re_path(r'^', include('workbench.urls')),
+    re_path(r'^', include('lti_consumer.plugin.urls')),
+]