Make mocking simple, free of hardcoded trings and therefore... refactorable!
<!-- TOC -->
* [Tutorial](#tutorial)
* [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)
<!-- TOC -->
# Tutorial
Consider this common piece of code:
```python
from unittest.mock import patch, MagicMock
@patch("mymodule.clients.APIClient._do_request")
def test(api_client_mock: MagicMock) -> None:
...
```
Because the mock target is a string, it makes it difficult to move things around without breaking the tests. You need a
tool that can extract the string representation of a python objet. This is what `ref` was built for:
```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import APIClient
@patch(*ref(APIClient._do_request))
def test(api_client_mock: MagicMock) -> None:
...
```
🚀 This way, you can rename or move `mymodule`, `clients`, `APIClient` or even `_do_request`, and your IDE should find
these and adjust them just like any other reference in your project.
Let's examine a more complex example:
```python
from unittest.mock import patch, MagicMock
from mymodule.tasks import process
@patch("mymodule.tasks.get_api_client")
def test(get_api_client_mock: MagicMock) -> None:
assert process() is None # pretend this tests the process function
```
The interesting thing in this example is that we're mocking `get_api_client` in the `tasks` module.
Let's take a look at the `tasks` module:
```python
from typing import Optional
from mymodule.clients import get_api_client
def process() -> Optional[bool]:
client = get_api_client()
return ...
```
As we can see, `get_api_client` is defined in another module.
The test needs to patch the function _in the tasks module_ since that's the context it will be called from.
Unfortunately, inspecting `get_api_client` from the `tasks` module at runtime leads us back to `mymodule.clients`.
This single complexity means that hardcoding the context `mymodule.tasks` and symbol `get_api_client` into a string
for the patch is the straightforward solution.
But with `ref`, you specify the context separately:
```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import get_api_client
from mymodule.tasks import process
@patch(*ref(get_api_client, context=process))
def test(get_api_client_mock: MagicMock) -> None:
assert process() is None # pretend this tests the process function
```
🚀 By giving a context to `ref`, the symbol `get_api_client` will be resolved from the context of `process`, which is the
`mymodule.tasks` module. The result is `mymodule.tasks.get_api_client`.
If either objects (`get_api_client` or `process`) are moved or renamed using a refactoring tool, the mock will still
point to the correct name and context.
🚀 And a nice bonus is that your IDE can jump to `get_api_client`'s definition from the test file now!
It should be noted that this isn't just some string manipulation. `ref` will import and inspect modules and objects
to make sure that they're correct. Here's a more complex case with a renamed symbol:
The module:
```python
from typing import Optional
from mymodule.clients import get_api_client as client_factory # it got renamed! 😱
def process() -> Optional[bool]:
client = client_factory()
return ...
```
The test:
```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import get_api_client
from mymodule.tasks import process
@patch(*ref(get_api_client, context=process))
def test(get_api_client_mock: MagicMock) -> None:
assert process() is None # pretend this tests the process function
```
Notice how the test and patch did not change despite the renamed symbol?
🚀 This is because `ref` will find `get_api_client` as `client_factory` when inspecting `mymodule.tasks` module,
and return `mymodule.tasks.client_factory`.
We can also use ref with `patch.object()` in order to patch a single instance. Consider the following code:
```python
from unittest.mock import patch
from mymodule.clients import APIClient
def test() -> None:
client = APIClient()
with patch.object(client, "_do_request"):
...
```
🚀 By specifying `obj=True` to `ref`, you will obtain a `Tuple[instance, attribute_to_patch_as_a_string]` that you
can unpack to `patch.object()`:
```python
from unittest.mock import patch
from coveo_ref import ref
from mymodule.clients import APIClient
def test() -> None:
client = APIClient()
with patch.object(*ref(client._do_request, obj=True)):
...
```
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
To mock something globally without regards for the context, it has to be accessed through a dot `.` by the context.
For instance, consider this test:
```python
from http.client import HTTPResponse
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.tasks import process
@patch(*ref(HTTPResponse.close))
def test(http_response_close_mock: MagicMock) -> None:
assert process()
```
The target is `HTTPResponse.close`, which lives in the `http.client` 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:
```python
from http import client
def process() -> bool:
_ = client.HTTPResponse(...) # of course this is fake, but serves the example
return ...
```
Since `mymodule.tasks` reaches `HTTPResponse` through a dot (i.e.: `client.HTTPResponse`), we can patch `HTTPResponse`
without using `mymodule.tasks` as the context.
However, if `mymodule.tasks` was written like this:
```python
from http.client import HTTPResponse
def process() -> bool:
_ = HTTPResponse(...)
return ...
```
Then the patch would not affect the object used by the `process` function anymore. However, it would affect any other
module that uses the dot to reach `HTTPResponse` since the patch was _still_ applied globally.
### 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.
Pretend this is `mymodule.clients`:
```python
class APIClient:
...
def get_api_client() -> APIClient:
return _get_api_client()
def _get_api_client() -> APIClient:
return APIClient()
```
And this is `mymodule.tasks`:
```python
from mymodule.clients import get_api_client
def process() -> bool:
return get_api_client() is not None
```
So you _know_ this works globally, because no one will (should?) import the private one except the test:
```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.tasks import process
from mymodule.clients import _get_api_client
@patch(*ref(_get_api_client))
def test(api_client_mock: MagicMock) -> None:
assert process()
```
## Mock something for a given context
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()
```
### Detailed Example:
`mymodule.tasks`:
```python
from mymodule.clients import get_api_client
def process() -> bool:
client = get_api_client()
return ...
```
The test, showing 3 different methods that work:
```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import get_api_client
from mymodule.tasks import process
# you can pass the module as the context
import mymodule
@patch(*ref(get_api_client, context=mymodule.tasks))
def test(get_api_client_mock: MagicMock) -> None:
assert process()
# you can pass the module as the context, version 2
from mymodule import tasks
@patch(*ref(get_api_client, context=tasks))
def test(get_api_client_mock: MagicMock) -> None:
assert process()
# you can also pass a function or a class defined in the `tasks` module
from mymodule.tasks import process
@patch(*ref(get_api_client, context=process))
def test(get_api_client_mock: MagicMock) -> None:
assert process()
```
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. 🚀
## Mock something for the current context
Sometimes, the test file _is_ the context. When that happens, just pass `__name__` as the context:
```python
from unittest.mock import patch
from coveo_ref import ref
from mymodule.clients import get_api_client, APIClient
def _prepare_test() -> APIClient:
client = get_api_client()
...
return client
@patch(*ref(get_api_client, context=__name__))
def test() -> None:
client = _prepare_test()
...
```
## 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:
```python
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
Simply add `obj=True` and use `patch.object()`:
```python
with patch.object(*ref(instance.fn, obj=True)): ...
```
## 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)
- You cannot refer an attribute that doesn't exist (because it doesn't exist!)
For this, there's no going around hardcoding the attribute name in a string:
```python
class MyClass:
def __init__(self) -> None:
self.a = 1
def test_attr() -> None:
instance = MyClass()
with patch.object(instance, "a", new=2):
assert instance.a == 2
assert MyClass().a == 1
```
This sometimes work when patching **instances**.
The example works because `a` is a simple attribute that lives in `instance.__dict__` and `patch.object` knows
about that.
But if you tried to patch `MyClass` instead of `instance`, `mock.patch` would complain that there's no
such thing as `a` over there.
Thus, patching an attribute globally will most likely result in a lot of wasted time, and should be avoided.
There's no way to make the example work with `ref` because there's no way to refer `instance.a` without actually
getting the value of `a`, unless we hardcode a string, which defeats the purpose of `ref` completely.
## Mock a property
You can only patch a property globally, through its class:
```python
class MyClass:
@property
def get(self) -> bool:
return False
```
```python
from unittest.mock import PropertyMock, patch, MagicMock
from coveo_ref import ref
from mymodule import MyClass
@patch(*ref(MyClass.get), new_callable=PropertyMock, return_value=True)
def test(my_class_get_mock: MagicMock) -> None:
assert MyClass().get == True
my_class_get_mock.assert_called_once()
```
You **cannot** patch a property on an instance, this is a limitation of `unittest.mock` because of the way
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
When inspecting these special methods on an instance, `ref` ends up finding the class instead of the instance.
Therefore, `ref` is unable to return a `Tuple[instance, function_name]`.
It would return `Tuple[class, function_name]`, resulting in a global patch. 😱
But `ref` will detect this mistake, and will raise a helpful exception if it cannot return an instance when you
specified `obj=True`.
For this particular scenario, the workaround is to provide the instance as the context:
```python
from unittest.mock import patch
from coveo_ref import ref
class MyClass:
@staticmethod
def get() -> bool:
return False
def test() -> None:
instance = MyClass()
with patch.object(*ref(instance.get, context=instance, obj=True)) as fn_mock:
assert instance.get == True
assert MyClass().get == False # new instances are not affected by the object mock
fn_mock.assert_called_once()
```
Some may prefer a more semantically-correct version by specifying the target through the class instead of the
instance. In the end, these are all equivalent:
```python
with patch.object(instance, "get"):
...
with patch.object(*ref(instance.get, context=instance, obj=True)):
...
with patch.object(*ref(MockClass.get, context=instance, obj=True)):
...
```
In this case, the version without ref is much shorter and arguably more pleasant for the eye, but `get` can no longer
be renamed without altering the tests.
Raw data
{
"_id": null,
"home_page": "https://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref",
"name": "coveo-ref",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": null,
"author": "Jonathan Pich\u00e9",
"author_email": "tools@coveo.com",
"download_url": "https://files.pythonhosted.org/packages/49/e5/df697d2fed1a7a20275ffbf153437e8697b85c44bcea456532c63ecf4dea/coveo_ref-1.0.4.tar.gz",
"platform": null,
"description": "Make mocking simple, free of hardcoded trings and therefore... refactorable!\n\n<!-- TOC -->\n* [Tutorial](#tutorial)\n* [Common Mock Recipes](#common-mock-recipes)\n * [Mock something globally without context](#mock-something-globally-without-context)\n * [Option 1: by leveraging the import mechanism](#option-1-by-leveraging-the-import-mechanism)\n * [Option 2: By wrapping a hidden function](#option-2-by-wrapping-a-hidden-function)\n * [Mock something for a given context](#mock-something-for-a-given-context)\n * [Brief Example:](#brief-example)\n * [Detailed Example:](#detailed-example)\n * [Mock something for the current context](#mock-something-for-the-current-context)\n * [Mock a method on a class](#mock-a-method-on-a-class)\n * [Mock a method on one instance of a class](#mock-a-method-on-one-instance-of-a-class)\n * [Mock an attribute on a class/instance/module/function/object/etc](#mock-an-attribute-on-a-classinstancemodulefunctionobjectetc)\n * [Mock a property](#mock-a-property)\n * [Mock a classmethod or staticmethod on a specific instance](#mock-a-classmethod-or-staticmethod-on-a-specific-instance)\n<!-- TOC -->\n\n\n# Tutorial\n\nConsider this common piece of code:\n\n```python\nfrom unittest.mock import patch, MagicMock\n\n@patch(\"mymodule.clients.APIClient._do_request\")\ndef test(api_client_mock: MagicMock) -> None:\n ...\n```\n\nBecause the mock target is a string, it makes it difficult to move things around without breaking the tests. You need a\ntool that can extract the string representation of a python objet. This is what `ref` was built for:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\nfrom mymodule.clients import APIClient\n\n@patch(*ref(APIClient._do_request))\ndef test(api_client_mock: MagicMock) -> None:\n ...\n```\n\n\ud83d\ude80 This way, you can rename or move `mymodule`, `clients`, `APIClient` or even `_do_request`, and your IDE should find\nthese and adjust them just like any other reference in your project.\n\nLet's examine a more complex example:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom mymodule.tasks import process\n\n@patch(\"mymodule.tasks.get_api_client\")\ndef test(get_api_client_mock: MagicMock) -> None:\n assert process() is None # pretend this tests the process function\n```\n\nThe interesting thing in this example is that we're mocking `get_api_client` in the `tasks` module. \nLet's take a look at the `tasks` module:\n\n```python\nfrom typing import Optional\nfrom mymodule.clients import get_api_client\n\ndef process() -> Optional[bool]:\n client = get_api_client()\n return ...\n```\n\nAs we can see, `get_api_client` is defined in another module.\nThe test needs to patch the function _in the tasks module_ since that's the context it will be called from. \nUnfortunately, inspecting `get_api_client` from the `tasks` module at runtime leads us back to `mymodule.clients`.\n\nThis single complexity means that hardcoding the context `mymodule.tasks` and symbol `get_api_client` into a string\nfor the patch is the straightforward solution.\n\nBut with `ref`, you specify the context separately:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\nfrom mymodule.clients import get_api_client\nfrom mymodule.tasks import process\n\n\n@patch(*ref(get_api_client, context=process))\ndef test(get_api_client_mock: MagicMock) -> None:\n assert process() is None # pretend this tests the process function\n```\n\n\ud83d\ude80 By giving a context to `ref`, the symbol `get_api_client` will be resolved from the context of `process`, which is the\n`mymodule.tasks` module. The result is `mymodule.tasks.get_api_client`.\n\nIf either objects (`get_api_client` or `process`) are moved or renamed using a refactoring tool, the mock will still\npoint to the correct name and context.\n\n\ud83d\ude80 And a nice bonus is that your IDE can jump to `get_api_client`'s definition from the test file now!\n\nIt should be noted that this isn't just some string manipulation. `ref` will import and inspect modules and objects\nto make sure that they're correct. Here's a more complex case with a renamed symbol:\n\nThe module:\n\n```python\nfrom typing import Optional\nfrom mymodule.clients import get_api_client as client_factory # it got renamed! \ud83d\ude31\n\ndef process() -> Optional[bool]:\n client = client_factory()\n return ...\n```\n\nThe test:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\nfrom mymodule.clients import get_api_client\nfrom mymodule.tasks import process\n\n\n@patch(*ref(get_api_client, context=process))\ndef test(get_api_client_mock: MagicMock) -> None:\n assert process() is None # pretend this tests the process function\n```\n\nNotice how the test and patch did not change despite the renamed symbol?\n\n\ud83d\ude80 This is because `ref` will find `get_api_client` as `client_factory` when inspecting `mymodule.tasks` module,\nand return `mymodule.tasks.client_factory`.\n\nWe can also use ref with `patch.object()` in order to patch a single instance. Consider the following code:\n\n```python\nfrom unittest.mock import patch\nfrom mymodule.clients import APIClient\n\ndef test() -> None:\n client = APIClient()\n with patch.object(client, \"_do_request\"):\n ...\n```\n\n\ud83d\ude80 By specifying `obj=True` to `ref`, you will obtain a `Tuple[instance, attribute_to_patch_as_a_string]` that you\ncan unpack to `patch.object()`:\n\n```python\nfrom unittest.mock import patch\nfrom coveo_ref import ref\nfrom mymodule.clients import APIClient\n\ndef test() -> None:\n client = APIClient()\n with patch.object(*ref(client._do_request, obj=True)):\n ...\n```\n\nPlease refer to the docstring of `ref` for argument usage information.\n\n# Common Mock Recipes\n## Mock something globally without context\n### Option 1: by leveraging the import mechanism\n\nTo mock something globally without regards for the context, it has to be accessed through a dot `.` by the context.\n\nFor instance, consider this test:\n\n```python\nfrom http.client import HTTPResponse\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule.tasks import process\n\n\n@patch(*ref(HTTPResponse.close))\ndef test(http_response_close_mock: MagicMock) -> None:\n assert process()\n```\n\nThe target is `HTTPResponse.close`, which lives in the `http.client` module.\nThe context of the test is the `process` function, which lives in the `mymodule.tasks` module.\nLet's take a look at `mymodule.tasks`'s source code:\n\n\n```python\nfrom http import client\n\ndef process() -> bool:\n _ = client.HTTPResponse(...) # of course this is fake, but serves the example\n return ...\n```\n\nSince `mymodule.tasks` reaches `HTTPResponse` through a dot (i.e.: `client.HTTPResponse`), we can patch `HTTPResponse`\nwithout using `mymodule.tasks` as the context.\n\nHowever, if `mymodule.tasks` was written like this:\n\n```python\nfrom http.client import HTTPResponse\n\ndef process() -> bool:\n _ = HTTPResponse(...)\n return ...\n```\n\nThen the patch would not affect the object used by the `process` function anymore. However, it would affect any other \nmodule that uses the dot to reach `HTTPResponse` since the patch was _still_ applied globally.\n \n\n### Option 2: By wrapping a hidden function\n\nAnother approach to mocking things globally is to hide a function behind another, and mock the hidden function.\nThis allows modules to use whatever import style they want, and the mocks become straightforward to setup.\n\nPretend this is `mymodule.clients`:\n\n```python\nclass APIClient:\n ...\n\ndef get_api_client() -> APIClient:\n return _get_api_client()\n\ndef _get_api_client() -> APIClient:\n return APIClient()\n```\n\nAnd this is `mymodule.tasks`:\n\n```python\nfrom mymodule.clients import get_api_client\n\ndef process() -> bool:\n return get_api_client() is not None\n```\n\nSo you _know_ this works globally, because no one will (should?) import the private one except the test:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule.tasks import process\nfrom mymodule.clients import _get_api_client\n\n\n@patch(*ref(_get_api_client))\ndef test(api_client_mock: MagicMock) -> None:\n assert process()\n```\n\n\n## Mock something for a given context\n\nIf you don't use a global mock, then you _must_ specify the context of the mock.\n\nThe context is a reference point for `ref`.\nMost of the time, the class or function you're testing should be the context.\nGenerally speaking, pick a context as close to your implementation as possible to allow seamless refactoring.\n\n### Brief Example:\n \n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom ... import thing_to_mock\nfrom ... import thing_to_test\n\n@patch(*ref(thing_to_mock, context=thing_to_test))\ndef test(mocked_thing: MagicMock) -> None:\n assert thing_to_test()\n mocked_thing.assert_called()\n```\n\n### Detailed Example:\n\n`mymodule.tasks`:\n\n```python\nfrom mymodule.clients import get_api_client\n\ndef process() -> bool:\n client = get_api_client()\n return ...\n```\n\nThe test, showing 3 different methods that work:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule.clients import get_api_client\nfrom mymodule.tasks import process\n\n# you can pass the module as the context\nimport mymodule\n\n@patch(*ref(get_api_client, context=mymodule.tasks))\ndef test(get_api_client_mock: MagicMock) -> None:\n assert process()\n\n# you can pass the module as the context, version 2\nfrom mymodule import tasks\n \n@patch(*ref(get_api_client, context=tasks))\ndef test(get_api_client_mock: MagicMock) -> None:\n assert process()\n\n# you can also pass a function or a class defined in the `tasks` module\nfrom mymodule.tasks import process\n@patch(*ref(get_api_client, context=process))\ndef test(get_api_client_mock: MagicMock) -> None:\n assert process()\n```\n\nThe 3rd method is encouraged: provide the function or class that is actually using the `get_api_client` import.\nIn our example, that's the `process` function.\nIf `process` was ever moved to a different module, it would carry the `get_api_client` import, and the mock would\nbe automatically adjusted to target `process`'s new module without changes. \ud83d\ude80\n\n## Mock something for the current context\n\nSometimes, the test file _is_ the context. When that happens, just pass `__name__` as the context:\n\n```python\nfrom unittest.mock import patch\nfrom coveo_ref import ref\nfrom mymodule.clients import get_api_client, APIClient\n\ndef _prepare_test() -> APIClient:\n client = get_api_client()\n ...\n return client\n \n@patch(*ref(get_api_client, context=__name__))\ndef test() -> None:\n client = _prepare_test()\n ...\n```\n\n\n## Mock a method on a class\n\nSince a method cannot be imported and can only be accessed through the use of a dot `.` on a class or instance, \nyou can always patch methods globally:\n\n```python\nwith patch(*ref(MyClass.fn)): ...\n```\n\nThis is because no module can import `fn`; it has to go through an import of `MyClass`.\n\n## Mock a method on one instance of a class\n\nSimply add `obj=True` and use `patch.object()`:\n\n```python\nwith patch.object(*ref(instance.fn, obj=True)): ...\n```\n\n\n## Mock an attribute on a class/instance/module/function/object/etc\n\n`ref` cannot help with this task:\n- You cannot refer an attribute that exists (you would pass the value, not a reference)\n- You cannot refer an attribute that doesn't exist (because it doesn't exist!)\n\nFor this, there's no going around hardcoding the attribute name in a string:\n\n```python\nclass MyClass:\n def __init__(self) -> None:\n self.a = 1\n\n\ndef test_attr() -> None:\n instance = MyClass()\n with patch.object(instance, \"a\", new=2):\n assert instance.a == 2\n assert MyClass().a == 1\n```\n\nThis sometimes work when patching **instances**. \nThe example works because `a` is a simple attribute that lives in `instance.__dict__` and `patch.object` knows\nabout that.\n\nBut if you tried to patch `MyClass` instead of `instance`, `mock.patch` would complain that there's no \nsuch thing as `a` over there.\nThus, patching an attribute globally will most likely result in a lot of wasted time, and should be avoided.\n\nThere's no way to make the example work with `ref` because there's no way to refer `instance.a` without actually\ngetting the value of `a`, unless we hardcode a string, which defeats the purpose of `ref` completely.\n\n\n## Mock a property\n\nYou can only patch a property globally, through its class:\n\n```python\nclass MyClass:\n @property\n def get(self) -> bool:\n return False\n```\n\n```python\nfrom unittest.mock import PropertyMock, patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule import MyClass\n\n@patch(*ref(MyClass.get), new_callable=PropertyMock, return_value=True)\ndef test(my_class_get_mock: MagicMock) -> None:\n assert MyClass().get == True\n my_class_get_mock.assert_called_once()\n```\n\nYou **cannot** patch a property on an instance, this is a limitation of `unittest.mock` because of the way\nproperties work.\nIf you try, `mock.patch.object()` will complain that the property is read only.\n\n\n## Mock a classmethod or staticmethod on a specific instance\n\nWhen inspecting these special methods on an instance, `ref` ends up finding the class instead of the instance.\n\nTherefore, `ref` is unable to return a `Tuple[instance, function_name]`.\nIt would return `Tuple[class, function_name]`, resulting in a global patch. \ud83d\ude31\n\nBut `ref` will detect this mistake, and will raise a helpful exception if it cannot return an instance when you\nspecified `obj=True`.\n\nFor this particular scenario, the workaround is to provide the instance as the context:\n\n```python\nfrom unittest.mock import patch\nfrom coveo_ref import ref\n\n\nclass MyClass:\n @staticmethod\n def get() -> bool:\n return False\n\n \ndef test() -> None:\n instance = MyClass()\n with patch.object(*ref(instance.get, context=instance, obj=True)) as fn_mock:\n assert instance.get == True\n assert MyClass().get == False # new instances are not affected by the object mock\n fn_mock.assert_called_once()\n```\n\nSome may prefer a more semantically-correct version by specifying the target through the class instead of the \ninstance. In the end, these are all equivalent:\n\n```python\nwith patch.object(instance, \"get\"): \n ...\n\nwith patch.object(*ref(instance.get, context=instance, obj=True)): \n ...\n\nwith patch.object(*ref(MockClass.get, context=instance, obj=True)): \n ...\n```\n\nIn this case, the version without ref is much shorter and arguably more pleasant for the eye, but `get` can no longer\nbe renamed without altering the tests.\n",
"bugtrack_url": null,
"license": "Apache-2.0",
"summary": "Allows using unittest.patch() without hardcoding strings.",
"version": "1.0.4",
"project_urls": {
"Homepage": "https://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref",
"Repository": "https://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "e315d0d2abc361f90bcb7c50131f15a2f6f05423b90e70e1853974d7f580930c",
"md5": "1ef746b2ba442d553042f2076c7d0be1",
"sha256": "b5d5dd7e47ebfcf702c79cf2647e9b32695057f1745d49dae32aa96f1adf5115"
},
"downloads": -1,
"filename": "coveo_ref-1.0.4-py3-none-any.whl",
"has_sig": false,
"md5_digest": "1ef746b2ba442d553042f2076c7d0be1",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 10216,
"upload_time": "2024-03-24T12:46:29",
"upload_time_iso_8601": "2024-03-24T12:46:29.808347Z",
"url": "https://files.pythonhosted.org/packages/e3/15/d0d2abc361f90bcb7c50131f15a2f6f05423b90e70e1853974d7f580930c/coveo_ref-1.0.4-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "49e5df697d2fed1a7a20275ffbf153437e8697b85c44bcea456532c63ecf4dea",
"md5": "06b99b20efbee9689079ff9d46bc2e7f",
"sha256": "b822bfb0c309b0f6b9e3b54e6303b90a01f50312a860ddf64322569cf6274bef"
},
"downloads": -1,
"filename": "coveo_ref-1.0.4.tar.gz",
"has_sig": false,
"md5_digest": "06b99b20efbee9689079ff9d46bc2e7f",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 12776,
"upload_time": "2024-03-24T12:46:31",
"upload_time_iso_8601": "2024-03-24T12:46:31.473433Z",
"url": "https://files.pythonhosted.org/packages/49/e5/df697d2fed1a7a20275ffbf153437e8697b85c44bcea456532c63ecf4dea/coveo_ref-1.0.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-03-24 12:46:31",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "coveooss",
"github_project": "coveo-python-oss",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "coveo-ref"
}