How to improve readabilty and maintainability of @patch and MagicMock statements (avoid long names and String identification)?

huangapple go评论95阅读模式
英文:

How to improve readabilty and maintainability of @patch and MagicMock statements (avoid long names and String identification)?

问题

以下是翻译好的部分:

在我的测试代码中,我有很多样板表达式 "Magic"、"return_"。我还有长字符串用于标识要模拟的函数路径。在重构过程中,这些字符串不会自动替换,我更喜欢直接使用导入的函数。

示例代码:

  1. from mock import patch, MagicMock
  2. from pytest import raises
  3. @patch(
  4. 'foo.baa.qux.long_module_name.calculation.energy_intensity.intensity_table',
  5. MagicMock(return_value='mocked_result_table'),
  6. )

而我更希望:

  1. from better_test_module import patch, Mock, raises
  2. from foo.baa.qux.long_module_name.calculation import energy_intensity
  3. @patch(
  4. energy_intensity.intensity_table,
  5. Mock('mocked_result_table'),
  6. )

或者

  1. @patch(
  2. energy_intensity.intensity_table,
  3. 'mocked_result_table',
  4. )

如果您有其他建议,请告诉我。我想知道为什么建议的解决方案不是默认选项。我不想重复发明轮子。因此,如果已经存在可以使用的库,请告诉我。

相关链接:

https://stackoverflow.com/questions/17181687/mock-vs-magicmock

https://stackoverflow.com/questions/67580353/how-to-override-getitem-on-a-magicmock-subclass

英文:

In my test code I have a lot of boilerplate expressions "Magic", "return_". I also have lengthy strings to identify the paths of the functions to mock.
The strings are not automatically replaced during refactoring and I would prefer to directly use the imported functions.

Example code:

  1. from mock import patch, MagicMock
  2. from pytest import raises
  3. @patch(
  4. 'foo.baa.qux.long_module_name.calculation.energy_intensity.intensity_table',
  5. MagicMock(return_value='mocked_result_table'),
  6. )

Instead I would prefer:

  1. from better_test_module import patch, Mock, raises
  2. from foo.baa.qux.long_module_name.calculation import energy_intensity
  3. @patch(
  4. energy_intensity.intensity_table,
  5. Mock('mocked_result_table'),
  6. )

or

  1. @patch(
  2. energy_intensity.intensity_table,
  3. 'mocked_result_table',
  4. )

I post my corresponding custom implementation as an answer below.

If you have other suggestions, please let me know. I am wondering why the proposed solution is not the default. I do not want to reinvent the wheel.
Therefore, if there is an already existing library I could use, please let me know.

Related:

https://stackoverflow.com/questions/17181687/mock-vs-magicmock

https://stackoverflow.com/questions/67580353/how-to-override-getitem-on-a-magicmock-subclass

答案1

得分: 0

创建一个包装模块,允许使用更短的名称并直接传递函数。(如果类似的东西已经存在作为一个pip包,请告诉我;不想重复造轮子。)

用法:

  1. from my_test_utils.mock import patch, Mock, raises
  2. from foo.baa.qux.long_module_name.calculation import energy_intensity
  3. @patch(
  4. energy_intensity.intensity_table,
  5. Mock('mocked_result_table'),
  6. )

my_test_utils/mock.py 中的第一个草案代码:

  1. from mock import MagicMock, DEFAULT
  2. from mock import patch as original_patch
  3. from pytest import raises as original_raises
  4. class Mock(MagicMock):
  5. # 这个类用作MagicMock的包装器,允许使用更短的语法
  6. def __new__(cls, *args, **kwargs):
  7. if len(args) > 0:
  8. first_argument = args[0]
  9. mock = MagicMock(return_value=first_argument, *args[1:], **kwargs)
  10. else:
  11. mock = MagicMock(**kwargs)
  12. return mock
  13. def assert_called_once(self, *args, **kwargs): # pylint: disable = useless-parent-delegation
  14. # pylint没有找到这个方法,除非定义为代理
  15. super().assert_called_once(*args, **kwargs)
  16. def assert_not_called(self, *args, **kwargs): # pylint: disable = useless-parent-delegation
  17. # pylint没有找到这个方法,除非定义为代理
  18. super().assert_not_called(*args, **kwargs)
  19. def patch(item_to_patch, *args, **kwargs):
  20. if isinstance(item_to_patch, str):
  21. raise KeyError('请直接导入并使用项目,而不是传递字符串路径!')
  22. module_path = item_to_patch.__module__
  23. if hasattr(item_to_patch, '__qualname__'):
  24. item_path = module_path + '.' + item_to_patch.__qualname__
  25. else:
  26. name = _try_to_get_object_name(item_to_patch, module_path)
  27. item_path = module_path + '.' + name
  28. item_path = item_path.lstrip('_')
  29. return original_patch(item_path, *args, **kwargs)
  30. def _try_to_get_object_name(object_to_patch, module_path):
  31. module = __import__(module_path)
  32. name = None
  33. for attribute_name in dir(module):
  34. attribute = getattr(module, attribute_name)
  35. if attribute == object_to_patch:
  36. if name is None:
  37. name = attribute_name
  38. else:
  39. # 对象在其父对象内不是唯一的,但被使用了两次
  40. message = (
  41. '无法识别要打补丁的项目,因为对象不是唯一的。'
  42. + ' 请使用唯一的字符串路径。'
  43. )
  44. raise KeyError(message)
  45. if name is None:
  46. raise KeyError('无法识别要打补丁的对象。')
  47. return name
  48. def raises(*args):
  49. # 这个函数用作raises的包装器,以便能够从与其他函数相同的模块导入它
  50. return original_raises(*args)

请注意:以上是您提供的代码的翻译。如果有任何其他问题或需要进一步的帮助,请随时告诉我。

英文:

Create a wrapper module allowing for shorter names and passing functions directly. (If something like this already exists as a pip package, please let me know; don't want to reinvent the wheel.)

Usage:

  1. from my_test_utils.mock import patch, Mock, raises
  2. from foo.baa.qux.long_module_name.calculation import energy_intensity
  3. @patch(
  4. energy_intensity.intensity_table,
  5. Mock('mocked_result_table'),
  6. )

First draft for my wrapping code in my_test_utils/mock.py:

  1. from mock import MagicMock, DEFAULT
  2. from mock import patch as original_patch
  3. from pytest import raises as original_raises
  4. class Mock(MagicMock):
  5. # This class serves as a wrapper for MagicMock to allow for shorter syntax
  6. def __new__(cls, *args, **kwargs):
  7. if len(args) > 0:
  8. first_argument = args[0]
  9. mock = MagicMock(return_value=first_argument, *args[1:], **kwargs)
  10. else:
  11. mock = MagicMock(**kwargs)
  12. return mock
  13. def assert_called_once(self, *args, **kwargs): # pylint: disable = useless-parent-delegation
  14. # pylint did not find this method without defining it as a proxy
  15. super().assert_called_once(*args, **kwargs)
  16. def assert_not_called(self, *args, **kwargs): # pylint: disable = useless-parent-delegation
  17. # pylint did not find this method without defining it as a proxy
  18. super().assert_not_called(*args, **kwargs)
  19. def patch(item_to_patch, *args, **kwargs):
  20. if isinstance(item_to_patch, str):
  21. raise KeyError('Please import and use items directly instead of passing string paths!')
  22. module_path = item_to_patch.__module__
  23. if hasattr(item_to_patch, '__qualname__'):
  24. item_path = module_path + '.' + item_to_patch.__qualname__
  25. else:
  26. name = _try_to_get_object_name(item_to_patch, module_path)
  27. item_path = module_path + '.' + name
  28. item_path = item_path.lstrip('_')
  29. return original_patch(item_path, *args, **kwargs)
  30. def _try_to_get_object_name(object_to_patch, module_path):
  31. module = __import__(module_path)
  32. name = None
  33. for attribute_name in dir(module):
  34. attribute = getattr(module, attribute_name)
  35. if attribute == object_to_patch:
  36. if name is None:
  37. name = attribute_name
  38. else:
  39. # object is not unique within its parent but used twice
  40. message = (
  41. 'Could not identify item to patch because object is not unique.'
  42. + ' Please use a unique string path.'
  43. )
  44. raise KeyError(message)
  45. if name is None:
  46. raise KeyError('Could not identify object to patch.')
  47. return name
  48. def raises(*args):
  49. # This function serves as a wrapper for raises to be able to import it from the same module as the other functions
  50. return original_raises(*args)

huangapple
  • 本文由 发表于 2023年6月27日 20:27:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76564857.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定