Commit a33ad617 by Will Daly

Refactor test setup for XBlock rendering and handlers.

parent e44b501c
"""
Base class for handler-level testing of the XBlock.
"""
import os.path
import json
from functools import wraps
from django.test import TestCase
from workbench.runtime import WorkbenchRuntime
import webob
def scenario(scenario_path, user_id=None):
"""
Method decorator to load a scenario for a test case.
Must be called on an `XBlockHandlerTestCase` subclass, or
else it will have no effect.
Args:
scenario_path (str): Path to the scenario XML file.
Kwargs:
user_id (str or None): User ID to log in as, or None.
Returns:
The decorated method
Example:
@scenario('data/test_scenario.xml')
def test_submit(self, xblock):
response = self.request(xblock, 'submit', 'Test submission')
self.assertTrue('Success' in response)
"""
def _decorator(func):
@wraps(func)
def _wrapped(*args, **kwargs):
# Retrieve the object (self)
# if this is a function, not a method, then do nothing.
xblock = None
if args:
self = args[0]
if isinstance(self, XBlockHandlerTestCase):
# Configure the runtime with our user id
self.set_user(user_id)
# Load the scenario
xblock = self.load_scenario(scenario_path)
# Pass the XBlock as the first argument to the decorated method (after `self`)
args = list(args)
args.insert(1, xblock)
return func(*args, **kwargs)
return _wrapped
return _decorator
class XBlockHandlerTestCase(TestCase):
"""
Load the XBlock in the workbench runtime to test its handler.
"""
def setUp(self):
"""
Create the runtime.
"""
self.runtime = WorkbenchRuntime()
def set_user(self, user_id):
"""
Provide a user ID to the runtime.
Args:
user_id (str): a user ID.
Returns:
None
"""
self.runtime.user_id = user_id
def load_scenario(self, xml_path):
"""
Load an XML definition of an XBlock and return the XBlock instance.
Args:
xml (string): Path to an XML definition of the XBlock, relative
to the test module.
Returns:
XBlock
"""
base_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(base_dir, xml_path)) as xml_file:
block_id = self.runtime.parse_xml_string(
xml_file.read(), self.runtime.id_generator
)
return self.runtime.get_block(block_id)
def request(self, xblock, handler_name, content, response_format=None):
"""
Make a request to an XBlock handler.
Args:
xblock (XBlock): The XBlock instance that should handle the request.
handler_name (str): The name of the handler.
content (unicode): Content of the request.
Kwargs:
response_format (None or str): Expected format of the response string.
If `None`, return the raw response content; if 'json', parse the
response as JSON and return the result.
Raises:
NotImplementedError: Response format not supported.
Returns:
Content of the response (mixed).
"""
# Create a fake request
request = webob.Request(dict())
request.body = content
# Send the request to the XBlock handler
response = self.runtime.handle(xblock, handler_name, request)
# Parse the response (if a format is specified)
if response_format is None:
return response.body
elif response_format == 'json':
return json.loads(response.body)
else:
raise NotImplementedError("Response format '{format}' not supported".format(response_format))
<openassessment start="2014-12-19T23:00:00" due="2014-12-21T23:00:00">
<prompt>
Given the state of the world today, what do you think should be done to
combat poverty? Please answer in a short essay of 200-300 words.
</prompt>
<rubric>
Read for conciseness, clarity of thought, and form.
<criterion name="concise">
How concise is it?
<option val="0">Neal Stephenson (late)</option>
<option val="1">HP Lovecraft</option>
<option val="3">Robert Heinlein</option>
<option val="4">Neal Stephenson (early)</option>
<option val="5">Earnest Hemingway</option>
</criterion>
<criterion name="clearheaded">
How clear is the thinking?
<option val="0">Yogi Berra</option>
<option val="1">Hunter S. Thompson</option>
<option val="2">Robert Heinlein</option>
<option val="3">Isaac Asimov</option>
<option val="10">Spock</option>
</criterion>
<criterion name="form">
Lastly, how is it's form? Punctuation, grammar, and spelling all count.
<option val="0">lolcats</option>
<option val="1">Facebook</option>
<option val="2">Reddit</option>
<option val="3">metafilter</option>
<option val="4">Usenet, 1996</option>
<option val="5">The Elements of Style</option>
</criterion>
</rubric>
<assessments>
<peer-assessment name="peer-assessment"
start="2014-12-20T19:00"
due="2014-12-21T22:22"
must_grade="5"
must_be_graded_by="3" />
<self-assessment name="self-assessment"/>
</assessments>
</openassessment>
......@@ -4,164 +4,91 @@ Tests the Open Assessment XBlock functionality.
import json
import datetime
from django.test import TestCase
from mock import patch
from workbench.runtime import WorkbenchRuntime
import webob
from openassessment.xblock.submission_mixin import SubmissionMixin
from submissions import api as sub_api
from submissions.api import SubmissionRequestError, SubmissionInternalError
RUBRIC_CONFIG = """
<openassessment start="2014-12-19T23:00:00" due="2014-12-21T23:00:00">
<prompt>
Given the state of the world today, what do you think should be done to
combat poverty? Please answer in a short essay of 200-300 words.
</prompt>
<rubric>
Read for conciseness, clarity of thought, and form.
<criterion name="concise">
How concise is it?
<option val="0">Neal Stephenson (late)</option>
<option val="1">HP Lovecraft</option>
<option val="3">Robert Heinlein</option>
<option val="4">Neal Stephenson (early)</option>
<option val="5">Earnest Hemingway</option>
</criterion>
<criterion name="clearheaded">
How clear is the thinking?
<option val="0">Yogi Berra</option>
<option val="1">Hunter S. Thompson</option>
<option val="2">Robert Heinlein</option>
<option val="3">Isaac Asimov</option>
<option val="10">Spock</option>
</criterion>
<criterion name="form">
Lastly, how is it's form? Punctuation, grammar, and spelling all count.
<option val="0">lolcats</option>
<option val="1">Facebook</option>
<option val="2">Reddit</option>
<option val="3">metafilter</option>
<option val="4">Usenet, 1996</option>
<option val="5">The Elements of Style</option>
</criterion>
</rubric>
<assessments>
<peer-assessment name="peer-assessment"
start="2014-12-20T19:00"
due="2014-12-21T22:22"
must_grade="5"
must_be_graded_by="3" />
<self-assessment name="self-assessment"/>
</assessments>
</openassessment>
"""
from .base import XBlockHandlerTestCase, scenario
class TestOpenAssessment(TestCase):
runtime = None
assessment = None
class TestOpenAssessment(XBlockHandlerTestCase):
def setUp(self):
self.runtime = WorkbenchRuntime()
self.runtime.user_id = "Bob"
assessment_id = self.runtime.parse_xml_string(
RUBRIC_CONFIG, self.runtime.id_generator)
self.assessment = self.runtime.get_block(assessment_id)
self.default_json_submission = json.dumps({"submission": "This is my answer to this test question!"})
SUBMISSION = json.dumps({"submission": "This is my answer to this test question!"})
def make_request(self, body):
"""Mock request method."""
request = webob.Request({})
request.body = body
return request
def test_submit_submission(self):
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_submit_submission(self, xblock):
"""XBlock accepts response, returns true on success"""
# This one should pass because we haven't submitted before
resp = self.runtime.handle(
self.assessment, 'submit',
self.make_request(self.default_json_submission)
)
result = json.loads(resp.body)
self.assertTrue(result[0])
def test_submission_multisubmit_failure(self):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertTrue(resp[0])
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_submission_multisubmit_failure(self, xblock):
"""XBlock returns true on first, false on second submission"""
# We don't care about return value of first one
resp = self.runtime.handle(
self.assessment, 'submit',
self.make_request(self.default_json_submission)
)
self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
# This one should fail becaus we're not allowed to submit multiple times
resp = self.runtime.handle(
self.assessment, 'submit',
self.make_request(self.default_json_submission)
)
result = json.loads(resp.body)
self.assertFalse(result[0])
self.assertEqual(result[1], "ENOMULTI")
self.assertEqual(result[2], self.assessment.submit_errors["ENOMULTI"])
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "ENOMULTI")
self.assertEqual(resp[2], xblock.submit_errors["ENOMULTI"])
@scenario('data/basic_scenario.xml')
@patch.object(sub_api, 'create_submission')
def test_submission_general_failure(self, mock_submit):
def test_submission_general_failure(self, xblock, mock_submit):
"""Internal errors return some code for submission failure."""
mock_submit.side_effect = SubmissionInternalError("Cat on fire.")
resp = self.runtime.handle(
self.assessment, 'submit',
self.make_request(self.default_json_submission)
)
result = json.loads(resp.body)
self.assertFalse(result[0])
self.assertEqual(result[1], "EUNKNOWN")
self.assertEqual(result[2], SubmissionMixin().submit_errors["EUNKNOWN"])
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "EUNKNOWN")
self.assertEqual(resp[2], SubmissionMixin().submit_errors["EUNKNOWN"])
@scenario('data/basic_scenario.xml')
@patch.object(sub_api, 'create_submission')
def test_submission_API_failure(self, mock_submit):
def test_submission_API_failure(self, xblock, mock_submit):
"""API usage errors return code and meaningful message."""
mock_submit.side_effect = SubmissionRequestError("Cat on fire.")
resp = self.runtime.handle(
self.assessment, 'submit',
self.make_request(self.default_json_submission)
)
result = json.loads(resp.body)
self.assertFalse(result[0])
self.assertEqual(result[1], "EBADFORM")
self.assertEqual(result[2], "Cat on fire.")
def test_load_student_view(self):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "EBADFORM")
self.assertEqual(resp[2], "Cat on fire.")
@scenario('data/basic_scenario.xml')
def test_load_student_view(self, xblock):
"""OA XBlock returns some HTML to the user.
View basic test for verifying we're returned some HTML about the
Open Assessment XBlock. We don't want to match too heavily against the
contents.
"""
xblock_fragment = self.runtime.render(self.assessment, "student_view")
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertTrue(xblock_fragment.body_html().find("Openassessmentblock"))
# Validate Submission Rendering.
submission_response = self.assessment.render_submission({})
submission_response = xblock.render_submission({})
self.assertIsNotNone(submission_response)
self.assertTrue(submission_response.body.find("openassessment__response"))
# Validate Peer Rendering.
peer_response = self.assessment.render_peer_assessment({})
peer_response = xblock.render_peer_assessment({})
self.assertIsNotNone(peer_response)
self.assertTrue(peer_response.body.find("openassessment__peer-assessment"))
# Validate Self Rendering.
self_response = self.assessment.render_self_assessment({})
self_response = xblock.render_self_assessment({})
self.assertIsNotNone(self_response)
self.assertTrue(self_response.body.find("openassessment__peer-assessment"))
# Validate Grading.
grade_response = self.assessment.render_grade({})
grade_response = xblock.render_grade({})
self.assertIsNotNone(grade_response)
self.assertTrue(grade_response.body.find("openassessment__grade"))
def test_start_end_date_checks(self):
@scenario('data/basic_scenario.xml')
def test_start_end_date_checks(self, xblock):
"""
Check if the start and end date checks work appropriately.
"""
......@@ -169,20 +96,20 @@ class TestOpenAssessment(TestCase):
past = now - datetime.timedelta(minutes = 10)
future = now + datetime.timedelta(minutes = 10)
way_future = now + datetime.timedelta(minutes = 20)
self.assessment.start_datetime = past.isoformat()
self.assessment.due_datetime = past.isoformat()
problem_open, reason = self.assessment.is_open()
xblock.start_datetime = past.isoformat()
xblock.due_datetime = past.isoformat()
problem_open, reason = xblock.is_open()
self.assertFalse(problem_open)
self.assertEqual("due", reason)
self.assessment.start_datetime = past.isoformat()
self.assessment.due_datetime = future.isoformat()
problem_open, reason = self.assessment.is_open()
xblock.start_datetime = past.isoformat()
xblock.due_datetime = future.isoformat()
problem_open, reason = xblock.is_open()
self.assertTrue(problem_open)
self.assertEqual(None, reason)
self.assessment.start_datetime = future.isoformat()
self.assessment.due_datetime = way_future.isoformat()
problem_open, reason = self.assessment.is_open()
xblock.start_datetime = future.isoformat()
xblock.due_datetime = way_future.isoformat()
problem_open, reason = xblock.is_open()
self.assertFalse(problem_open)
self.assertEqual("start", reason)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment