Skip to content

Commit 73f6599

Browse files
JP-Ellisvalkolovos
andcommitted
chore(examples): add v3 message consumer examples
This change also includes a minor fix to ensure that the server is fully started before the test proceeds. Co-authored-by: valkolovos <[email protected]> Co-authored-by: JP-Ellis <[email protected]> Signed-off-by: JP-Ellis <[email protected]>
1 parent 0e4a174 commit 73f6599

7 files changed

+182
-1
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
src/pact/bin
55
src/pact/data
66

7+
# Test outputs
8+
examples/tests/pacts
9+
710
# Version is determined from the VCS
811
src/pact/__version__.py
912

examples/docker-compose.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ services:
1515
broker:
1616
image: pactfoundation/pact-broker:latest-multi
1717
depends_on:
18-
- postgres
18+
postgres:
19+
condition: service_healthy
1920
ports:
2021
- "9292:9292"
2122
restart: always
@@ -41,3 +42,4 @@ services:
4142
interval: 1s
4243
timeout: 2s
4344
retries: 5
45+
start_period: 30s

examples/tests/test_01_provider_fastapi.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from __future__ import annotations
2626

27+
import time
2728
from multiprocessing import Process
2829
from typing import Any, Dict, Generator, Union
2930
from unittest.mock import MagicMock
@@ -93,6 +94,7 @@ def verifier() -> Generator[Verifier, Any, None]:
9394
provider_base_url=str(PROVIDER_URL),
9495
)
9596
proc.start()
97+
time.sleep(2)
9698
yield verifier
9799
proc.kill()
98100

examples/tests/test_01_provider_flask.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from __future__ import annotations
2626

27+
import time
2728
from multiprocessing import Process
2829
from typing import Any, Dict, Generator, Union
2930
from unittest.mock import MagicMock
@@ -81,6 +82,7 @@ def verifier() -> Generator[Verifier, Any, None]:
8182
provider_base_url=str(PROVIDER_URL),
8283
)
8384
proc.start()
85+
time.sleep(2)
8486
yield verifier
8587
proc.kill()
8688

examples/tests/v3/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""
2+
Consumer test of example message handler using the v3 API.
3+
4+
This test will create a pact between the message handler
5+
and the message provider.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import json
11+
import logging
12+
from pathlib import Path
13+
from typing import (
14+
TYPE_CHECKING,
15+
Any,
16+
Dict,
17+
Generator,
18+
)
19+
from unittest.mock import MagicMock
20+
21+
import pytest
22+
23+
from examples.src.message import Handler
24+
from pact.v3.pact import Pact
25+
26+
if TYPE_CHECKING:
27+
from collections.abc import Callable
28+
29+
30+
log = logging.getLogger(__name__)
31+
32+
33+
@pytest.fixture(scope="module")
34+
def pact() -> Generator[Pact, None, None]:
35+
"""
36+
Set up Message Pact Consumer.
37+
38+
This fixtures sets up the Message Pact consumer and the pact it has with a
39+
provider. The consumer defines the expected messages it will receive from
40+
the provider, and the Python test suite verifies that the correct actions
41+
are taken.
42+
43+
The verify method takes a function as an argument. This function
44+
will be called with one or two arguments - the value of `with_body` and
45+
the contents of `with_metadata` if provided.
46+
47+
If the function under test does not take those parameters, you can create
48+
a wrapper function to convert the pact parameters into the values
49+
expected by your function.
50+
51+
52+
For each interaction, the consumer defines the following:
53+
54+
```python
55+
(
56+
pact = Pact("consumer name", "provider name")
57+
processed_messages: list[MessagePact.MessagePactResult] = pact \
58+
.with_specification("V3")
59+
.upon_receiving("a request", "Async") \
60+
.given("a request to write test.txt") \
61+
.with_body(msg) \
62+
.with_metadata({"Content-Type": "application/json"})
63+
.verify(pact_handler)
64+
)
65+
66+
```
67+
"""
68+
pact_dir = Path(Path(__file__).parent.parent / "pacts")
69+
pact = Pact("v3_message_consumer", "v3_message_provider")
70+
log.info("Creating Message Pact with V3 specification")
71+
yield pact.with_specification("V3")
72+
pact.write_file(pact_dir, overwrite=True)
73+
74+
75+
@pytest.fixture()
76+
def handler() -> Handler:
77+
"""
78+
Fixture for the Handler.
79+
80+
This fixture mocks the filesystem calls in the handler, so that we can
81+
verify that the handler is calling the filesystem correctly.
82+
"""
83+
handler = Handler()
84+
handler.fs = MagicMock()
85+
handler.fs.write.return_value = None
86+
handler.fs.read.return_value = "Hello world!"
87+
return handler
88+
89+
90+
@pytest.fixture()
91+
def verifier(
92+
handler: Handler,
93+
) -> Generator[Callable[[str | bytes | None, Dict[str, Any]], None], Any, None]:
94+
"""
95+
Verifier function for the Pact.
96+
97+
This function is passed to the `verify` method of the Pact object. It is
98+
responsible for taking in the messages (along with the context/metadata)
99+
and ensuring that the consumer is able to process the message correctly.
100+
101+
In our case, we deserialize the message and pass it to the (pre-mocked)
102+
handler for processing. We then verify that the underlying filesystem
103+
calls were made as expected.
104+
"""
105+
assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked"
106+
107+
def _verifier(msg: str | bytes | None, context: Dict[str, Any]) -> None:
108+
assert msg is not None, "Message is None"
109+
data = json.loads(msg)
110+
log.info(
111+
"Processing message: ",
112+
extra={"input": msg, "processed_message": data, "context": context},
113+
)
114+
handler.process(data)
115+
116+
yield _verifier
117+
118+
assert handler.fs.mock_calls, "Handler did not call the filesystem"
119+
120+
121+
def test_async_message_handler_write(
122+
pact: Pact,
123+
handler: Handler,
124+
verifier: Callable[[str | bytes | None, Dict[str, Any]], None],
125+
) -> None:
126+
"""
127+
Create a pact between the message handler and the message provider.
128+
"""
129+
assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked"
130+
131+
(
132+
pact.upon_receiving("a write request", "Async")
133+
.given("a request to write test.txt")
134+
.with_body(
135+
json.dumps({
136+
"action": "WRITE",
137+
"path": "my_file.txt",
138+
"contents": "Hello, world!",
139+
})
140+
)
141+
)
142+
pact.verify(verifier, "Async")
143+
144+
handler.fs.write.assert_called_once_with("my_file.txt", "Hello, world!")
145+
146+
147+
def test_async_message_handler_read(
148+
pact: Pact,
149+
handler: Handler,
150+
verifier: Callable[[str | bytes | None, Dict[str, Any]], None],
151+
) -> None:
152+
"""
153+
Create a pact between the message handler and the message provider.
154+
"""
155+
assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked"
156+
157+
(
158+
pact.upon_receiving("a read request", "Async")
159+
.given("a request to read test.txt")
160+
.with_body(
161+
json.dumps({
162+
"action": "READ",
163+
"path": "my_file.txt",
164+
"contents": "Hello, world!",
165+
})
166+
)
167+
)
168+
pact.verify(verifier, "Async")
169+
170+
handler.fs.read.assert_called_once_with("my_file.txt")

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,10 @@ addopts = [
189189
"--cov-report=xml",
190190
]
191191
filterwarnings = [
192+
"ignore::DeprecationWarning:examples",
192193
"ignore::DeprecationWarning:pact",
193194
"ignore::DeprecationWarning:tests",
195+
"ignore::PendingDeprecationWarning:examples",
194196
"ignore::PendingDeprecationWarning:pact",
195197
"ignore::PendingDeprecationWarning:tests",
196198
]

0 commit comments

Comments
 (0)