Added feedback check for AI-assessment test Added peer assessment feedback test Added tests for peer grading module Added stub peer grading module service Updated Studio/LMS page objects and tests to use bok-choy v0.1.0 Added LMS bok-choy tests from e2e repo Add bok-choy tests for peer grading module, including calibration.
281 lines
10 KiB
Python
281 lines
10 KiB
Python
"""
|
|
Unit tests for stub ORA implementation.
|
|
"""
|
|
|
|
import unittest
|
|
import requests
|
|
import json
|
|
from ..ora import StubOraService, StubOraHandler, StudentState
|
|
|
|
|
|
class StubOraServiceTest(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
"""
|
|
Start the stub server.
|
|
"""
|
|
self.server = StubOraService()
|
|
self.addCleanup(self.server.shutdown)
|
|
|
|
def test_calibration(self):
|
|
|
|
# Ensure that we use the same student ID throughout
|
|
student_id = '1234'
|
|
|
|
# Initially, student should not be calibrated
|
|
response = requests.get(
|
|
self._peer_url('is_student_calibrated'),
|
|
params={'student_id': student_id, 'problem_id': '5678'}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'total_calibrated_on_so_far': 0,
|
|
'calibrated': False
|
|
})
|
|
|
|
# Retrieve a calibration essay
|
|
response = requests.get(
|
|
self._peer_url('show_calibration_essay'),
|
|
params={'student_id': student_id, 'problem_id': '5678'}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'submission_id': self.server.DUMMY_DATA['submission_id'],
|
|
'submission_key': self.server.DUMMY_DATA['submission_key'],
|
|
'student_response': self.server.DUMMY_DATA['student_response'],
|
|
'prompt': self.server.DUMMY_DATA['prompt'],
|
|
'rubric': self.server.DUMMY_DATA['rubric'],
|
|
'max_score': self.server.DUMMY_DATA['max_score']
|
|
})
|
|
|
|
# Grade the calibration essay
|
|
response = requests.post(
|
|
self._peer_url('save_calibration_essay'),
|
|
data={
|
|
'student_id': student_id,
|
|
'location': 'test location',
|
|
'calibration_essay_id': 1,
|
|
'score': 2,
|
|
'submission_key': 'key',
|
|
'feedback': 'Good job!'
|
|
}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'message': self.server.DUMMY_DATA['message'],
|
|
'actual_score': self.server.DUMMY_DATA['actual_score'],
|
|
'actual_rubric': self.server.DUMMY_DATA['actual_rubric'],
|
|
'actual_feedback': self.server.DUMMY_DATA['actual_feedback']
|
|
})
|
|
|
|
# Now the student should be calibrated
|
|
response = requests.get(
|
|
self._peer_url('is_student_calibrated'),
|
|
params={'student_id': student_id, 'problem_id': '5678'}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'total_calibrated_on_so_far': 1,
|
|
'calibrated': True
|
|
})
|
|
|
|
# But a student with a different ID should NOT be calibrated.
|
|
response = requests.get(
|
|
self._peer_url('is_student_calibrated'),
|
|
params={'student_id': 'another', 'problem_id': '5678'}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'total_calibrated_on_so_far': 0,
|
|
'calibrated': False
|
|
})
|
|
|
|
def test_grade_peers(self):
|
|
|
|
# Ensure a consistent student ID
|
|
student_id = '1234'
|
|
|
|
# Check initial number of submissions
|
|
# Should be none graded and 1 required
|
|
self._assert_num_graded(student_id, None, 0, 1)
|
|
|
|
# Register a problem that DOES have "peer" in the name
|
|
self._register_problem('test_location', 'Peer Assessed Problem')
|
|
|
|
# Retrieve the next submission
|
|
response = requests.get(
|
|
self._peer_url('get_next_submission'),
|
|
params={'grader_id': student_id, 'location': 'test_location'}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'submission_id': self.server.DUMMY_DATA['submission_id'],
|
|
'submission_key': self.server.DUMMY_DATA['submission_key'],
|
|
'student_response': self.server.DUMMY_DATA['student_response'],
|
|
'prompt': self.server.DUMMY_DATA['prompt'],
|
|
'rubric': self.server.DUMMY_DATA['rubric'],
|
|
'max_score': self.server.DUMMY_DATA['max_score']
|
|
})
|
|
|
|
# Grade the submission
|
|
response = requests.post(
|
|
self._peer_url('save_grade'),
|
|
data={
|
|
'location': 'test_location',
|
|
'grader_id': student_id,
|
|
'submission_id': 1,
|
|
'score': 2,
|
|
'feedback': 'Good job!',
|
|
'submission_key': 'key'
|
|
}
|
|
)
|
|
self._assert_response(response, {'version': 1, 'success': True})
|
|
|
|
# Check final number of submissions
|
|
# Shoud be one graded and none required
|
|
self._assert_num_graded(student_id, 'test_location', 1, 0)
|
|
|
|
# Grade the next submission the submission
|
|
response = requests.post(
|
|
self._peer_url('save_grade'),
|
|
data={
|
|
'location': 'test_location',
|
|
'grader_id': student_id,
|
|
'submission_id': 1,
|
|
'score': 2,
|
|
'feedback': 'Good job!',
|
|
'submission_key': 'key'
|
|
}
|
|
)
|
|
self._assert_response(response, {'version': 1, 'success': True})
|
|
|
|
# Check final number of submissions
|
|
# Shoud be two graded and none required
|
|
self._assert_num_graded(student_id, 'test_location', 2, 0)
|
|
|
|
def test_problem_list(self):
|
|
|
|
self._register_problem('test_location', 'Peer Grading Problem')
|
|
|
|
# The problem list returns dummy counts which are not updated
|
|
# The location we use is ignored by the LMS, and we ignore it in the stub,
|
|
# so we use a dummy value there too.
|
|
response = requests.get(
|
|
self._peer_url('get_problem_list'),
|
|
params={'course_id': 'test course'}
|
|
)
|
|
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'problem_list': [{
|
|
'location': 'test_location',
|
|
'problem_name': 'Peer Grading Problem',
|
|
'num_graded': self.server.DUMMY_DATA['problem_list_num_graded'],
|
|
'num_pending': self.server.DUMMY_DATA['problem_list_num_pending'],
|
|
'num_required': self.server.DUMMY_DATA['problem_list_num_required']
|
|
}]
|
|
})
|
|
|
|
def test_ignore_non_peer_problem(self):
|
|
|
|
# Register a problem that does NOT have "peer" in the name
|
|
self._register_problem('test_location', 'Self Assessed Problem')
|
|
|
|
# Expect that the problem list is empty
|
|
response = requests.get(
|
|
self._peer_url('get_problem_list'),
|
|
params={'course_id': 'test course'}
|
|
)
|
|
|
|
self._assert_response(response,
|
|
{'version': 1, 'success': True, 'problem_list': []}
|
|
)
|
|
|
|
# Expect that no data is available for the problem location
|
|
response = requests.get(
|
|
self._peer_url('get_data_for_location'),
|
|
params={'location': 'test_location', 'student_id': 'test'}
|
|
)
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertEqual(response.json(), {'version': 1, 'success': False})
|
|
|
|
def test_empty_problem_list(self):
|
|
|
|
# Without configuring any problem location, should return an empty list
|
|
response = requests.get(
|
|
self._peer_url('get_problem_list'),
|
|
params={'course_id': 'test course'}
|
|
)
|
|
self._assert_response(response, {'version': 1, 'success': True, 'problem_list': []})
|
|
|
|
def _peer_url(self, path):
|
|
"""
|
|
Construt a URL to the stub ORA peer-grading service.
|
|
"""
|
|
return "http://127.0.0.1:{port}/peer_grading/{path}/".format(
|
|
port=self.server.port, path=path
|
|
)
|
|
|
|
def _register_problem(self, location, name):
|
|
"""
|
|
Configure the stub to use a particular problem location
|
|
The actual implementation discovers problem locations by submission
|
|
to the XQueue; we do something similar by having the XQueue stub
|
|
register submitted locations with the ORA stub.
|
|
"""
|
|
grader_payload = json.dumps({'location': location, 'problem_id': name})
|
|
url = "http://127.0.0.1:{port}/test/register_submission".format(port=self.server.port)
|
|
response = requests.post(url, data={'grader_payload': grader_payload})
|
|
self.assertTrue(response.ok)
|
|
|
|
def _assert_response(self, response, expected_json):
|
|
"""
|
|
Assert that the `response` was successful and contained
|
|
`expected_json` (dict) as its content.
|
|
"""
|
|
self.assertTrue(response.ok)
|
|
self.assertEqual(response.json(), expected_json)
|
|
|
|
def _assert_num_graded(self, student_id, location, num_graded, num_required):
|
|
"""
|
|
ORA provides two distinct ways to get the submitted/graded counts.
|
|
Here we check both of them to ensure that the number that we've graded
|
|
is consistently `num_graded`.
|
|
"""
|
|
|
|
# Unlike the actual ORA service,
|
|
# we keep track of counts on a per-student basis.
|
|
# This means that every user starts with N essays to grade,
|
|
# and as they grade essays, that number decreases.
|
|
# We do NOT simulate students adding more essays to the queue,
|
|
# and essays that the current student submits are NOT graded
|
|
# by other students.
|
|
num_pending = StudentState.INITIAL_ESSAYS_AVAILABLE - num_graded
|
|
|
|
# Notifications
|
|
response = requests.get(
|
|
self._peer_url('get_notifications'),
|
|
params={'student_id': student_id, 'course_id': 'test course'}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'count_required': num_required,
|
|
'student_sub_count': self.server.DUMMY_DATA['student_sub_count'],
|
|
'count_graded': num_graded,
|
|
'count_available': num_pending
|
|
})
|
|
|
|
# Location data
|
|
if location is not None:
|
|
response = requests.get(
|
|
self._peer_url('get_data_for_location'),
|
|
params={'location': location, 'student_id': student_id}
|
|
)
|
|
self._assert_response(response, {
|
|
'version': 1, 'success': True,
|
|
'count_required': num_required,
|
|
'student_sub_count': self.server.DUMMY_DATA['student_sub_count'],
|
|
'count_graded': num_graded,
|
|
'count_available': num_pending
|
|
})
|