Skip to content

Commit 9de486d

Browse files
committed
docs(examples): wip on a kafka example
1 parent ed9e473 commit 9de486d

9 files changed

+175
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"consumer": {
3+
"name": "AnimalConsumer_message"
4+
},
5+
"provider": {
6+
"name": "DogProducer_message"
7+
},
8+
"messages": [
9+
{
10+
"description": "Spot the poodle",
11+
"providerStates": [
12+
{
13+
"name": "A dog created",
14+
"params": null
15+
}
16+
],
17+
"contents": "{\"type\": \"dog\", \"name\": \"spot\", \"breed\": \"poodle\"}",
18+
"matchingRules": {
19+
"body": {
20+
}
21+
},
22+
"metaData": {
23+
"Content-Type": "application/json"
24+
}
25+
}
26+
],
27+
"metadata": {
28+
"pactSpecification": {
29+
"version": "3.0.0"
30+
}
31+
}
32+
}

examples/kafka/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ psutil==5.8.0
88
requests==2.25.1
99
six==1.15.0
1010
urllib3==1.26.4
11+
pytest==5.4.1

examples/kafka/src/__init__.py

Whitespace-only changes.

examples/kafka/src/kafka_consumer.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,31 @@ def __init__(self, type, name, breed):
1818
def __str__(self):
1919
return f'Animal {self.type}, Name {self.name}, Breed {self.breed}'
2020

21-
def object_decoder(obj):
22-
if '__type__' in obj and obj['__type__'] == 'dog':
21+
class CustomError(Exception):
22+
def __init__(self, *args):
23+
if args:
24+
self.topic = args[0]
25+
else:
26+
self.topic = None
27+
28+
def __str__(self):
29+
if self.topic:
30+
return 'Custom Error:, {0}'.format(self.topic)
31+
32+
33+
def message_decoder(obj):
34+
if 'type' in obj and obj['type'] == 'dog':
2335
return Animal('dog', obj['name'], obj['breed'])
24-
return obj
36+
raise CustomError('type unknown')
37+
38+
39+
def send_dog_event_foo(foo, payload):
40+
# ignoring foo here
41+
return send_dog_event(payload)
2542

2643

2744
def send_dog_event(payload):
28-
dog = json.loads(payload, object_hook=object_decoder)
45+
dog = json.loads(payload, object_hook=message_decoder)
2946
print(dog)
3047
return dog
3148

examples/kafka/src/kafka_producer.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
NAMES = ['Spot', 'Wolf', 'Winston', 'Tobey']
77
BREEDS = ['poodle', 'bulldog', 'Great Dane', 'Greyhound']
88

9+
PRODUCER = Producer({'bootstrap.servers': 'localhost:9092'})
10+
911
def acked(err, msg):
1012
if err is not None:
1113
print("Failed to deliver dog: {0}: {1}"
@@ -24,20 +26,19 @@ def __init__(self):
2426
def __str__(self):
2527
return f'Name {self.name}, Breed {self.breed}'
2628

27-
def send_message(p, dog):
29+
def send_message(dog):
2830
print(dog)
2931

30-
p.produce('mytopic', json.dumps(dog.__dict__), callback=acked)
31-
p.poll(0.5)
32+
PRODUCER.produce('mytopic', json.dumps(dog.__dict__), callback=acked)
33+
PRODUCER.poll(0.5)
3234

3335
def main():
3436
number_of_dogs = int(sys.argv[1])
35-
p = Producer({'bootstrap.servers': 'localhost:9092'})
3637

3738
try:
3839
for i in range(0, number_of_dogs):
39-
send_message(p, Dog())
40-
p.flush(30)
40+
send_message(Dog())
41+
PRODUCER.flush(30)
4142

4243
except KeyboardInterrupt:
4344
pass

examples/kafka/tests/__init__.py

Whitespace-only changes.

examples/kafka/tests/conftest.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
def pytest_addoption(parser):
3+
parser.addoption(
4+
"--publish-pact", type=str, action="store",
5+
help="Upload generated pact file to pact broker with version"
6+
)

examples/kafka/tests/test_consumer.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""pact test for a message consumer"""
2+
3+
import logging
4+
import pytest
5+
import json
6+
7+
from pact import MessageConsumer, Provider
8+
from src.kafka_consumer import send_dog_event, send_dog_event_foo, CustomError
9+
10+
log = logging.getLogger(__name__)
11+
logging.basicConfig(level=logging.INFO)
12+
13+
PACT_BROKER_URL = "http://localhost"
14+
PACT_BROKER_USERNAME = "pactbroker"
15+
PACT_BROKER_PASSWORD = "pactbroker"
16+
PACT_DIR = 'pacts'
17+
18+
CONSUMER_NAME = 'AnimalConsumer'
19+
PROVIDER_NAME = 'DogProducer'
20+
21+
PACT_FILE = (f"{PACT_DIR}/{CONSUMER_NAME.lower().replace(' ', '_')}_message-"
22+
+ f"{PROVIDER_NAME.lower().replace(' ', '_')}_message.json")
23+
24+
EXPECTED_DOG = {'type': 'dog', 'name': 'spot', 'breed': 'poodle'}
25+
26+
@pytest.fixture(scope='session')
27+
def pact(request):
28+
version = request.config.getoption('--publish-pact')
29+
publish = True if version else False
30+
31+
pact = MessageConsumer(CONSUMER_NAME, version=version).has_pact_with(
32+
Provider(PROVIDER_NAME),
33+
publish_to_broker=publish, broker_base_url=PACT_BROKER_URL,
34+
broker_username=PACT_BROKER_USERNAME, broker_password=PACT_BROKER_PASSWORD, pact_dir=PACT_DIR)
35+
36+
yield pact
37+
38+
39+
def test_assert_verify_message(pact):
40+
41+
(pact
42+
.given('A dog created')
43+
.expects_to_receive('Spot the poodle')
44+
.with_content(str(json.dumps(EXPECTED_DOG)))
45+
.with_metadata({
46+
'Content-Type': 'application/json'
47+
})
48+
.verify(send_dog_event)
49+
)
50+
51+
52+
def test_throw_exception_handler(pact):
53+
54+
with pytest.raises(CustomError):
55+
(pact
56+
.given('A dog created')
57+
.expects_to_receive('Spot the poodle')
58+
.with_content(str(json.dumps({'type': 'cat', 'name': 'spot', 'breed': 'poodle'})))
59+
.with_metadata({
60+
'Content-Type': 'application/json'
61+
})
62+
.verify(send_dog_event))
63+
64+
65+
def test_assert_dog_returned(pact):
66+
67+
(pact
68+
.given('A dog created')
69+
.expects_to_receive('Spot the poodle')
70+
.with_content(str(json.dumps(EXPECTED_DOG)))
71+
.with_metadata({
72+
'Content-Type': 'application/json'
73+
})
74+
.verify(send_dog_event)
75+
)
76+
77+
with pact:
78+
dog = pact.result
79+
assert dog.name == 'spot'
80+
assert dog.breed == 'poodle'
81+
82+
83+
def test_assert_calling_dog(pact):
84+
85+
(pact
86+
.given('A dog created')
87+
.expects_to_receive('Spot the poodle')
88+
.with_content(str(json.dumps(EXPECTED_DOG)))
89+
.with_metadata({
90+
'Content-Type': 'application/json'
91+
})
92+
)
93+
94+
with pact:
95+
dog = send_dog_event_foo({}, str(json.dumps(EXPECTED_DOG)))
96+
assert dog.name == 'spot'
97+
assert dog.breed == 'poodle'

pact/message_pact.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ def write_to_pact_file(self):
171171

172172
self._message_process = Popen(command)
173173

174+
def verify(self, func_to_call):
175+
"""Verify our target method with our content."""
176+
self.result = func_to_call(self._messages[0]['contents'])
177+
174178
def _insert_message_if_complete(self):
175179
"""
176180
Insert a new message if current message is complete.
@@ -185,8 +189,13 @@ def _insert_message_if_complete(self):
185189
self._messages.insert(0, {})
186190

187191
def __enter__(self):
188-
"""Enter a Python context. This function is required for context manager to work."""
189-
pass
192+
"""
193+
Enter a Python context. This function is required for context manager to work.
194+
195+
Calls our handler function with our payload
196+
"""
197+
if hasattr(self, 'func_to_call') and callable(self.func_to_call):
198+
self.result = self.func_to_call(self._messages[0]['contents'])
190199

191200
def __exit__(self, exc_type, exc_val, exc_tb):
192201
"""

0 commit comments

Comments
 (0)