class CommandTest(TestCase):
    """
    Tests that command correctly parses arguments, creates helper class instances and invokes correct
    methods on them
    """
    def setUp(self):
        """ Test setup """
        super(CommandTest, self).setUp()
        self.command = ExportDiscussionCommand()
        self.command.stdout = mock.Mock()
        self.command.stderr = mock.Mock()

    def set_up_default_mocks(self, patched_get_course, patched_get_settings, cohorted_thread_ids=None):
        """ Sets up default mocks passed via class decorator """
        patched_get_course.return_value = mock.Mock(spec=CourseLocator)
        patched_get_settings.return_value = mock.Mock(spec=CourseCohortsSettings)
        patched_get_settings.return_value.cohorted_discussions = cohorted_thread_ids or []

    # pylint:disable=unused-argument
    def test_handle_given_no_arguments_raises_command_error(self, patched_get_course, patched_get_settings):
        """ Tests that raises error if invoked with no arguments """
        with self.assertRaises(CommandError):
            self.command.handle()

    # pylint:disable=unused-argument
    def test_handle_given_more_than_two_args_raises_command_error(self, patched_get_course, patched_get_setting):
        """ Tests that raises error if invoked with too many arguments """
        with self.assertRaises(CommandError):
            self.command.handle(1, 2, 3)

    def test_handle_given_invalid_course_key_raises_invalid_key_error(self, patched_get_course, patched_get_settings):
        """ Tests that invalid key errors are propagated """
        patched_get_course.return_value = None
        with self.assertRaises(InvalidKeyError):
            self.command.handle("I'm invalid key")

    def test_handle_given_missing_course_raises_command_error(self, patched_get_course, patched_get_settings):
        """ Tests that raises command error if missing course key was provided """
        patched_get_course.return_value = None
        with self.assertRaises(CommandError):
            self.command.handle("edX/demoX/now")

    # pylint: disable=unused-argument
    def test_all_option(self, patched_get_course, patched_get_settings):
        """ Tests that the 'all' option does run the dump command for all courses """
        self.command.dump_one = mock.Mock()
        self.command.get_all_courses = mock.Mock()
        course_list = [mock.Mock() for __ in range(0, 3)]
        locator_list = [
            CourseLocator(org="edX", course="demoX", run="now"),
            CourseLocator(org="Sandbox", course="Sandbox", run="Sandbox"),
            CourseLocator(org="Test", course="Testy", run="Testify"),
        ]
        for index, course in enumerate(course_list):
            course.location.course_key = locator_list[index]
        self.command.get_all_courses.return_value = course_list
        self.command.handle("test_dir", all=True, dummy='test')
        calls = self.command.dump_one.call_args_list
        self.assertEqual(len(calls), 3)
        self.assertEqual(calls[0][0][0], 'course-v1:edX+demoX+now')
        self.assertEqual(calls[1][0][0], 'course-v1:Sandbox+Sandbox+Sandbox')
        self.assertEqual(calls[2][0][0], 'course-v1:Test+Testy+Testify')
        self.assertIn('test_dir/social_stats_course-v1edXdemoXnow', calls[0][0][1])
        self.assertIn('test_dir/social_stats_course-v1SandboxSandboxSandbox', calls[1][0][1])
        self.assertIn('test_dir/social_stats_course-v1TestTestyTestify', calls[2][0][1])

    def test_all_cohortedonly_options_together(self, patched_get_course, patched_get_settings):
        """ Ensure the 'all' option doesn't stop when one of the course doesn't have cohorted discussions """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        self.command.get_all_courses = mock.Mock()
        course_list = [mock.Mock() for __ in range(0, 3)]
        locator_list = [
            CourseLocator(org="edX", course="demoX", run="now"),
            CourseLocator(org="Sandbox", course="Sandbox", run="Sandbox"),
            CourseLocator(org="Test", course="Testy", run="Testify"),
        ]
        for index, course in enumerate(course_list):
            course.location.course_key = locator_list[index]
        self.command.get_all_courses.return_value = course_list
        self.command.handle("test_dir", all=True, cohorted_only=True, dummy='test')
        calls = patched_get_course.call_args_list
        self.assertEqual(len(calls), 3)
        self.assertEqual(calls[0][0][0], locator_list[0])
        self.assertEqual(calls[1][0][0], locator_list[1])
        self.assertEqual(calls[2][0][0], locator_list[2])

    @ddt.data("edX/demoX/now", "otherX/CourseX/later")
    def test_handle_writes_to_correct_location_when_output_file_not_specified(
            self, course_key, patched_get_course, patched_get_settings
    ):
        """ Tests that when no explicit filename is given data is exported to default location """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        expected_filename = utils.format_filename(
            "social_stats_{course}_{date:%Y_%m_%d_%H_%M_%S}.csv".format(course=course_key, date=datetime.utcnow())
        )
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
            patched_extractor.return_value = []
            self.command.handle(course_key)
            patched_open.assert_called_with(expected_filename, 'wb')

    @ddt.data("test.csv", "other_file.csv")
    def test_handle_writes_to_correct_location_when_output_file_is_specified(
            self, location, patched_get_course, patched_get_settings
    ):
        """ Tests that when explicit filename is given data is exported to chosen location """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
            patched_extractor.return_value = []
            self.command.handle("irrelevant/course/key", location)
            patched_open.assert_called_with(location, 'wb')

    def test_handle_creates_correct_exporter(self, patched_get_course, patched_get_settings):
        """ Tests that creates correct exporter """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor, \
                mock.patch(_target_module + ".Exporter") as patched_exporter:
            open_retval = patched_open()
            patched_extractor.return_value = []
            self.command.handle("irrelevant/course/key", "irrelevant_location.csv")
            patched_exporter.assert_called_with(open_retval)

    @ddt.data(
        {},
        {"1": {"num_threads": 12}},
        {"1": {"num_threads": 14, "num_comments": 7}}
    )
    def test_handle_exports_correct_data(self, extracted, patched_get_course, patched_get_settings):
        """ Tests that invokes export with correct data """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor, \
                mock.patch(_target_module + ".Exporter.export") as patched_exporter:
            patched_extractor.return_value = extracted
            self.command.handle("irrelevant/course/key", "irrelevant_location.csv")
            patched_exporter.assert_called_with(extracted)

    @ddt.unpack
    @ddt.data(*_std_parameters_list)
    def test_handle_passes_correct_parameters_to_extractor(
            self, course_key, end_date, thread_type, cohorted_thread_ids,
            patched_get_course, patched_get_settings):
        """ Tests that when no explicit filename is given data is exported to default location """
        self.set_up_default_mocks(patched_get_course, patched_get_settings, cohorted_thread_ids)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
            patched_extractor.return_value = []
            self.command.handle(
                str(course_key),
                end_date=end_date.isoformat() if end_date else end_date,
                thread_type=thread_type,
                cohorted_only=True if cohorted_thread_ids else False
            )
            patched_extractor.assert_called_with(
                course_key, end_date=end_date, thread_type=thread_type, thread_ids=cohorted_thread_ids
            )
Example #2
0
class CommandTest(TestCase):
    """
    Tests that command correctly parses arguments, creates helper class instances and invokes correct
    methods on them
    """
    def setUp(self):
        """ Test setup """
        super(CommandTest, self).setUp()
        self.command = ExportDiscussionCommand()
        self.command.stdout = mock.Mock()
        self.command.stderr = mock.Mock()

    def set_up_default_mocks(self,
                             patched_get_course,
                             patched_get_settings,
                             cohorted_thread_ids=None):
        """ Sets up default mocks passed via class decorator """
        patched_get_course.return_value = mock.Mock(spec=CourseLocator)
        patched_get_settings.return_value = mock.Mock(
            spec=CourseCohortsSettings)
        patched_get_settings.return_value.cohorted_discussions = cohorted_thread_ids or []

    # pylint:disable=unused-argument
    def test_handle_given_no_arguments_raises_command_error(
            self, patched_get_course, patched_get_settings):
        """ Tests that raises error if invoked with no arguments """
        with self.assertRaises(CommandError):
            self.command.handle()

    # pylint:disable=unused-argument
    def test_handle_given_more_than_two_args_raises_command_error(
            self, patched_get_course, patched_get_setting):
        """ Tests that raises error if invoked with too many arguments """
        with self.assertRaises(CommandError):
            self.command.handle(1, 2, 3)

    def test_handle_given_invalid_course_key_raises_invalid_key_error(
            self, patched_get_course, patched_get_settings):
        """ Tests that invalid key errors are propagated """
        patched_get_course.return_value = None
        with self.assertRaises(InvalidKeyError):
            self.command.handle("I'm invalid key")

    def test_handle_given_missing_course_raises_command_error(
            self, patched_get_course, patched_get_settings):
        """ Tests that raises command error if missing course key was provided """
        patched_get_course.return_value = None
        with self.assertRaises(CommandError):
            self.command.handle("edX/demoX/now")

    # pylint: disable=unused-argument
    def test_all_option(self, patched_get_course, patched_get_settings):
        """ Tests that the 'all' option does run the dump command for all courses """
        self.command.dump_one = mock.Mock()
        self.command.get_all_courses = mock.Mock()
        course_list = [mock.Mock() for __ in range(0, 3)]
        locator_list = [
            CourseLocator(org="edX", course="demoX", run="now"),
            CourseLocator(org="Sandbox", course="Sandbox", run="Sandbox"),
            CourseLocator(org="Test", course="Testy", run="Testify"),
        ]
        for index, course in enumerate(course_list):
            course.location.course_key = locator_list[index]
        self.command.get_all_courses.return_value = course_list
        self.command.handle("test_dir", all=True, dummy='test')
        calls = self.command.dump_one.call_args_list
        self.assertEqual(len(calls), 3)
        self.assertEqual(calls[0][0][0], 'course-v1:edX+demoX+now')
        self.assertEqual(calls[1][0][0], 'course-v1:Sandbox+Sandbox+Sandbox')
        self.assertEqual(calls[2][0][0], 'course-v1:Test+Testy+Testify')
        self.assertIn('test_dir/social_stats_course-v1edXdemoXnow',
                      calls[0][0][1])
        self.assertIn('test_dir/social_stats_course-v1SandboxSandboxSandbox',
                      calls[1][0][1])
        self.assertIn('test_dir/social_stats_course-v1TestTestyTestify',
                      calls[2][0][1])

    def test_all_cohortedonly_options_together(self, patched_get_course,
                                               patched_get_settings):
        """ Ensure the 'all' option doesn't stop when one of the course doesn't have cohorted discussions """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        self.command.get_all_courses = mock.Mock()
        course_list = [mock.Mock() for __ in range(0, 3)]
        locator_list = [
            CourseLocator(org="edX", course="demoX", run="now"),
            CourseLocator(org="Sandbox", course="Sandbox", run="Sandbox"),
            CourseLocator(org="Test", course="Testy", run="Testify"),
        ]
        for index, course in enumerate(course_list):
            course.location.course_key = locator_list[index]
        self.command.get_all_courses.return_value = course_list
        self.command.handle("test_dir",
                            all=True,
                            cohorted_only=True,
                            dummy='test')
        calls = patched_get_course.call_args_list
        self.assertEqual(len(calls), 3)
        self.assertEqual(calls[0][0][0], locator_list[0])
        self.assertEqual(calls[1][0][0], locator_list[1])
        self.assertEqual(calls[2][0][0], locator_list[2])

    @ddt.data("edX/demoX/now", "otherX/CourseX/later")
    def test_handle_writes_to_correct_location_when_output_file_not_specified(
            self, course_key, patched_get_course, patched_get_settings):
        """ Tests that when no explicit filename is given data is exported to default location """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        expected_filename = utils.format_filename(
            "social_stats_{course}_{date:%Y_%m_%d_%H_%M_%S}.csv".format(
                course=course_key, date=datetime.utcnow()))
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
            patched_extractor.return_value = []
            self.command.handle(course_key)
            patched_open.assert_called_with(expected_filename, 'wb')

    @ddt.data("test.csv", "other_file.csv")
    def test_handle_writes_to_correct_location_when_output_file_is_specified(
            self, location, patched_get_course, patched_get_settings):
        """ Tests that when explicit filename is given data is exported to chosen location """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
            patched_extractor.return_value = []
            self.command.handle("irrelevant/course/key", location)
            patched_open.assert_called_with(location, 'wb')

    def test_handle_creates_correct_exporter(self, patched_get_course,
                                             patched_get_settings):
        """ Tests that creates correct exporter """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor, \
                mock.patch(_target_module + ".Exporter") as patched_exporter:
            open_retval = patched_open()
            patched_extractor.return_value = []
            self.command.handle("irrelevant/course/key",
                                "irrelevant_location.csv")
            patched_exporter.assert_called_with(open_retval)

    @ddt.data({}, {"1": {
        "num_threads": 12
    }}, {"1": {
        "num_threads": 14,
        "num_comments": 7
    }})
    def test_handle_exports_correct_data(self, extracted, patched_get_course,
                                         patched_get_settings):
        """ Tests that invokes export with correct data """
        self.set_up_default_mocks(patched_get_course, patched_get_settings)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor, \
                mock.patch(_target_module + ".Exporter.export") as patched_exporter:
            patched_extractor.return_value = extracted
            self.command.handle("irrelevant/course/key",
                                "irrelevant_location.csv")
            patched_exporter.assert_called_with(extracted)

    @ddt.unpack
    @ddt.data(*_std_parameters_list)
    def test_handle_passes_correct_parameters_to_extractor(
            self, course_key, end_date, thread_type, cohorted_thread_ids,
            patched_get_course, patched_get_settings):
        """ Tests that when no explicit filename is given data is exported to default location """
        self.set_up_default_mocks(patched_get_course, patched_get_settings,
                                  cohorted_thread_ids)
        patched_open = mock.mock_open()
        with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
                mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
            patched_extractor.return_value = []
            self.command.handle(
                str(course_key),
                end_date=end_date.isoformat() if end_date else end_date,
                thread_type=thread_type,
                cohorted_only=True if cohorted_thread_ids else False)
            patched_extractor.assert_called_with(
                course_key,
                end_date=end_date,
                thread_type=thread_type,
                thread_ids=cohorted_thread_ids)