diff --git a/coveo-ref/README.md b/coveo-ref/README.md index fd1f911..1f9b9ef 100644 --- a/coveo-ref/README.md +++ b/coveo-ref/README.md @@ -1,8 +1,21 @@ -# coveo-ref - -Refactorable mock targets - -## Demo + +* [Demo](#demo) +* [Common Mock Recipes](#common-mock-recipes) + * [Mock something globally without context](#mock-something-globally-without-context) + * [Option 1: by leveraging the import mechanism](#option-1-by-leveraging-the-import-mechanism) + * [Option 2: By wrapping a hidden function](#option-2-by-wrapping-a-hidden-function) + * [Mock something for a given context](#mock-something-for-a-given-context) + * [Brief Example:](#brief-example) + * [Detailed Example:](#detailed-example) + * [Mock something for the current context](#mock-something-for-the-current-context) + * [Mock a method on a class](#mock-a-method-on-a-class) + * [Mock a method on one instance of a class](#mock-a-method-on-one-instance-of-a-class) + * [Mock an attribute on a class/instance/module/function/object/etc](#mock-an-attribute-on-a-classinstancemodulefunctionobjectetc) + * [Mock a property](#mock-a-property) + * [Mock a classmethod or staticmethod on a specific instance](#mock-a-classmethod-or-staticmethod-on-a-specific-instance) + + +# Demo Consider this common piece of code: @@ -143,10 +156,9 @@ def test() -> None: Please refer to the docstring of `ref` for argument usage information. -## Common Mock Recipes - -### Mock something globally without context -#### Option 1: by leveraging the import mechanism +# Common Mock Recipes +## Mock something globally without context +### Option 1: by leveraging the import mechanism To mock something globally without regards for the context, it has to be accessed through a dot `.` by the context. @@ -166,7 +178,7 @@ def test(http_response_close_mock: MagicMock) -> None: ``` The target is `HTTPResponse.close`, which lives in the `http.client` module. -The contextof the test is the `process` function, which lives in the `mymodule.tasks` module. +The context of the test is the `process` function, which lives in the `mymodule.tasks` module. Let's take a look at `mymodule.tasks`'s source code: @@ -195,7 +207,7 @@ Then the patch would not affect the object used by the `process` function anymor module that uses the dot to reach `HTTPResponse` since the patch was _still_ applied globally. -#### Option 2: By wrapping a hidden function +### Option 2: By wrapping a hidden function Another approach to mocking things globally is to hide a function behind another, and mock the hidden function. This allows modules to use whatever import style they want, and the mocks become straightforward to setup. @@ -238,12 +250,30 @@ def test(api_client_mock: MagicMock) -> None: ``` -### Mock something for a given context +## Mock something for a given context -When you want to mock something for a given module, you must provide a hint to `ref` as the `context` argument. +If you don't use a global mock, then you _must_ specify the context of the mock. + +The context is a reference point for `ref`. +Most of the time, the class or function you're testing should be the context. +Generally speaking, pick a context as close to your implementation as possible to allow seamless refactoring. + +### Brief Example: + +```python +from unittest.mock import patch, MagicMock +from coveo_ref import ref + +from ... import thing_to_mock +from ... import thing_to_test + +@patch(*ref(thing_to_mock, context=thing_to_test)) +def test(mocked_thing: MagicMock) -> None: + assert thing_to_test() + mocked_thing.assert_called() +``` -The hint may be a module, or a function/class defined within that module. "Defined" here means that "def" or "class" -was used _in that module_. If the hint was imported into the module, it will not work: +### Detailed Example: `mymodule.tasks`: @@ -288,9 +318,9 @@ def test(get_api_client_mock: MagicMock) -> None: The 3rd method is encouraged: provide the function or class that is actually using the `get_api_client` import. In our example, that's the `process` function. If `process` was ever moved to a different module, it would carry the `get_api_client` import, and the mock would -be automatically adjusted to target `process`'s new module without changes. +be automatically adjusted to target `process`'s new module without changes. 🚀 -### Mock something for the current context +## Mock something for the current context Sometimes, the test file _is_ the context. When that happens, just pass `__name__` as the context: @@ -311,7 +341,7 @@ def test() -> None: ``` -### Mock a method on a class +## Mock a method on a class Since a method cannot be imported and can only be accessed through the use of a dot `.` on a class or instance, you can always patch methods globally: @@ -322,7 +352,7 @@ with patch(*ref(MyClass.fn)): ... This is because no module can import `fn`; it has to go through an import of `MyClass`. -### Mock a method on one instance of a class +## Mock a method on one instance of a class Simply add `obj=True` and use `patch.object()`: @@ -331,7 +361,7 @@ with patch.object(*ref(instance.fn, obj=True)): ... ``` -### Mock an attribute on a class/instance/module/function/object/etc +## Mock an attribute on a class/instance/module/function/object/etc `ref` cannot help with this task: - You cannot refer an attribute that exists (you would pass the value, not a reference) @@ -364,7 +394,7 @@ There's no way to make the example work with `ref` because there's no way to ref getting the value of `a`, unless we hardcode a string, which defeats the purpose of `ref` completely. -### Mock a property +## Mock a property You can only patch a property globally, through its class: @@ -392,7 +422,7 @@ properties work. If you try, `mock.patch.object()` will complain that the property is read only. -### Mock a classmethod or staticmethod on a specific instance +## Mock a classmethod or staticmethod on a specific instance When inspecting these special methods on an instance, `ref` ends up finding the class instead of the instance. diff --git a/coveo-ref/coveo_ref/__init__.py b/coveo-ref/coveo_ref/__init__.py index f7124c6..44225bc 100644 --- a/coveo-ref/coveo_ref/__init__.py +++ b/coveo-ref/coveo_ref/__init__.py @@ -307,13 +307,37 @@ def ref( _bypass_context_check: bool = False, ) -> Union[Tuple[str], Tuple[Any, str]]: """ - Replaces `resolves_mock_target`. Named for brevity. + Docs: https://github.com/coveooss/coveo-python-oss/blob/main/coveo-ref/README.md + + Cheat sheet: + + Mock a property (this is global and will not work on instances): + *ref(cls.property, new_callable=PropertyMock, return_value=...) + + Mock a method (no need for a context): + *ref(cls.method) + + Mock a method on a single instance: + *ref(instance.method, obj=True) + + Mock an attribute on a class/instance/module/function/object: + You can't use ref to mock an attribute. + If patching an instance, try this (sorry about the string): + patch.object(instance, "attribute", new="return_value") + + Mock a staticmethod or a classmethod: + To patch globally: + *ref(cls.staticorclassmethod) + To patch on a single instance: + *ref(cls.staticorclassmethod, context=instance, obj=True) + + Function doc for maintainers: Returns a tuple meant to be unpacked into the `mock.patch` or `mock.patch.object` functions in order to enable refactorable mocks. The idea is to provide the thing to mock as the target, and sometimes, the thing that is being tested - as the context. Refer to `coveo-ref`'s readme to better understand when a context is necessary. + as the context. For example, pass the `HTTPResponse` class as the target and the `my_module.function_to_test` function as the context, so that `my_module.HTTPResponse` becomes mocked (and not httplib.client.HTTPResponse).