|
1 |
| -# uMock (MicroMock) |
| 1 | +# uMock (MicroMock) 🔬🥸 |
2 | 2 |
|
3 | 3 | MicroMock is a very simple and small module for mocking objects in MicroPython
|
4 |
| -with PyScript. |
| 4 | +with PyScript. |
| 5 | + |
| 6 | +It currently only implements relatively naive versions of the following |
| 7 | +features inspired by the `unittest.mock` module in the CPython standard |
| 8 | +library. |
| 9 | + |
| 10 | +* A simplified `Mock` class to replace synchronous objects in Python. |
| 11 | +* A simplified `AsyncMock` class to replace `await`-able objects in Python. |
| 12 | +* A `patch` decorator / context manager to replace a target for the lifetime |
| 13 | + of the decorated function / context manager. |
| 14 | + |
| 15 | +## Usage |
| 16 | + |
| 17 | +**This module is for use with MicroPython within PyScript.** |
| 18 | + |
| 19 | +### Setup |
| 20 | + |
| 21 | +1. Ensure the `umock.py` file is in your Python path. You may need to copy this |
| 22 | + over using the |
| 23 | + [files settings](https://docs.pyscript.net/2024.7.1/user-guide/configuration/#files). |
| 24 | + You should probably ensure use of [uPyTest](https://github.com/ntoll/upytest) |
| 25 | + by copying over the `upytest.py` file into your Python path. |
| 26 | + (See the `config.json` file in this repository for an example of this in |
| 27 | + action.) |
| 28 | +2. Create your tests that use mocks / patching, as described below. |
| 29 | +3. Ensure you have your tests setup properly as per the instruction in the |
| 30 | + [upytest README](https://github.com/ntoll/upytest?tab=readme-ov-file#setup). |
| 31 | +4. In your `index.html` make sure you use the `async` and `terminal` attributes |
| 32 | + referencing your MicroPython script (as in the `index.html` file in this |
| 33 | + repository): |
| 34 | + |
| 35 | + ``` |
| 36 | + <script type="mpy" src="./main.py" config="./config.json" terminal async></script> |
| 37 | + ``` |
| 38 | + |
| 39 | +Now point your browser at the `index.html` and you should see the test suite, |
| 40 | +including your mocks and patches, run. |
| 41 | + |
| 42 | +### Mocking |
| 43 | + |
| 44 | +In code, a mock is simply something that imitates something else. Furthermore, |
| 45 | +mock objects often record their interactions with other aspects of the code so |
| 46 | +you're able to observe and "spy" on the behaviour of your code, and perhaps |
| 47 | +even check expected behaviours are occuring. |
| 48 | + |
| 49 | +Such objects are used in test situations when necessary objects are perhaps |
| 50 | +very complicated to set up, or you only wish to test isolated code in a highly |
| 51 | +constrained context without having to configure a complicated test setting. |
| 52 | + |
| 53 | +For example, you may wish to mock away a database connection so the mock |
| 54 | +emulates a real database connection without the need for an expensive or |
| 55 | +complicated to configure database. All other aspects of the code under test |
| 56 | +remain the same. |
| 57 | + |
| 58 | +However, when using mocks, there is a danger you may mock away the universe and |
| 59 | +the resulting context in which your test code is run doesn't accurately bear |
| 60 | +any resemblance to the real world. |
| 61 | + |
| 62 | +With this context in mind, the `Mock` class provided by uMock is inspired by |
| 63 | +(but not the same as) Python's own unittest.mock.Mock class. |
| 64 | + |
| 65 | +The main differences between this `Mock` class and Python's `unittest.mock.Mock` |
| 66 | +class include: |
| 67 | + |
| 68 | +* Instantiation of the object only allows use of the `spec`, `side_effect` and |
| 69 | + `return_value` keyword arguments (no `name`, `spec_set`, `wraps` or `unsafe` |
| 70 | + arguments). However, arbitrary keyword arguments can be passed to become |
| 71 | + attributes on the resulting mock object. |
| 72 | +* Calls are recorded in a list of tuples in the form `(args, kwargs)` rather |
| 73 | + than a list of special `Call` instance objects. |
| 74 | +* Mock objects do NOT record nor reveal call information relating to thier |
| 75 | + child mock objects (i.e. calls are not propagated to parent mocks). |
| 76 | +* None of the following methods exist in this implementation: |
| 77 | + `mock_add_spec`, `attach_mock`, `configure_mock`, `_get_child_mock`, |
| 78 | + `method_calls`, `mock_calls`. |
| 79 | + |
| 80 | +The `Mock` class takes several optional arguments that specify the behaviour of |
| 81 | +the Mock object: |
| 82 | + |
| 83 | +* `spec`: This can be either a list of strings or an existing object (a |
| 84 | + class or instance) that acts as the specification for the mock |
| 85 | + object. If you pass in an object then a list of strings is formed by |
| 86 | + calling dir on the object (excluding unsupported magic attributes and |
| 87 | + methods). Accessing any attribute not in this list will raise an |
| 88 | + `AttributeError`. |
| 89 | + |
| 90 | + If `spec` is an object (rather than a list of strings) then `__class__` |
| 91 | + returns the class of the `spec` object. This allows mocks to pass |
| 92 | + `isinstance()` tests. |
| 93 | +* `side_effect`: A function to be called whenever the Mock is called. |
| 94 | + Useful for raising exceptions or dynamically changing return values. |
| 95 | + The function is called with the same arguments as the mock, and the |
| 96 | + return value of this function is used as the mock's return value. |
| 97 | + |
| 98 | + Alternatively `side_effect` can be an exception class or instance. In |
| 99 | + this case the exception will be raised when the mock is called. |
| 100 | + |
| 101 | + If `side_effect` is an iterable then each call to the mock will return |
| 102 | + the next value from the iterable. |
| 103 | + |
| 104 | + A `side_effect` can be cleared by setting it to `None`. |
| 105 | +* `return_value`: The value returned when the mock is called. By default |
| 106 | + this is a new Mock (created on first access). |
| 107 | + |
| 108 | +The resulting mock object has the following properties: |
| 109 | + |
| 110 | +* `call_count`: the number of calls made to the mock object. |
| 111 | +* `called`: `True` if the mock object was called at least once. |
| 112 | +* `call_args`: the arguments of the last call to the mock object. |
| 113 | +* `call_args_list`: a list of the arguments of each call to the mock object. |
| 114 | + |
| 115 | +The mock object also has the following methods: |
| 116 | + |
| 117 | +* `reset_mock()`: reset the mock object to a clean state. This is useful for when |
| 118 | + you want to reuse a mock object. |
| 119 | +* `assert_called()`: assert that the mock object was called at least once. |
| 120 | +* `assert_called_once()`: assert that the mock object was called once. |
| 121 | +* `assert_called_with(*args, **kwargs)`: assert that the mock object was last |
| 122 | + called in a particular way. |
| 123 | +* `assert_called_once_with(*args, **kwargs)`: assert that the mock object was |
| 124 | + called once with the given arguments. |
| 125 | +* `assert_any_call(*args, **kwargs)`: assert that the mock object was called at |
| 126 | + least once with the given arguments. |
| 127 | +* `assert_has_calls(calls, any_order=False)`: assert the mock has been called |
| 128 | + with the specified `calls`. If any_order is `False` then the calls must be |
| 129 | + sequential. If any_order is `False`` then the calls can be in any order, but |
| 130 | + they must all appear in mock_calls. |
| 131 | +* `assert_never_called()`: assert that the mock object was never called. |
| 132 | + |
| 133 | +As a result, given a mock object it is possible to call it, have it behave in |
| 134 | +a specified manner, and interrogate it about how it has been used: |
| 135 | + |
| 136 | +```python |
| 137 | +from umock import Mock |
| 138 | + |
| 139 | + |
| 140 | +m = Mock(return_value=42) |
| 141 | + |
| 142 | +meaning_of_life = m() |
| 143 | + |
| 144 | +assert meaning_of_life == 42, "Meaning of life is not H2G2 compliant." |
| 145 | +m.assert_called_once() |
| 146 | +``` |
| 147 | + |
| 148 | +### Patching |
| 149 | + |
| 150 | +The `patch` class acts as a function decorator or a context manager. Inside the |
| 151 | +body of the function or `with` statement, the target is patched with a new |
| 152 | +object. When the function/with statement exits the patch is undone. |
| 153 | + |
| 154 | +The `patch` must always have a target argument that identifies the Python |
| 155 | +object to replace. This string much be of the form: |
| 156 | + |
| 157 | +`"module.submodule:object_name.method_name"` |
| 158 | + |
| 159 | +(Note the colon ":"!) |
| 160 | + |
| 161 | +If no `new` object is provided as the optional second argument, then a new Mock |
| 162 | +object is created with the supplied `kwargs`. |
| 163 | + |
| 164 | +If the `patch` class is being used as a decorator for a function, it will pass |
| 165 | +in the resulting Mock object as the function's argument. |
| 166 | + |
| 167 | +```python |
| 168 | +from umock import patch |
| 169 | + |
| 170 | + |
| 171 | +@patch("tests.a_package.a_module:a_function", return_value=42) |
| 172 | +def test(mock_object): |
| 173 | + from tests.a_package.a_module import a_function |
| 174 | + |
| 175 | + assert mock_object is a_function, "Wrong object patched." |
| 176 | + assert ( |
| 177 | + a_function(1, 2) == 42 |
| 178 | + ), "Wrong return value from patched object." |
| 179 | +``` |
| 180 | + |
| 181 | +Alternatively, if the `patch` class can used as a context manager. |
| 182 | + |
| 183 | +```python |
| 184 | +from umock import patch, Mock |
| 185 | + |
| 186 | + |
| 187 | +mock_function = Mock(return_value=42) |
| 188 | + |
| 189 | +with patch("tests.a_package.a_module:a_function", mock_function) as mock_object: |
| 190 | + assert mock_object is mock_function, "Wrong replacement object." |
| 191 | + from tests.a_package.a_module import a_function |
| 192 | + |
| 193 | + assert ( |
| 194 | + a_function(1, 2) == 42 |
| 195 | + ), "Wrong return value from patched object." |
| 196 | + |
| 197 | +mock_function.assert_called_once_with(1, 2) |
| 198 | +``` |
| 199 | + |
| 200 | +## Developer setup |
| 201 | + |
| 202 | +This is easy: |
| 203 | + |
| 204 | +1. Clone the project. |
| 205 | +2. Start a local web server: `python -m http.server` |
| 206 | +3. Point your browser at http://localhost:8000/ |
| 207 | +4. Change code and refresh your browser to check your changes. |
| 208 | +5. **DO NOT CREATE A NEW FEATURE WITHOUT FIRST CREATING AN ISSUE FOR IT IN WHICH |
| 209 | + YOU PROPOSE YOUR CHANGE**. (We want to avoid a situation where you work hard |
| 210 | + on something that is ultimately rejected by the maintainers.) |
| 211 | +6. Given all the above, pull requests are welcome and greatly appreciated. |
| 212 | + |
| 213 | +We expect all contributors to abide by the spirit of our |
| 214 | +[code of conduct](./CODE_OF_CONDUCT.md). |
| 215 | + |
| 216 | +## Testing uMock |
| 217 | + |
| 218 | +See the content of the `tests` directory in this repository. To run the test |
| 219 | +suite, just follow steps 1, 2 and 3 in the developer setup section. |
| 220 | + |
| 221 | +We use the [uPyTest](https://github.com/ntoll/upytest) to run our test suite. |
| 222 | + |
| 223 | +## License |
| 224 | + |
| 225 | +Copyright (c) 2024 Nicholas H.Tollervey |
| 226 | + |
| 227 | +Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 228 | +this software and associated documentation files (the "Software"), to deal in |
| 229 | +the Software without restriction, including without limitation the rights to |
| 230 | +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
| 231 | +of the Software, and to permit persons to whom the Software is furnished to do |
| 232 | +so, subject to the following conditions: |
| 233 | + |
| 234 | +The above copyright notice and this permission notice shall be included in all |
| 235 | +copies or substantial portions of the Software. |
| 236 | + |
| 237 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 238 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 239 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 240 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 241 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 242 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 243 | +SOFTWARE. |
0 commit comments