robotframework-mock 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {robotframework_mock-0.7.0/src/robotframework_mock.egg-info → robotframework_mock-0.8.0}/PKG-INFO +32 -2
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/README.md +31 -1
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/setup.cfg +1 -1
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/MockCoverage/__init__.py +7 -1
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/MockResource/__init__.py +114 -41
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/_mock_core/__init__.py +65 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0/src/robotframework_mock.egg-info}/PKG-INFO +32 -2
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/LICENSE +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/MANIFEST.in +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/pyproject.toml +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/setup.py +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/MockLibrary/__init__.py +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/robotframework_mock.egg-info/SOURCES.txt +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/robotframework_mock.egg-info/dependency_links.txt +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/robotframework_mock.egg-info/requires.txt +0 -0
- {robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/robotframework_mock.egg-info/top_level.txt +0 -0
{robotframework_mock-0.7.0/src/robotframework_mock.egg-info → robotframework_mock-0.8.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-mock
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Robot Framework library for mocking keywords in unit tests
|
|
5
5
|
Home-page: https://github.com/olesz/robotframework-mock
|
|
6
6
|
Author: Lajos Olah
|
|
@@ -444,9 +444,39 @@ MockLibrary dynamically replaces keyword implementations:
|
|
|
444
444
|
3. Stores original methods before mocking
|
|
445
445
|
4. Replaces methods with mock implementations using Python's unittest.mock.Mock
|
|
446
446
|
5. Returns mocked values or executes side effects
|
|
447
|
-
6. Tracks call counts for verification
|
|
447
|
+
6. Tracks call counts and call arguments for verification
|
|
448
448
|
7. Raises AttributeError if attempting to mock a non-existent keyword
|
|
449
449
|
|
|
450
|
+
#### Mocks are installed on the library class
|
|
451
|
+
|
|
452
|
+
Step 4 replaces the method on the target library's **class**, not on the
|
|
453
|
+
instance. Two consequences are worth knowing, because neither is reported as an
|
|
454
|
+
error:
|
|
455
|
+
|
|
456
|
+
**A mock stays active until `Reset Mocks`, across tests and suites.** The mock
|
|
457
|
+
is not scoped to the test that created it. A test that mocks a keyword and
|
|
458
|
+
leaves without resetting hands the mock to everything that follows. Reset in a
|
|
459
|
+
teardown rather than at the end of a test body, so the mock is also removed when
|
|
460
|
+
the test fails midway.
|
|
461
|
+
|
|
462
|
+
**Two `MockLibrary` instances wrapping the same library share one mock.** Last
|
|
463
|
+
registration wins, and resetting *either* instance restores the real method:
|
|
464
|
+
|
|
465
|
+
```robot
|
|
466
|
+
Library MockLibrary DateTime WITH NAME MockA
|
|
467
|
+
Library MockLibrary DateTime WITH NAME MockB
|
|
468
|
+
...
|
|
469
|
+
MockA.Mock Keyword Convert Time return_value=from-a
|
|
470
|
+
MockB.Mock Keyword Convert Time return_value=from-b
|
|
471
|
+
${result}= Convert Time 12:00:00 # from-b: MockA's mock was overwritten
|
|
472
|
+
MockB.Reset Mocks
|
|
473
|
+
${result}= Convert Time 12:00:00 # the real keyword, although MockA never reset
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Use a single alias per library, and separate aliases only for *different*
|
|
477
|
+
libraries. `MockResource` is not affected: it keys mocks per resource file, so
|
|
478
|
+
instances for different resources are independent.
|
|
479
|
+
|
|
450
480
|
### MockResource
|
|
451
481
|
|
|
452
482
|
MockResource patches Robot Framework's keyword execution:
|
|
@@ -420,9 +420,39 @@ MockLibrary dynamically replaces keyword implementations:
|
|
|
420
420
|
3. Stores original methods before mocking
|
|
421
421
|
4. Replaces methods with mock implementations using Python's unittest.mock.Mock
|
|
422
422
|
5. Returns mocked values or executes side effects
|
|
423
|
-
6. Tracks call counts for verification
|
|
423
|
+
6. Tracks call counts and call arguments for verification
|
|
424
424
|
7. Raises AttributeError if attempting to mock a non-existent keyword
|
|
425
425
|
|
|
426
|
+
#### Mocks are installed on the library class
|
|
427
|
+
|
|
428
|
+
Step 4 replaces the method on the target library's **class**, not on the
|
|
429
|
+
instance. Two consequences are worth knowing, because neither is reported as an
|
|
430
|
+
error:
|
|
431
|
+
|
|
432
|
+
**A mock stays active until `Reset Mocks`, across tests and suites.** The mock
|
|
433
|
+
is not scoped to the test that created it. A test that mocks a keyword and
|
|
434
|
+
leaves without resetting hands the mock to everything that follows. Reset in a
|
|
435
|
+
teardown rather than at the end of a test body, so the mock is also removed when
|
|
436
|
+
the test fails midway.
|
|
437
|
+
|
|
438
|
+
**Two `MockLibrary` instances wrapping the same library share one mock.** Last
|
|
439
|
+
registration wins, and resetting *either* instance restores the real method:
|
|
440
|
+
|
|
441
|
+
```robot
|
|
442
|
+
Library MockLibrary DateTime WITH NAME MockA
|
|
443
|
+
Library MockLibrary DateTime WITH NAME MockB
|
|
444
|
+
...
|
|
445
|
+
MockA.Mock Keyword Convert Time return_value=from-a
|
|
446
|
+
MockB.Mock Keyword Convert Time return_value=from-b
|
|
447
|
+
${result}= Convert Time 12:00:00 # from-b: MockA's mock was overwritten
|
|
448
|
+
MockB.Reset Mocks
|
|
449
|
+
${result}= Convert Time 12:00:00 # the real keyword, although MockA never reset
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Use a single alias per library, and separate aliases only for *different*
|
|
453
|
+
libraries. `MockResource` is not affected: it keys mocks per resource file, so
|
|
454
|
+
instances for different resources are independent.
|
|
455
|
+
|
|
426
456
|
### MockResource
|
|
427
457
|
|
|
428
458
|
MockResource patches Robot Framework's keyword execution:
|
|
@@ -60,6 +60,8 @@ except ModuleNotFoundError: # pragma: no cover - exercised only on <3.11
|
|
|
60
60
|
from robot.api import get_resource_model
|
|
61
61
|
from robot.api.parsing import ModelVisitor
|
|
62
62
|
|
|
63
|
+
from _mock_core import is_keyword_mocked
|
|
64
|
+
|
|
63
65
|
|
|
64
66
|
DEFAULT_CONFIG_PATH = "mock-coverage.toml"
|
|
65
67
|
DEFAULT_OUTPUT_PATH = "coverage.json"
|
|
@@ -571,6 +573,10 @@ class ExecutionTracker:
|
|
|
571
573
|
def record(self, owner: Optional[str], name: str) -> None:
|
|
572
574
|
"""Record a keyword execution if it belongs to a measured resource.
|
|
573
575
|
|
|
576
|
+
Keywords whose body is currently replaced by a ``MockResource`` mock are
|
|
577
|
+
ignored: the real body never ran, so counting them would credit the
|
|
578
|
+
resource with coverage it did not earn.
|
|
579
|
+
|
|
574
580
|
Args:
|
|
575
581
|
owner: Robot's owner name for the keyword (resource base name).
|
|
576
582
|
name: The executed keyword's name.
|
|
@@ -579,7 +585,7 @@ class ExecutionTracker:
|
|
|
579
585
|
if owner not in self._owner_names:
|
|
580
586
|
return
|
|
581
587
|
source = self._resolve_source(owner, name)
|
|
582
|
-
if source:
|
|
588
|
+
if source and not is_keyword_mocked(source, name):
|
|
583
589
|
self._executed.setdefault(source, set()).add(name)
|
|
584
590
|
|
|
585
591
|
def _resolve_source(self, owner: str, name: str) -> Optional[str]:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Mock resource for Robot Framework keyword mocking in unit tests."""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
+
import weakref
|
|
3
4
|
from typing import Any, Callable
|
|
4
5
|
from unittest.mock import Mock
|
|
5
6
|
|
|
@@ -9,7 +10,12 @@ from robot.running import Return
|
|
|
9
10
|
from robot.running.namespace import Namespace
|
|
10
11
|
from robot.utils import split_from_equals
|
|
11
12
|
|
|
12
|
-
from _mock_core import
|
|
13
|
+
from _mock_core import (
|
|
14
|
+
CallInspectionMixin,
|
|
15
|
+
normalize_keyword_name,
|
|
16
|
+
register_mocked_keyword,
|
|
17
|
+
unregister_mocked_keyword,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
# Robot resolves the values of a RETURN statement as variable expressions, so a
|
|
15
21
|
# mocked return value cannot be embedded in one directly - anything that is not
|
|
@@ -18,6 +24,74 @@ from _mock_core import CallInspectionMixin
|
|
|
18
24
|
# object (a query result set, a parsed JSON body, a list of pods...).
|
|
19
25
|
RETURN_VARIABLE = '${__mock_resource_return__}'
|
|
20
26
|
|
|
27
|
+
# Namespace.get_runner is patched once per process rather than once per
|
|
28
|
+
# MockResource instance. Patching per instance chained a new wrapper around the
|
|
29
|
+
# previous one for every library import, and nothing ever restored them, so
|
|
30
|
+
# every keyword lookup in the run paid for each import. The single wrapper
|
|
31
|
+
# consults the live instances below and is inert when none of them has mocks.
|
|
32
|
+
_INSTANCES = []
|
|
33
|
+
_ORIGINAL_GET_RUNNER = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _live_instances():
|
|
37
|
+
"""Return the MockResource instances that are still alive.
|
|
38
|
+
|
|
39
|
+
Instances are held weakly so a library instance going out of scope does not
|
|
40
|
+
keep its mocks alive, and dead references are pruned on access.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The live instances, in the order they were created.
|
|
44
|
+
"""
|
|
45
|
+
live = []
|
|
46
|
+
for reference in list(_INSTANCES):
|
|
47
|
+
instance = reference()
|
|
48
|
+
if instance is None:
|
|
49
|
+
_INSTANCES.remove(reference)
|
|
50
|
+
else:
|
|
51
|
+
live.append(instance)
|
|
52
|
+
return live
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _install_shared_patch():
|
|
56
|
+
"""Patch ``Namespace.get_runner`` once, routing to all live instances."""
|
|
57
|
+
global _ORIGINAL_GET_RUNNER # pylint: disable=global-statement
|
|
58
|
+
if _ORIGINAL_GET_RUNNER is not None:
|
|
59
|
+
return
|
|
60
|
+
_ORIGINAL_GET_RUNNER = Namespace.get_runner
|
|
61
|
+
original_get_runner = _ORIGINAL_GET_RUNNER
|
|
62
|
+
|
|
63
|
+
def patched_get_runner(self, keyword_name, recommend_on_failure):
|
|
64
|
+
keyword_runner = original_get_runner(self, keyword_name, recommend_on_failure)
|
|
65
|
+
resource_file = getattr(keyword_runner.keyword, "source", None)
|
|
66
|
+
|
|
67
|
+
for instance in _live_instances():
|
|
68
|
+
if instance.source not in str(resource_file):
|
|
69
|
+
continue
|
|
70
|
+
mock = instance.mocks.get(normalize_keyword_name(keyword_name))
|
|
71
|
+
if not mock:
|
|
72
|
+
continue
|
|
73
|
+
original_run = keyword_runner.run
|
|
74
|
+
|
|
75
|
+
def patched_run(data, result, context, run,
|
|
76
|
+
_mock=mock, _original_run=original_run):
|
|
77
|
+
args, kwargs = _resolve_arguments(
|
|
78
|
+
keyword_runner.keyword, data, context
|
|
79
|
+
)
|
|
80
|
+
mock_result = _mock(*args, **kwargs)
|
|
81
|
+
# Test scope, so the value is visible to the keyword's own
|
|
82
|
+
# body even when the mocked keyword is called from inside
|
|
83
|
+
# another keyword rather than directly from the test.
|
|
84
|
+
context.variables.set_test(RETURN_VARIABLE, mock_result)
|
|
85
|
+
keyword_runner.keyword.body._items = [Return(values=[RETURN_VARIABLE])] # pylint: disable=protected-access
|
|
86
|
+
return _original_run(data, result, context, run)
|
|
87
|
+
|
|
88
|
+
keyword_runner.run = patched_run
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
return keyword_runner
|
|
92
|
+
|
|
93
|
+
Namespace.get_runner = patched_get_runner
|
|
94
|
+
|
|
21
95
|
|
|
22
96
|
def _resolve_arguments(keyword_obj, data, context):
|
|
23
97
|
"""Split a keyword call's raw arguments into resolved args and kwargs.
|
|
@@ -72,44 +146,37 @@ class MockResource(CallInspectionMixin):
|
|
|
72
146
|
|
|
73
147
|
def __init__(self, source):
|
|
74
148
|
self._source = source
|
|
75
|
-
self._original_get_runner = Namespace.get_runner
|
|
76
149
|
self._original_items = {}
|
|
77
150
|
self._original_setups = {}
|
|
78
151
|
self._original_teardowns = {}
|
|
152
|
+
self._mocked_sources = {}
|
|
79
153
|
self._mocks = {}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
keyword_runner.keyword.body._items = [Return(values=[RETURN_VARIABLE])] # pylint: disable=protected-access
|
|
107
|
-
return original_run(data, result, context, run)
|
|
108
|
-
keyword_runner.run = patched_run
|
|
109
|
-
|
|
110
|
-
return keyword_runner
|
|
111
|
-
|
|
112
|
-
Namespace.get_runner = patched_get_runner
|
|
154
|
+
_INSTANCES.append(weakref.ref(self))
|
|
155
|
+
_install_shared_patch()
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def source(self):
|
|
159
|
+
"""Return the resource file this instance mocks keywords from."""
|
|
160
|
+
return self._source
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def mocks(self):
|
|
164
|
+
"""Return the active mocks, keyed by normalized keyword name."""
|
|
165
|
+
return self._mocks
|
|
166
|
+
|
|
167
|
+
def _mock_key(self, keyword_name: str) -> str:
|
|
168
|
+
"""Return the ``self._mocks`` key for *keyword_name*.
|
|
169
|
+
|
|
170
|
+
Mocks are keyed the way Robot Framework matches keyword names, so
|
|
171
|
+
inspecting a call works whatever case or spacing the test used.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
keyword_name: Keyword name as written in the test.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The normalized name used as the mock key.
|
|
178
|
+
"""
|
|
179
|
+
return normalize_keyword_name(keyword_name)
|
|
113
180
|
|
|
114
181
|
@keyword
|
|
115
182
|
def mock_keyword(
|
|
@@ -150,9 +217,12 @@ class MockResource(CallInspectionMixin):
|
|
|
150
217
|
resource_file = getattr(keyword_runner.keyword, "source", None)
|
|
151
218
|
|
|
152
219
|
if self._source not in str(resource_file):
|
|
153
|
-
raise AttributeError(
|
|
220
|
+
raise AttributeError(
|
|
221
|
+
f"Keyword '{keyword_name}' not found in {self._source}"
|
|
222
|
+
)
|
|
154
223
|
|
|
155
224
|
self._original_items[keyword_name] = keyword_runner.keyword.body._items # pylint: disable=protected-access
|
|
225
|
+
self._mocked_sources[keyword_name] = resource_file
|
|
156
226
|
if skip_setup:
|
|
157
227
|
self._original_setups[keyword_name] = keyword_runner.keyword.setup
|
|
158
228
|
keyword_runner.keyword.setup = None
|
|
@@ -160,7 +230,10 @@ class MockResource(CallInspectionMixin):
|
|
|
160
230
|
self._original_teardowns[keyword_name] = keyword_runner.keyword.teardown
|
|
161
231
|
keyword_runner.keyword.teardown = None
|
|
162
232
|
mock = Mock(return_value=return_value, side_effect=side_effect)
|
|
163
|
-
self._mocks[keyword_name] = mock
|
|
233
|
+
self._mocks[normalize_keyword_name(keyword_name)] = mock
|
|
234
|
+
# Tell MockCoverage this keyword's body no longer runs, so it is not
|
|
235
|
+
# credited with coverage it did not earn.
|
|
236
|
+
register_mocked_keyword(resource_file, keyword_name)
|
|
164
237
|
return mock
|
|
165
238
|
|
|
166
239
|
@keyword
|
|
@@ -174,6 +247,8 @@ class MockResource(CallInspectionMixin):
|
|
|
174
247
|
| MockRes.Reset Mocks |
|
|
175
248
|
"""
|
|
176
249
|
self._mocks.clear()
|
|
250
|
+
for keyword_name, source in self._mocked_sources.items():
|
|
251
|
+
unregister_mocked_keyword(source, keyword_name)
|
|
177
252
|
for keyword_name, items in self._original_items.items():
|
|
178
253
|
keyword_runner = BuiltIn()._namespace.get_runner(keyword_name, True) # pylint: disable=protected-access
|
|
179
254
|
keyword_runner.keyword.body._items = items # pylint: disable=protected-access
|
|
@@ -186,6 +261,7 @@ class MockResource(CallInspectionMixin):
|
|
|
186
261
|
self._original_items.clear()
|
|
187
262
|
self._original_setups.clear()
|
|
188
263
|
self._original_teardowns.clear()
|
|
264
|
+
self._mocked_sources.clear()
|
|
189
265
|
|
|
190
266
|
@keyword
|
|
191
267
|
def verify_keyword_called(self, keyword_name: str, times: int = None):
|
|
@@ -201,9 +277,6 @@ class MockResource(CallInspectionMixin):
|
|
|
201
277
|
Example:
|
|
202
278
|
| MockDB.Verify Keyword Called | Execute Sql | times=1 |
|
|
203
279
|
"""
|
|
204
|
-
|
|
205
|
-
raise AssertionError(f"Keyword '{keyword_name}' was not mocked")
|
|
206
|
-
|
|
207
|
-
mock = self._mocks[keyword_name]
|
|
280
|
+
mock = self._get_mock(keyword_name)
|
|
208
281
|
if times is not None and mock.call_count != times:
|
|
209
282
|
raise AssertionError(f"Expected {times} calls, got {mock.call_count}")
|
|
@@ -8,10 +8,75 @@ implemented once here and mixed into both libraries, so the two cannot drift
|
|
|
8
8
|
apart.
|
|
9
9
|
"""
|
|
10
10
|
# pylint: disable=invalid-name
|
|
11
|
+
import os
|
|
11
12
|
from typing import Any, Dict, List
|
|
12
13
|
from unittest.mock import call
|
|
13
14
|
|
|
14
15
|
from robot.api.deco import keyword
|
|
16
|
+
from robot.utils import normalize
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_keyword_name(name) -> str:
|
|
20
|
+
"""Return a keyword name in the form Robot Framework matches on.
|
|
21
|
+
|
|
22
|
+
Robot treats keyword names as case-, space- and underscore-insensitive, so
|
|
23
|
+
mocks have to be keyed the same way. Without this, mocking ``Fetch Rows``
|
|
24
|
+
and calling ``fetch rows`` would silently run the real keyword.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Keyword name as written anywhere.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The canonical comparison key for that name.
|
|
31
|
+
"""
|
|
32
|
+
return normalize(str(name), ignore='_')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Keywords whose body is currently replaced by a mock, as
|
|
36
|
+
# ``(absolute source path, normalized keyword name)`` pairs.
|
|
37
|
+
#
|
|
38
|
+
# MockCoverage consults this so a keyword that was only ever mocked is not
|
|
39
|
+
# counted as covered: its real body never ran, and counting it would inflate the
|
|
40
|
+
# reported coverage of the resource it lives in.
|
|
41
|
+
MOCKED_KEYWORDS = set()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def mocked_keyword_key(source, keyword_name) -> tuple:
|
|
45
|
+
"""Build the registry key for a keyword in a resource file.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
source: Path of the resource file defining the keyword.
|
|
49
|
+
keyword_name: Keyword name as written.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The ``(source key, normalized name)`` tuple used by the registry.
|
|
53
|
+
"""
|
|
54
|
+
return (os.path.normcase(os.path.abspath(str(source))),
|
|
55
|
+
normalize_keyword_name(keyword_name))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_mocked_keyword(source, keyword_name) -> None:
|
|
59
|
+
"""Record that a keyword's body is replaced by a mock."""
|
|
60
|
+
MOCKED_KEYWORDS.add(mocked_keyword_key(source, keyword_name))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def unregister_mocked_keyword(source, keyword_name) -> None:
|
|
64
|
+
"""Record that a keyword's body is restored."""
|
|
65
|
+
MOCKED_KEYWORDS.discard(mocked_keyword_key(source, keyword_name))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_keyword_mocked(source_key: str, keyword_name) -> bool:
|
|
69
|
+
"""Return whether a keyword is currently mocked.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
source_key: Source path already normalised with ``os.path.normcase`` and
|
|
73
|
+
``os.path.abspath``, as :func:`mocked_keyword_key` produces.
|
|
74
|
+
keyword_name: Keyword name as reported at execution time.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
``True`` when the keyword's body is currently a mock.
|
|
78
|
+
"""
|
|
79
|
+
return (source_key, normalize_keyword_name(keyword_name)) in MOCKED_KEYWORDS
|
|
15
80
|
|
|
16
81
|
|
|
17
82
|
class CallInspectionMixin:
|
{robotframework_mock-0.7.0 → robotframework_mock-0.8.0/src/robotframework_mock.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-mock
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Robot Framework library for mocking keywords in unit tests
|
|
5
5
|
Home-page: https://github.com/olesz/robotframework-mock
|
|
6
6
|
Author: Lajos Olah
|
|
@@ -444,9 +444,39 @@ MockLibrary dynamically replaces keyword implementations:
|
|
|
444
444
|
3. Stores original methods before mocking
|
|
445
445
|
4. Replaces methods with mock implementations using Python's unittest.mock.Mock
|
|
446
446
|
5. Returns mocked values or executes side effects
|
|
447
|
-
6. Tracks call counts for verification
|
|
447
|
+
6. Tracks call counts and call arguments for verification
|
|
448
448
|
7. Raises AttributeError if attempting to mock a non-existent keyword
|
|
449
449
|
|
|
450
|
+
#### Mocks are installed on the library class
|
|
451
|
+
|
|
452
|
+
Step 4 replaces the method on the target library's **class**, not on the
|
|
453
|
+
instance. Two consequences are worth knowing, because neither is reported as an
|
|
454
|
+
error:
|
|
455
|
+
|
|
456
|
+
**A mock stays active until `Reset Mocks`, across tests and suites.** The mock
|
|
457
|
+
is not scoped to the test that created it. A test that mocks a keyword and
|
|
458
|
+
leaves without resetting hands the mock to everything that follows. Reset in a
|
|
459
|
+
teardown rather than at the end of a test body, so the mock is also removed when
|
|
460
|
+
the test fails midway.
|
|
461
|
+
|
|
462
|
+
**Two `MockLibrary` instances wrapping the same library share one mock.** Last
|
|
463
|
+
registration wins, and resetting *either* instance restores the real method:
|
|
464
|
+
|
|
465
|
+
```robot
|
|
466
|
+
Library MockLibrary DateTime WITH NAME MockA
|
|
467
|
+
Library MockLibrary DateTime WITH NAME MockB
|
|
468
|
+
...
|
|
469
|
+
MockA.Mock Keyword Convert Time return_value=from-a
|
|
470
|
+
MockB.Mock Keyword Convert Time return_value=from-b
|
|
471
|
+
${result}= Convert Time 12:00:00 # from-b: MockA's mock was overwritten
|
|
472
|
+
MockB.Reset Mocks
|
|
473
|
+
${result}= Convert Time 12:00:00 # the real keyword, although MockA never reset
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Use a single alias per library, and separate aliases only for *different*
|
|
477
|
+
libraries. `MockResource` is not affected: it keys mocks per resource file, so
|
|
478
|
+
instances for different resources are independent.
|
|
479
|
+
|
|
450
480
|
### MockResource
|
|
451
481
|
|
|
452
482
|
MockResource patches Robot Framework's keyword execution:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{robotframework_mock-0.7.0 → robotframework_mock-0.8.0}/src/robotframework_mock.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|