test_ora.py 15.3 KB
Newer Older
1 2 3 4
"""
Tests for ORA (Open Response Assessment) through the LMS UI.
"""

5
import json
6 7
from unittest import skip

8 9
from bok_choy.promise import Promise, BrokenPromise
from ..pages.lms.peer_confirm import PeerConfirmPage
10
from ..pages.lms.auto_auth import AutoAuthPage
11 12 13 14
from ..pages.lms.course_info import CourseInfoPage
from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.open_response import OpenResponsePage
15 16
from ..pages.lms.peer_grade import PeerGradePage
from ..pages.lms.peer_calibrate import PeerCalibratePage
17

18
from ..pages.lms.progress import ProgressPage
19
from ..fixtures.course import XBlockFixtureDesc, CourseFixture
20
from ..fixtures.xqueue import XQueueResponseFixture
21

22
from .helpers import load_data_str, UniqueCourseTest
23 24


25
class OpenResponseTest(UniqueCourseTest):
26 27
    """
    Tests that interact with ORA (Open Response Assessment) through the LMS UI.
28 29
    This base class sets up a course with open response problems and defines
    some helper functions used in the ORA tests.
30 31
    """

32 33 34 35
    # Grade response (dict) to return from the XQueue stub
    # in response to our unique submission text.
    XQUEUE_GRADE_RESPONSE = None

36 37
    def setUp(self):
        """
38
        Install a test course with ORA problems.
39 40 41 42
        Always start in the subsection with open response problems.
        """
        super(OpenResponseTest, self).setUp()

43 44 45 46 47 48
        # Create page objects
        self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id)
        self.course_info_page = CourseInfoPage(self.browser, self.course_id)
        self.tab_nav = TabNavPage(self.browser)
        self.course_nav = CourseNavPage(self.browser)
        self.open_response = OpenResponsePage(self.browser)
49 50 51
        self.peer_grade = PeerGradePage(self.browser)
        self.peer_calibrate = PeerCalibratePage(self.browser)
        self.peer_confirm = PeerConfirmPage(self.browser)
52
        self.progress_page = ProgressPage(self.browser, self.course_id)
53 54

        # Configure the test course
55 56 57 58 59
        course_fix = CourseFixture(
            self.course_info['org'], self.course_info['number'],
            self.course_info['run'], self.course_info['display_name']
        )

60 61 62 63 64 65 66
        # Create a unique name for the peer assessed problem.  This will show up
        # in the list of peer problems, which is shared among tests running
        # in parallel; it needs to be unique so we can find it.
        # It's also import that the problem has "Peer" in the name; otherwise,
        # the ORA stub will ignore it.
        self.peer_problem_name = "Peer-Assessed {}".format(self.unique_id[0:6])

67 68 69
        course_fix.add_children(
            XBlockFixtureDesc('chapter', 'Test Section').add_children(
                XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
70

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
                    XBlockFixtureDesc(
                        'combinedopenended',
                        'Self-Assessed',
                        data=load_data_str('ora_self_problem.xml'),
                        metadata={
                            'graded': True,
                        },
                    ),

                    XBlockFixtureDesc(
                        'combinedopenended',
                        'AI-Assessed',
                        data=load_data_str('ora_ai_problem.xml'),
                        metadata={
                            'graded': True,
                        },
                    ),

                    XBlockFixtureDesc(
                        'combinedopenended',
                        self.peer_problem_name,
                        data=load_data_str('ora_peer_problem.xml'),
                        metadata={
                            'graded': True,
                        },
                    ),
97

98
                    # This is the interface a student can use to grade his/her peers
99 100
                    XBlockFixtureDesc('peergrading', 'Peer Module'),

101 102 103
                )
            )
        ).install()
104

105
        # Configure the XQueue stub's response for the text we will submit
106 107
        # The submission text is unique so we can associate each response with a particular test case.
        self.submission = "Test submission " + self.unique_id[0:4]
108
        if self.XQUEUE_GRADE_RESPONSE is not None:
109
            XQueueResponseFixture(self.submission, self.XQUEUE_GRADE_RESPONSE).install()
110

111 112 113 114
        # Log in and navigate to the essay problems
        self.auth_page.visit()
        self.course_info_page.visit()
        self.tab_nav.go_to_tab('Courseware')
115 116 117 118 119 120 121 122 123

    def submit_essay(self, expected_assessment_type, expected_prompt):
        """
        Submit an essay and verify that the problem uses
        the `expected_assessment_type` ("self", "ai", or "peer") and
        shows the `expected_prompt` (a string).
        """

        # Check the assessment type and prompt
124 125
        self.assertEqual(self.open_response.assessment_type, expected_assessment_type)
        self.assertIn(expected_prompt, self.open_response.prompt)
126 127

        # Enter a submission, which will trigger a pre-defined response from the XQueue stub.
128
        self.open_response.set_response(self.submission)
129 130

        # Save the response and expect some UI feedback
131
        self.open_response.save_response()
132
        self.assertEqual(
133
            self.open_response.alert_message,
134 135 136 137
            "Answer saved, but not yet submitted."
        )

        # Submit the response
138
        self.open_response.submit_response()
139 140 141 142 143 144 145

    def get_asynch_feedback(self, assessment_type):
        """
        Wait for and retrieve asynchronous feedback
        (e.g. from AI, instructor, or peer grading)
        `assessment_type` is either "ai" or "peer".
        """
146 147 148
        # Because the check function involves fairly complicated actions
        # (navigating through several screens), we give it more time to complete
        # than the default.
149
        return Promise(
150
            self._check_feedback_func(assessment_type),
151 152
            'Got feedback for {0} problem'.format(assessment_type),
            timeout=600, try_interval=5
153
        ).fulfill()
154 155 156 157 158 159 160 161 162 163 164 165 166

    def _check_feedback_func(self, assessment_type):
        """
        Navigate away from, then return to, the peer problem to
        receive updated feedback.

        The returned function will return a tuple `(is_success, rubric_feedback)`,
        `is_success` is True iff we have received feedback for the problem;
        `rubric_feedback` is a list of "correct" or "incorrect" strings.
        """
        if assessment_type == 'ai':
            section_name = 'AI-Assessed'
        elif assessment_type == 'peer':
167
            section_name = self.peer_problem_name
168 169 170 171
        else:
            raise ValueError('Assessment type not recognized.  Must be either "ai" or "peer"')

        def _inner_check():
172 173
            self.course_nav.go_to_sequential('Self-Assessed')
            self.course_nav.go_to_sequential(section_name)
174 175 176 177 178 179

            try:
                feedback = self.open_response.rubric.feedback

            # Unsuccessful if the rubric hasn't loaded
            except BrokenPromise:
180
                return False, None
181 182

            # Successful if `feedback` is a non-empty list
183
            else:
184
                return bool(feedback), feedback
185 186 187 188 189 190 191 192

        return _inner_check


class SelfAssessmentTest(OpenResponseTest):
    """
    Test ORA self-assessment.
    """
193 194 195

    def test_self_assessment(self):
        """
196 197 198 199
        Given I am viewing a self-assessment problem
        When I submit an essay and complete a self-assessment rubric
        Then I see a scored rubric
        And I see my score in the progress page.
200
        """
201

202
        # Navigate to the self-assessment problem and submit an essay
203
        self.course_nav.go_to_sequential('Self-Assessed')
204
        self.submit_essay('self', 'Censorship in the Libraries')
205

206 207
        # Fill in the rubric and expect that we get feedback
        rubric = self.open_response.rubric
208

209 210
        self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
        rubric.set_scores([0, 1])
211 212
        rubric.submit('self')

213
        self.assertEqual(rubric.feedback, ['incorrect', 'correct'])
214

215
        # Verify the progress page
216 217
        self.progress_page.visit()
        scores = self.progress_page.scores('Test Section', 'Test Subsection')
218 219 220 221 222 223 224 225 226 227 228 229 230

        # The first score is self-assessment, which we've answered, so it's 1/2
        # The other scores are AI- and peer-assessment, which we haven't answered so those are 0/2
        self.assertEqual(scores, [(1, 2), (0, 2), (0, 2)])


class AIAssessmentTest(OpenResponseTest):
    """
    Test ORA AI-assessment.
    """

    XQUEUE_GRADE_RESPONSE = {
        'score': 1,
231
        'feedback': json.dumps({"spelling": "Ok.", "grammar": "Ok.", "markup_text": "NA"}),
232 233 234 235 236 237 238 239
        'grader_type': 'BC',
        'success': True,
        'grader_id': 1,
        'submission_id': 1,
        'rubric_scores_complete': True,
        'rubric_xml': load_data_str('ora_rubric.xml')
    }

240
    @skip('Intermittently failing, see ORA-342')
241 242
    def test_ai_assessment(self):
        """
243 244 245 246
        Given I am viewing an AI-assessment problem that has a trained ML model
        When I submit an essay and wait for a response
        Then I see a scored rubric
        And I see my score in the progress page.
247 248 249
        """

        # Navigate to the AI-assessment problem and submit an essay
250
        self.course_nav.go_to_sequential('AI-Assessed')
251
        self.submit_essay('ai', 'Censorship in the Libraries')
252

253 254 255
        # Refresh the page to get the updated feedback
        # then verify that we get the feedback sent by our stub XQueue implementation
        self.assertEqual(self.get_asynch_feedback('ai'), ['incorrect', 'correct'])
256

257
        # Verify the progress page
258 259
        self.progress_page.visit()
        scores = self.progress_page.scores('Test Section', 'Test Subsection')
260

261 262 263 264
        # First score is the self-assessment score, which we haven't answered, so it's 0/2
        # Second score is the AI-assessment score, which we have answered, so it's 1/2
        # Third score is peer-assessment, which we haven't answered, so it's 0/2
        self.assertEqual(scores, [(0, 2), (1, 2), (0, 2)])
265

266

267
class InstructorAssessmentTest(OpenResponseTest):
268 269
    """
    Test an AI-assessment that has been graded by an instructor.
270
    This runs the same test as the AI-assessment test, except
271 272 273 274 275 276
    that the feedback comes from an instructor instead of the machine grader.
    From the student's perspective, it should look the same.
    """

    XQUEUE_GRADE_RESPONSE = {
        'score': 1,
277
        'feedback': json.dumps({"feedback": "Good job!"}),
278 279 280 281 282 283 284 285
        'grader_type': 'IN',
        'success': True,
        'grader_id': 1,
        'submission_id': 1,
        'rubric_scores_complete': True,
        'rubric_xml': load_data_str('ora_rubric.xml')
    }

286
    @skip('Intermittently failing, see ORA-342')
287 288 289 290 291 292 293 294 295 296 297 298
    def test_instructor_assessment(self):
        """
        Given an instructor has graded my submission
        When I view my submission
        Then I see a scored rubric
        And my progress page shows the problem score.
        """

        # Navigate to the AI-assessment problem and submit an essay
        # We have configured the stub to simulate that this essay will be staff-graded
        self.course_nav.go_to_sequential('AI-Assessed')
        self.submit_essay('ai', 'Censorship in the Libraries')
299

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
        # Refresh the page to get the updated feedback
        # then verify that we get the feedback sent by our stub XQueue implementation
        self.assertEqual(self.get_asynch_feedback('ai'), ['incorrect', 'correct'])

        # Verify the progress page
        self.progress_page.visit()
        scores = self.progress_page.scores('Test Section', 'Test Subsection')

        # First score is the self-assessment score, which we haven't answered, so it's 0/2
        # Second score is the AI-assessment score, which we have answered, so it's 1/2
        # Third score is peer-assessment, which we haven't answered, so it's 0/2
        self.assertEqual(scores, [(0, 2), (1, 2), (0, 2)])


class PeerAssessmentTest(OpenResponseTest):
315
    """
316
    Test ORA peer-assessment, including calibration and giving/receiving scores.
317 318 319 320 321 322 323 324 325 326 327 328 329 330
    """

    # Unlike other assessment types, peer assessment has multiple scores
    XQUEUE_GRADE_RESPONSE = {
        'score': [2, 2, 2],
        'feedback': [json.dumps({"feedback": ""})] * 3,
        'grader_type': 'PE',
        'success': True,
        'grader_id': [1, 2, 3],
        'submission_id': 1,
        'rubric_scores_complete': [True, True, True],
        'rubric_xml': [load_data_str('ora_rubric.xml')] * 3
    }

331
    def test_peer_calibrate_and_grade(self):
332
        """
333 334
        Given I am viewing a peer-assessment problem
        And the instructor has submitted enough example essays
335
        When I submit acceptable scores for enough calibration essays
336 337
        Then I am able to peer-grade other students' essays.

338
        Given I have submitted an essay for peer-assessment
339
        And I have peer-graded enough students essays
340 341 342 343
        And enough other students have scored my essay
        Then I can view the scores and written feedback
        And I see my score in the progress page.
        """
344 345 346 347 348 349 350
        # Initially, the student should NOT be able to grade peers,
        # because he/she hasn't submitted any essays.
        self.course_nav.go_to_sequential('Peer Module')
        self.assertIn("You currently do not have any peer grading to do", self.peer_calibrate.message)

        # Submit an essay
        self.course_nav.go_to_sequential(self.peer_problem_name)
351 352
        self.submit_essay('peer', 'Censorship in the Libraries')

353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
        # Need to reload the page to update the peer grading module
        self.course_info_page.visit()
        self.tab_nav.go_to_tab('Courseware')
        self.course_nav.go_to_section('Test Section', 'Test Subsection')

        # Select the problem to calibrate
        self.course_nav.go_to_sequential('Peer Module')
        self.assertIn(self.peer_problem_name, self.peer_grade.problem_list)
        self.peer_grade.select_problem(self.peer_problem_name)

        # Calibrate
        self.peer_confirm.start(is_calibrating=True)
        rubric = self.peer_calibrate.rubric
        self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
        rubric.set_scores([0, 1])
368
        rubric.submit('peer')
369 370 371 372 373 374 375 376 377 378
        self.peer_calibrate.continue_to_grading()

        # Grade a peer
        self.peer_confirm.start()
        rubric = self.peer_grade.rubric
        self.assertEqual(rubric.categories, ["Writing Applications", "Language Conventions"])
        rubric.set_scores([0, 1])
        rubric.submit()

        # Expect to receive essay feedback
379 380
        # We receive feedback from all three peers, each of which
        # provide 2 scores (one for each rubric item)
381 382
        # Written feedback is a dummy value sent by the XQueue stub.
        self.course_nav.go_to_sequential(self.peer_problem_name)
383 384 385
        self.assertEqual(self.get_asynch_feedback('peer'), ['incorrect', 'correct'] * 3)

        # Verify the progress page
386 387
        self.progress_page.visit()
        scores = self.progress_page.scores('Test Section', 'Test Subsection')
388 389 390 391 392

        # First score is the self-assessment score, which we haven't answered, so it's 0/2
        # Second score is the AI-assessment score, which we haven't answered, so it's 0/2
        # Third score is peer-assessment, which we have answered, so it's 2/2
        self.assertEqual(scores, [(0, 2), (0, 2), (2, 2)])