|
2 | 2 | Assertions |
3 | 3 | ========== |
4 | 4 |
|
5 | | -.. highlight:: python |
| 5 | +Requests spies |
| 6 | +============== |
6 | 7 |
|
7 | | -Request logging |
8 | | -=============== |
| 8 | +The results of the ``<<`` or ``>>`` operations of type :class:`kmock.Reaction` can be preserved into variables and later used in assertions — a typical "spy" or a "checkpoint" pattern. |
9 | 9 |
|
10 | | -All incoming requests, including their parsed interpretations, are recorded for further assertions in tests and can be accessed either via the mock handler itself or via individual rules (i.e. ``kmock`` or ``kmock << …``). |
| 10 | +Note that every such filter instance keeps its own list of requests served, so a repeated filtering on the same criteria will return a new instance with no requests in its log. You should preserve the original filter or reaction to collect & see the requests. |
11 | 11 |
|
12 | | -The handler and the rules are iterables of requests that have been served by these rules; for the root handler, that ever arrived. This can be used in assertions:: |
| 12 | +Spies can be preserved either as simple filters, or as responses with ``None`` as the payload (read as "no payload"). |
13 | 13 |
|
14 | | - def test_me(kmock): |
15 | | - rule1 = kmock['get /'] << b'hello' |
16 | | - rule2 = kmock['post /'] << b'world' |
| 14 | +The root handler and the inidividual filters/reactions, among other things, are sequences of requests of type :class:`kmock.Request` that have been served by these rules; for the root handler, those that ever arrived — even if responded with HTTP 404 Not Found. This can be used in assertions: |
17 | 15 |
|
18 | | - funcion_under_test() |
| 16 | +.. code-block:: python |
19 | 17 |
|
20 | | - all_requests = list(kmock) |
21 | | - requests1 = list(rule1) |
22 | | - requests2 = list(rule2) |
23 | | - assert len(all_requests) == 3 |
24 | | - assert requests1[0].path == '/' |
25 | | - assert requests2[-1].path == '/' |
| 18 | + import kmock |
26 | 19 |
|
27 | | -The requests are of type :class:`kmock.Request`. |
| 20 | + async def test_requests_assertions(kmock: kmock.RawHandler) -> None: |
| 21 | + # Setup the responses and preserve the spies for later assertions. |
| 22 | + gets = kmock['get'] # just a filter, no response/reaction |
| 23 | + posts = kmock['post'] << b'hello' # a filter with response/reaction |
28 | 24 |
|
29 | | -For convenience, the requests can be compared directly to any value that otherwise could be used in the handler filters (i.e., ``kmock[…]``). In particular, regular strings are automatically recognized as either an HTTP verb or path, or as a Kubernetes action or resource:: |
| 25 | + # Simulate the activity of the system-under-test. |
| 26 | + await kmock.get('/info') |
| 27 | + await kmock.post('/data', data={'key': 'val'}) |
| 28 | + await kmock.delete('/info') |
30 | 29 |
|
31 | | - def test_me(kmock): |
32 | | - rule1 = kmock['get /'] << b'hello' |
33 | | - rule2 = kmock['post /'] << b'world' |
| 30 | + # Check the overall number of requests globally & filtered. |
| 31 | + assert len(kmock) == 3 |
| 32 | + assert len(gets) == 1 |
| 33 | + assert len(posts) == 1 |
34 | 34 |
|
35 | | - funcion_under_test() |
| 35 | + # Check the 1st request globall and the 1st to the get-filter. |
| 36 | + assert gets[0].path == '/info' |
36 | 37 |
|
37 | | - requests1 = list(rule1) |
38 | | - requests2 = list(rule2) |
39 | | - assert requests1[0] == 'get' |
40 | | - assert requests2[-1] == '/' |
| 38 | + # Check the 2nd request globally or the 1st one to the post-filter. |
| 39 | + assert posts[0].path == '/data' |
| 40 | + assert posts[0].data == {'key': 'val'} |
41 | 41 |
|
| 42 | + # Check the 3rd request globally (zero-based index 2). |
| 43 | + assert kmock[2].method == kmock.method.DELETE |
| 44 | + assert kmock[2].path == '/info' |
42 | 45 |
|
43 | | -Server-side errors |
44 | | -================== |
| 46 | +To make a spy which responds with an empty body and stops matching the following filters, explicitly mention ``b""`` as the content, so that it does not go to the following filters at all. |
45 | 47 |
|
46 | | -To catch server-side errors, usually coming from the callbacks of the mock server itself, ``kmock.errors`` (a list of exceptions) can be asserted to be empty. |
| 48 | +.. code-block:: python |
47 | 49 |
|
48 | | -Alternatively, the mock server can be created with the ``strict=True`` option. In this case, the assertion will be performed on the client (test) side when closing the fixture. However, the server shutdown usually happens outside of the test, so explicit checking might be more preferable in some cases. |
| 50 | + import kmock |
49 | 51 |
|
50 | | -The ``StopIteration`` exception is not escalated, it is processed internally to deactivate the response handler as "depleted". |
| 52 | + async def test_spies_with_payloads(kmock: kmock.RawHandler) -> None: |
| 53 | + get1 = kmock['get'] << b'' # this one will intercept all requests |
| 54 | + get2 = kmock['get /'] << b'' # this will never be matched |
51 | 55 |
|
| 56 | + await kmock.get('/') |
52 | 57 |
|
53 | | -Kubernetes objects |
54 | | -================== |
| 58 | + assert len(get1) == 1 |
| 59 | + assert len(get2) == 0 |
| 60 | +
|
| 61 | +
|
| 62 | +Unexpected errors |
| 63 | +================= |
| 64 | + |
| 65 | +Errors list |
| 66 | +----------- |
55 | 67 |
|
56 | | -:class:`KubernetesEmulator` —the in-memory stateful database of objects— exposes the property ``.objects`` to either pre-populate the server's database or to assert it after the test. |
| 68 | +To catch server-side errors usually coming from the callbacks of the mock server itself, :attr:`kmock.RawHandler.errors` (a list of exceptions) can be asserted to be empty. |
57 | 69 |
|
58 | | -Objects are accessed with a 3- or 4-item key: the resource, the namespace, the name of the object, and an optional version index (an integer or a slice). |
| 70 | +.. code-block:: python |
59 | 71 |
|
60 | | -The objects are stored and fetched "as is", i.e. the server does not add or remove any implicit fields, such as the metadata, namespaces, names. These values are taken from the URL and used in the keys only. However, it uses some fields from the payload to retrieve the values of they cannot be figured from the URL — for example, the name of a newly created object. |
| 72 | + import kmock |
61 | 73 |
|
62 | | -To pre-populate the server:: |
| 74 | + async def test_errors_assertions(kmock: kmock.RawHandler) -> None: |
| 75 | + kmock['/'] << ZeroDivisionError('boo!') |
63 | 76 |
|
64 | | - def test_me(kmock): |
65 | | - kmock.objects[resource, 'ns1', 'n1'] = {'spec': 'val1'} |
66 | | - kmock.objects[resource, 'ns1', 'n2'] = {'spec': 'val2'} |
67 | | - function_under_test() |
| 77 | + resp = await kmock.get('/') |
| 78 | + assert resp.status == 500 |
68 | 79 |
|
69 | | -To assert the objects after the test:: |
| 80 | + assert len(kmock.errors) == 1 |
| 81 | + assert str(kmock.errors[0]) == 'boo!' |
| 82 | + assert isinstance(kmock.errors[0], ZeroDivisionError) |
70 | 83 |
|
71 | | - def test_me(kmock): |
72 | | - function_under_test() |
73 | | - assert kmock.objects[resource, 'ns1', 'n1']['spec'] == 'val1' |
74 | | - assert kmock.objects[resource, 'ns1', 'n2']['spec'] == 'val2' |
75 | 84 |
|
76 | | -To access the selected versions of the object as it was manipulated via the API, use either ``.history[idx]``, or the 4th item in the main key. The usual magic with negative indexes or slices works:: |
| 85 | +Strict mode |
| 86 | +----------- |
77 | 87 |
|
78 | | - def test_me(kmock): |
79 | | - function_under_test() |
| 88 | +Alternatively, the mock server can be created with the :attr:`kmock.RawHandler.strict` set t ``True`` option. In this case, the assertion will be performed on the client (test) side when closing the fixture. However, the server shutdown usually happens outside of the test, so explicit checking might be more preferable in some cases. |
80 | 89 |
|
81 | | - assert kmock.objects[resource, 'ns1', 'n1', 0]['spec'] == 'val1' |
82 | | - assert kmock.objects[resource, 'ns1', 'n2'].history[-1]['spec'] == 'val2' |
| 90 | +in this example, the test will fail: while the main test body will succeed as all expectations are met, the test's teardown will raise an exception ``ZeroDivisionError`` because it has happened in the request and was remembered (assume it is some unexpected error, despite simulated intentionally for the sake of the example): |
83 | 91 |
|
84 | | - assert kmock.objects[resource, 'ns1', 'n1', :] == [{'spec': 'val1'}] |
85 | | - assert kmock.objects[resource, 'ns1', 'n2'].history[:] == [{'spec': 'val2'}] |
| 92 | +.. code-block:: python |
86 | 93 |
|
87 | | -To avoid the dependency on very specific payloads generated by some realistic API clients, use the partial dict matching instead of the precise equality — with set-like ``<=`` and ``>=`` operations on objects:: |
| 94 | + import kmock |
88 | 95 |
|
89 | | - def test_me(kmock): |
90 | | - function_under_test() |
91 | | - assert kmock.objects[resource, 'ns1', 'n1', 0] >= |
92 | | - assert {'spec': 'val2'} <= kmock.objects[resource, 'ns1', 'n2', -1] |
| 96 | + @pytest.mark.kmock(strict=True) |
| 97 | + async def test_errors_assertions(kmock: kmock.RawHandler) -> None: |
| 98 | + kmock['/'] << ZeroDivisionError('boo!') |
93 | 99 |
|
94 | | -Mind that the object should always be the "greater" operand, the pattern as the "lesser" operand. Usually, the object contains more fields than required by the pattern. But all fields present in the pattern MUST match. In other words, a pattern must be a sub-dict of the object. |
| 100 | + resp = await kmock.get('/') |
| 101 | + assert resp.status == 500 |
| 102 | +
|
| 103 | +.. note:: |
| 104 | + |
| 105 | + The :class:`StopIteration` exception is not escalated, it is processed internally to deactivate the response handler as "depleted". |
| 106 | + |
| 107 | + |
| 108 | +Kubernetes objects |
| 109 | +================== |
95 | 110 |
|
96 | | -Nested sub-dicts are also matched partially, recursively — either by selecting them by the key, or by adding them into the pattern. |
| 111 | +:class:`KubernetesEmulator` —the in-memory stateful database of objects— exposes the property :attr:`kmock.KubernetesEmulator.objects` to either pre-populate the server's database or to assert it after the test which uses the API. |
97 | 112 |
|
98 | | -Beware: accessing objects and sub-dicts always returns a dict-like wrapper instead of the originally stored dict. This includes iterating over object's fields, and converting them to ``dict()`` directly. To unwrap, use ``.raw``: e.g. ``obj.raw`` or ``obj['metadata'].raw``. |
| 113 | +It is explained in detail in :doc:`/kubernetes/assertions` (and, to the extent needed, in :doc:`/kubernetes/persistence`). |
0 commit comments