Skip to content

Commit

Permalink
Merge pull request #175 from coveooss/coveo-ref/documentation-update
Browse files Browse the repository at this point in the history
improve docs
  • Loading branch information
jonapich authored Oct 31, 2023
2 parents 6103b1b + 7d82e61 commit 31aca13
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 24 deletions.
77 changes: 55 additions & 22 deletions coveo-ref/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
# coveo-ref

Refactorable mock targets

## Demo
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:

Expand Down Expand Up @@ -143,10 +159,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.

Expand All @@ -166,7 +181,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:


Expand Down Expand Up @@ -195,7 +210,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.
Expand Down Expand Up @@ -238,12 +253,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`:

Expand Down Expand Up @@ -288,9 +321,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:

Expand All @@ -311,7 +344,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:
Expand All @@ -322,7 +355,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()`:

Expand All @@ -331,7 +364,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)
Expand Down Expand Up @@ -364,7 +397,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:

Expand Down Expand Up @@ -392,7 +425,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.

Expand Down
28 changes: 26 additions & 2 deletions coveo-ref/coveo_ref/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
patch(*ref(cls.property, new_callable=PropertyMock, return_value=...))
Mock a method (no need for a context):
patch(*ref(cls.method))
Mock a method on a single instance:
patch.object(*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:
patch(*ref(cls.staticorclassmethod))
To patch on a single instance:
patch.object(*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).
Expand Down

0 comments on commit 31aca13

Please sign in to comment.