Skip to content

Commit

Permalink
Fix oppia#21203 :Mock datetime in stats model test (oppia#21208)
Browse files Browse the repository at this point in the history
* fix: mock datetime to ensure consistent date comparison in stats model test

* fix: Mock datetime to ensure consistent timestamp comparison in suggestion stats tests

* fix: Mock datetime to ensure consistent timestamp comparison in suggestion stats tests

* fix: Mock datetime to ensure consistent timestamp comparison in suggestion stats tests

* fix: Mock datetime to ensure consistent timestamp comparison in suggestion stats tests

* fix: Mock datetime to ensure consistent timestamp comparison in suggestion stats tests

* fix: refactor datetime mocking to AppTestBase for streamlined usage in tests

* fix: lint checks
  • Loading branch information
TARishabh authored Nov 12, 2024
1 parent 7749c22 commit 6389baf
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 113 deletions.
126 changes: 65 additions & 61 deletions core/jobs/batch_jobs/suggestion_stats_computation_jobs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,65 +712,69 @@ def test_creates_translation_stats_models_from_two_accepted_suggestions(
def test_creates_multiple_stats_models_from_multiple_users(
self
) -> None:
opportunity_model = self.create_model(
opportunity_models.ExplorationOpportunitySummaryModel,
id=self.EXP_1_ID,
topic_id=self.TOPIC_1_ID,
chapter_title='irelevant',
content_count=1,
story_id='irelevant',
story_title='irelevant',
topic_name='irelevant'
)
opportunity_model.update_timestamps()
opportunity_model.put()
first_suggestion_model = self.create_model(
suggestion_models.GeneralSuggestionModel,
suggestion_type=feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
author_id=self.VALID_USER_ID_1,
change_cmd={
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'state',
'content_id': 'content_id',
'language_code': 'lang',
'content_html': '111 222 333',
'translation_html': '111 222 333',
'data_format': 'unicode'
},
score_category='irelevant',
status=suggestion_models.STATUS_ACCEPTED,
target_type='exploration',
target_id=self.EXP_1_ID,
target_version_at_submission=0,
language_code=self.LANG_1,
final_reviewer_id='reviewer1'
)
first_suggestion_model.update_timestamps()
first_suggestion_model.put()

second_suggestion_model = self.create_model(
suggestion_models.GeneralSuggestionModel,
suggestion_type=feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
author_id=self.VALID_USER_ID_2,
change_cmd={
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'state',
'content_id': 'content_id',
'language_code': 'lang',
'content_html': '111 222 333',
'translation_html': '111 222 333',
'data_format': 'unicode'
},
score_category='irelevant',
status=suggestion_models.STATUS_ACCEPTED,
target_type='exploration',
target_id=self.EXP_1_ID,
target_version_at_submission=0,
language_code=self.LANG_1,
final_reviewer_id='reviewer1'
)
second_suggestion_model.update_timestamps()
second_suggestion_model.put()
# Define a fixed datetime.
mocked_now = datetime.datetime(2024, 10, 28)

with self.mock_datetime_utcnow(mocked_now):
opportunity_model = self.create_model(
opportunity_models.ExplorationOpportunitySummaryModel,
id=self.EXP_1_ID,
topic_id=self.TOPIC_1_ID,
chapter_title='irelevant',
content_count=1,
story_id='irelevant',
story_title='irelevant',
topic_name='irelevant'
)
opportunity_model.update_timestamps()
opportunity_model.put()
first_suggestion_model = self.create_model(
suggestion_models.GeneralSuggestionModel,
suggestion_type=feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
author_id=self.VALID_USER_ID_1,
change_cmd={
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'state',
'content_id': 'content_id',
'language_code': 'lang',
'content_html': '111 222 333',
'translation_html': '111 222 333',
'data_format': 'unicode'
},
score_category='irelevant',
status=suggestion_models.STATUS_ACCEPTED,
target_type='exploration',
target_id=self.EXP_1_ID,
target_version_at_submission=0,
language_code=self.LANG_1,
final_reviewer_id='reviewer1'
)
first_suggestion_model.update_timestamps()
first_suggestion_model.put()

second_suggestion_model = self.create_model(
suggestion_models.GeneralSuggestionModel,
suggestion_type=feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT,
author_id=self.VALID_USER_ID_2,
change_cmd={
'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
'state_name': 'state',
'content_id': 'content_id',
'language_code': 'lang',
'content_html': '111 222 333',
'translation_html': '111 222 333',
'data_format': 'unicode'
},
score_category='irelevant',
status=suggestion_models.STATUS_ACCEPTED,
target_type='exploration',
target_id=self.EXP_1_ID,
target_version_at_submission=0,
language_code=self.LANG_1,
final_reviewer_id='reviewer1'
)
second_suggestion_model.update_timestamps()
second_suggestion_model.put()

self.assert_job_output_is([
job_run_result.JobRunResult(
Expand Down Expand Up @@ -879,11 +883,11 @@ def test_creates_multiple_stats_models_from_multiple_users(
translation_review_stats_model.accepted_translation_word_count, 6)
self.assertEqual(
translation_review_stats_model.first_contribution_date,
datetime.datetime.utcnow().date()
mocked_now.date()
)
self.assertEqual(
translation_review_stats_model.last_contribution_date,
datetime.datetime.utcnow().date()
mocked_now.date()
)

def _create_valid_question_data(
Expand Down
104 changes: 52 additions & 52 deletions core/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2089,6 +2089,58 @@ def mock_set_constants_to_default(self) -> None:
"""
raise Exception('Please mock this method in the test.')

@contextlib.contextmanager
def mock_datetime_utcnow(
self, mocked_now: datetime.datetime
) -> Iterator[None]:
"""Mocks parts of the datastore to accept a fake datetime type that
always returns the same value for utcnow.
Example:
import datetime
mocked_now = datetime.datetime.utcnow() - datetime.timedelta(days=1)
with mock_datetime_utcnow(mocked_now):
self.assertEqual(datetime.datetime.utcnow(), mocked_now)
actual_now = datetime.datetime.utcnow() # Returns actual time.
Args:
mocked_now: datetime.datetime. The datetime which will be used
instead of the current UTC datetime.
Yields:
None. Empty yield statement.
Raises:
Exception. Given argument is not a datetime.
"""
if not isinstance(mocked_now, datetime.datetime):
raise Exception('mocked_now must be datetime, got: %r' % mocked_now)

old_datetime = datetime.datetime

class MockDatetimeType(type):
"""Overrides isinstance() behavior."""

@classmethod
def __instancecheck__(mcs, instance: datetime.datetime) -> bool:
return isinstance(instance, old_datetime)

class MockDatetime(datetime.datetime, metaclass=MockDatetimeType):
"""Always returns mocked_now as the current UTC time."""

# Here we use MyPy ignore because the signature of this
# method doesn't match with datetime.datetime's utcnow().
@classmethod
def utcnow(cls) -> datetime.datetime: # type: ignore[override]
"""Returns the mocked datetime."""
return mocked_now

setattr(datetime, 'datetime', MockDatetime)
try:
yield
finally:
setattr(datetime, 'datetime', old_datetime)


class GenericTestBase(AppEngineTestBase):
"""Base test class with common/generic helper methods.
Expand Down Expand Up @@ -2524,58 +2576,6 @@ def logout(self) -> None:
os.environ['USER_EMAIL'] = ''
os.environ['USER_IS_ADMIN'] = '0'

@contextlib.contextmanager
def mock_datetime_utcnow(
self, mocked_now: datetime.datetime
) -> Iterator[None]:
"""Mocks parts of the datastore to accept a fake datetime type that
always returns the same value for utcnow.
Example:
import datetime
mocked_now = datetime.datetime.utcnow() - datetime.timedelta(days=1)
with mock_datetime_utcnow(mocked_now):
self.assertEqual(datetime.datetime.utcnow(), mocked_now)
actual_now = datetime.datetime.utcnow() # Returns actual time.
Args:
mocked_now: datetime.datetime. The datetime which will be used
instead of the current UTC datetime.
Yields:
None. Empty yield statement.
Raises:
Exception. Given argument is not a datetime.
"""
if not isinstance(mocked_now, datetime.datetime):
raise Exception('mocked_now must be datetime, got: %r' % mocked_now)

old_datetime = datetime.datetime

class MockDatetimeType(type):
"""Overrides isinstance() behavior."""

@classmethod
def __instancecheck__(mcs, instance: datetime.datetime) -> bool:
return isinstance(instance, old_datetime)

class MockDatetime(datetime.datetime, metaclass=MockDatetimeType):
"""Always returns mocked_now as the current UTC time."""

# Here we use MyPy ignore because the signature of this
# method doesn't match with datetime.datetime's utcnow().
@classmethod
def utcnow(cls) -> datetime.datetime: # type: ignore[override]
"""Returns the mocked datetime."""
return mocked_now

setattr(datetime, 'datetime', MockDatetime)
try:
yield
finally:
setattr(datetime, 'datetime', old_datetime)

@contextlib.contextmanager
def login_context(
self, email: str, is_super_admin: bool = False
Expand Down

0 comments on commit 6389baf

Please sign in to comment.