import logging from collections import OrderedDict from unittest.mock import patch import pytest from requests.exceptions import ConnectTimeout, ReadTimeout from src.config.objects import LastFetchDatetime, TargetObject from src.error.exceptions import SalesforceAPIException from src.fetch_crm_data_process import fetch_crm_data_process from src.system_var.constants import FETCH_JP_NAME from src.util.execute_datetime import ExecuteDateTime from .test_utils.log_message import generate_log_message_tuple common_target_object_dict = { 'object_name': 'Account', 'columns': [ 'Id', 'AccountNumber', 'LastModifiedDate', 'LastModifiedById', 'SystemModstamp', 'IsDeleted' ] } common_last_fetch_datetime_dict = { 'last_fetch_datetime_from': '2000-01-01T00:00:00.000Z', 'last_fetch_datetime_to': '2010-01-01T00:00:00.000Z', } common_execute_datetime = ExecuteDateTime() common_target_object = TargetObject(common_target_object_dict, common_execute_datetime) common_last_fetch_datetime = LastFetchDatetime(common_last_fetch_datetime_dict, common_execute_datetime) common_expect = [ OrderedDict([ ('attributes', OrderedDict([('type', 'Account'), ('url', '/services/data/v1.0/sobjects/Account/TEST001')])), ('Id', 'TEST001'), ('AccountNumber', 'test001'), ('LastModifiedDate', '2022-06-01T00:00:00.000+0000'), ('LastModifiedById', 1.234567E+6), ('SystemModstamp', '2022-06-01T00:00:00.000+0000'), ('IsDeleted', False) ]), OrderedDict([ ('attributes', OrderedDict([('type', 'Account'), ('url', '/services/data/v1.0/sobjects/Account/TEST002')])), ('Id', 'TEST002'), ('AccountNumber', 'test002'), ('LastModifiedDate', '2022-06-01T00:00:00.000+0000'), ('LastModifiedById', 1.234567E+6), ('SystemModstamp', '2022-06-01T00:00:00.000+0000'), ('IsDeleted', False) ]), OrderedDict([ ('attributes', OrderedDict([('type', 'Account'), ('url', '/services/data/v1.0/sobjects/Account/TEST002')])), ('Id', 'TEST002'), ('AccountNumber', 'test002'), ('LastModifiedDate', '2022-06-01T00:00:00.000+0000'), ('LastModifiedById', 1.234567E+6), ('SystemModstamp', '2022-06-01T00:00:00.000+0000'), ('IsDeleted', False) ]), ] class TestFetchCrmDataProcess: def test_run_process_success(self, monkeypatch, caplog): """ Cases: CRMデータ取得処理が正常終了し、期待通りの結果が返ること Arranges: - SalesforceApiClientクラスをモック化し、固定の値を返すようにする Expects: - CRMから取得した取得オブジェクトのリストが返却される - CRMデータ取得処理の仕様に沿った正常系ログが出力されること(デバッグログは除く) """ # Arrange # モック化 with patch('src.fetch_crm_data_process.SalesforceApiClient') as mock: instance = mock.return_value instance.fetch_sf_count.return_value = len(common_expect) instance.fetch_sf_data.return_value = common_expect # Act actual_crm_data_response = fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert # 返り値の期待値チェック assert isinstance(actual_crm_data_response, list) assert isinstance(actual_crm_data_response[0], OrderedDict) assert actual_crm_data_response == common_expect # ログの確認 assert generate_log_message_tuple(log_message='I-FETCH-01 [Account] のCRMからのデータ取得処理を開始します') in caplog.record_tuples assert generate_log_message_tuple(log_message='I-FETCH-02 [Account] の件数取得を開始します') in caplog.record_tuples assert generate_log_message_tuple(log_message='I-FETCH-03 [Account] の件数:[3]') in caplog.record_tuples assert generate_log_message_tuple(log_message='I-FETCH-04 [Account] のレコード取得を開始します') in caplog.record_tuples assert generate_log_message_tuple(log_message='I-FETCH-05 [Account] のレコード取得が成功しました') in caplog.record_tuples assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') in caplog.record_tuples def test_call_depended_modules(self): """ Cases: CRMデータ取得処理内で依存しているモジュールが正しく呼ばれていること Arranges: - CRMデータ取得処理の依存モジュールをモック化する Expects: - 依存しているモジュールが正しく呼ばれている """ # Arrange with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \ patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_counter_inst = mock_counter.return_value mock_counter_inst.describe.return_value = 1 mock_counter_inst.increment.return_value = 1 mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.return_value = '' mock_builder_inst.create_fetch_soql.return_value = '' mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.return_value = 1 mock_client_inst.fetch_sf_data.return_value = common_expect # Act fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert assert mock_counter.call_count == 2, 'リトライカウント用クラスのインスタンスが生成されていること' assert mock_counter_inst.describe.called is False, 'リトライ用のカウントが取得されていないこと' assert mock_counter_inst.increment.called is False, 'リトライ用のカウントが足されていないこと' assert mock_soql_builder.call_count == 1, 'SOQL生成クラスのインスタンスが生成されていること' assert mock_builder_inst.create_count_soql.called is True, '件数取得SOQLが生成されていること' assert mock_builder_inst.create_fetch_soql.called is True, 'データ取得SOQLが生成されていること' assert mock_api_client.call_count == 2, 'Salesforce APIクライアントのインスタンスが生成されていること' assert mock_client_inst.fetch_sf_count.called is True, '件数の取得が呼ばれていること' assert mock_client_inst.fetch_sf_data.called is True, 'レコードの取得が呼ばれていること' def test_raise_create_count_soql(self, monkeypatch, caplog): """ Cases: 件数取得用SOQLが生成できない場合、エラーが発生すること Arranges: - 件数取得用SOQL生成処理で例外が発生するようにする Expects: - 例外が発生する - データ件数取得に失敗した旨のエラーが出力される """ # Arrange with patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.side_effect = Exception('生成エラー') mock_builder_inst.create_fetch_soql.return_value = '' mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.return_value = 1 mock_client_inst.fetch_sf_data.return_value = common_expect # Act with pytest.raises(SalesforceAPIException) as e: fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert assert e.value.error_id == 'E-FETCH-01' assert e.value.func_name == FETCH_JP_NAME assert e.value.args[0] == f'[Account] の件数取得に失敗しました エラー内容:[生成エラー]' def test_raise_create_fetch_soql(self, monkeypatch, caplog): """ Cases: データ取得用SOQLが生成できない場合、エラーが発生すること Arranges: - データ取得用SOQL生成処理で例外が発生するようにする Expects: - 例外が発生する - データ取得に失敗した旨のエラーが出力される """ # Arrange with patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.return_value = '' mock_builder_inst.create_fetch_soql.side_effect = Exception('生成エラー') mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.return_value = 1 mock_client_inst.fetch_sf_data.return_value = common_expect # Act with pytest.raises(SalesforceAPIException) as e: fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert assert e.value.error_id == 'E-FETCH-02' assert e.value.func_name == FETCH_JP_NAME assert e.value.args[0] == f'[Account] のレコード取得に失敗しました エラー内容:[生成エラー]' @pytest.mark.parametrize('timeout_env_name, exception, expect_message', [ ('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-01 CRMの接続処理がタイムアウトしため、リトライします:[1] エラー内容:[接続タイムアウト]'), ('CRM_GET_RECORD_COUNT_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-02 [Account] の件数取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'), ('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-03 [Account] の件数取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'), ], ids=['connection_timeout', 'read_timeout', 'unexpected_exception']) def test_raise_fetch_sf_count_with_retry_success(self, monkeypatch, caplog, timeout_env_name, exception, expect_message): """ Cases: 1. データ件数取得処理で接続タイムアウト例外が発生した場合、リトライした結果復旧し、正常終了すること 2. データ件数取得処理で読み取りタイムアウト例外が発生した場合、リトライした結果復旧し、正常終了すること 3. データ件数取得処理で予期せぬ例外が発生した場合、リトライした結果復旧し、正常終了すること Arranges: - データ件数取得処理の最大リトライ試行回数を3に設定する - timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する - データ件数取得処理の初回に接続タイムアウト例外が発生するようにする Expects: - 正常終了する - データ件数取得に失敗した旨のエラーが出力されない """ monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT', 3) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_INTERVAL', 1) monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1) # Arrange with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \ patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_counter_inst = mock_counter.return_value mock_counter_inst.describe.side_effect = [1, 2, 3] mock_counter_inst.increment.side_effect = [2, 3, 4] mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.return_value = '' mock_builder_inst.create_fetch_soql.return_value = '' mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.side_effect = [exception, len(common_expect)] mock_client_inst.fetch_sf_data.return_value = common_expect # Act fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert assert mock_counter_inst.describe.call_count == 1 assert mock_counter_inst.increment.call_count == 1 assert generate_log_message_tuple( log_level=logging.WARNING, log_message=expect_message) in caplog.record_tuples called_log_counts = len([log for log in caplog.messages if log == expect_message]) assert called_log_counts == 1 assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') in caplog.record_tuples @pytest.mark.parametrize('timeout_env_name, exception, expect_message', [ ('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-01 CRMの接続処理がタイムアウトしため、リトライします:[1] エラー内容:[接続タイムアウト]'), ('CRM_GET_RECORD_COUNT_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-02 [Account] の件数取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'), ('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-03 [Account] の件数取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'), ], ids=['connection_timeout', 'read_timeout', 'unexpected_exception']) def test_raise_fetch_sf_count_with_retry_fail(self, monkeypatch, caplog, timeout_env_name, exception, expect_message): """ Cases: 1. データ件数取得処理で接続タイムアウト例外が発生した場合、リトライした結果復旧せず、異常終了すること 2. データ件数取得処理で読み取りタイムアウト例外が発生した場合、リトライした結果復旧せず、異常終了すること 3. データ件数取得処理で予期せぬ例外が発生した場合、リトライした結果復旧せず、異常終了すること Arranges: - データ件数取得処理の最大リトライ試行回数を3に設定する - timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する - データ件数取得処理の1回目、2回目、3回目で接続タイムアウト例外が発生するようにする Expects: - 異常終了する - データ件数取得に失敗した旨のエラーが出力される """ monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT', 3) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_INTERVAL', 1) monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1) # Arrange with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \ patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_counter_inst = mock_counter.return_value mock_counter_inst.describe.side_effect = [1, 2, 3] mock_counter_inst.increment.side_effect = [2, 3, 4] mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.return_value = '' mock_builder_inst.create_fetch_soql.return_value = '' mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.side_effect = [exception, exception, exception] mock_client_inst.fetch_sf_data.return_value = common_expect # Act with pytest.raises(SalesforceAPIException) as e: fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert # 取得は3回行われる assert mock_counter_inst.describe.call_count == 3 # 足し込みは2回のみ assert mock_counter_inst.increment.call_count == 2 assert generate_log_message_tuple( log_level=logging.WARNING, log_message=expect_message) in caplog.record_tuples called_log_counts = len([log for log in caplog.messages if log == expect_message]) assert called_log_counts == 2 assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') not in caplog.record_tuples assert e.value.error_id == 'E-FETCH-01' assert e.value.func_name == FETCH_JP_NAME # リトライ例外のオブジェクトIDが違うため、in句で比較 assert f'[Account] の件数取得に失敗しました エラー内容:[RetryError' in e.value.args[0] @pytest.mark.parametrize('timeout_env_name, exception, expect_message', [ ('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-04 CRMの接続処理がタイムアウトしため、リトライします:[1] エラー内容:[接続タイムアウト]'), ('CRM_FETCH_RECORD_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-05 [Account] のレコード取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'), ('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-06 [Account] のレコード取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'), ], ids=['connection_timeout', 'read_timeout', 'unexpected_exception']) def test_raise_fetch_sf_data_with_retry_success(self, monkeypatch, caplog, timeout_env_name, exception, expect_message): """ Cases: 1. レコード取得処理で接続タイムアウト例外が発生した場合、リトライした結果復旧し、正常終了すること 2. レコード取得処理で読み取りタイムアウト例外が発生した場合、リトライした結果復旧し、正常終了すること 3. レコード取得処理で予期せぬ例外が発生した場合、リトライした結果復旧し、正常終了すること Arranges: - レコード取得処理の最大リトライ試行回数を3に設定する - timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する - レコード取得処理の初回に接続タイムアウト例外が発生するようにする Expects: - 正常終了する - データレコード取得に失敗した旨のエラーが出力されない """ monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT', 3) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_INTERVAL', 1) monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1) # Arrange with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \ patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_counter_inst = mock_counter.return_value mock_counter_inst.describe.side_effect = [1, 2, 3] mock_counter_inst.increment.side_effect = [2, 3, 4] mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.return_value = '' mock_builder_inst.create_fetch_soql.return_value = '' mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.return_value = 1 mock_client_inst.fetch_sf_data.side_effect = [exception, common_expect] # Act fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert assert mock_counter_inst.describe.call_count == 1 assert mock_counter_inst.increment.call_count == 1 assert generate_log_message_tuple( log_level=logging.WARNING, log_message=expect_message) in caplog.record_tuples called_log_counts = len([log for log in caplog.messages if log == expect_message]) assert called_log_counts == 1 assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') in caplog.record_tuples @pytest.mark.parametrize('timeout_env_name, exception, expect_message', [ ('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-04 CRMの接続処理がタイムアウトしため、リトライします:[1] エラー内容:[接続タイムアウト]'), ('CRM_FETCH_RECORD_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-05 [Account] のレコード取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'), ('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-06 [Account] のレコード取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'), ], ids=['connection_timeout', 'read_timeout', 'unexpected_exception']) def test_raise_fetch_sf_data_with_retry_fail(self, monkeypatch, caplog, timeout_env_name, exception, expect_message): """ Cases: 1. レコード取得処理で接続タイムアウト例外が発生した場合、リトライした結果復旧せず、異常終了すること 2. レコード取得処理で読み取りタイムアウト例外が発生した場合、リトライした結果復旧せず、異常終了すること 3. レコード取得処理で予期せぬ例外が発生した場合、リトライした結果復旧せず、異常終了すること Arranges: - レコード取得処理の最大リトライ試行回数を3に設定する - timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する - レコード取得処理の1回目、2回目、3回目で接続タイムアウト例外が発生するようにする Expects: - 異常終了する - データレコード取得に失敗した旨のエラーが出力される """ monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT', 3) monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_RETRY_MAX_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_RETRY_MIN_INTERVAL', 1) monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_RETRY_INTERVAL', 1) monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1) # Arrange with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \ patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \ patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client: # モック化 mock_counter_inst = mock_counter.return_value mock_counter_inst.describe.side_effect = [1, 2, 3] mock_counter_inst.increment.side_effect = [2, 3, 4] mock_builder_inst = mock_soql_builder.return_value mock_builder_inst.create_count_soql.return_value = '' mock_builder_inst.create_fetch_soql.return_value = '' mock_client_inst = mock_api_client.return_value mock_client_inst.fetch_sf_count.return_value = 1 mock_client_inst.fetch_sf_data.side_effect = [exception, exception, exception] # Act with pytest.raises(SalesforceAPIException) as e: fetch_crm_data_process(common_target_object, common_last_fetch_datetime) # Assert # 取得は3回行われる assert mock_counter_inst.describe.call_count == 3 # 足し込みは2回のみ assert mock_counter_inst.increment.call_count == 2 assert generate_log_message_tuple( log_level=logging.WARNING, log_message=expect_message) in caplog.record_tuples called_log_counts = len([log for log in caplog.messages if log == expect_message]) assert called_log_counts == 2 assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') not in caplog.record_tuples assert e.value.error_id == 'E-FETCH-02' assert e.value.func_name == FETCH_JP_NAME # リトライ例外のオブジェクトIDが違うため、in句で比較 assert f'[Account] のレコード取得に失敗しました エラー内容:[RetryError' in e.value.args[0]