Skip to content

Commit eaa90e1

Browse files
Merge pull request #194 from williaminfante/feat/pact-message-2
feat: implement pact message
2 parents 2946242 + 5ed73db commit eaa90e1

21 files changed

+1287
-245
lines changed

Makefile

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,29 @@ define E2E
4040
endef
4141
export E2E
4242

43+
44+
define messaging
45+
echo "messaging make"
46+
cd examples/message
47+
pip install -r requirements.txt
48+
pip install -e ../../
49+
pytest
50+
endef
51+
export messaging
52+
53+
4354
.PHONY: e2e
4455
e2e:
4556
bash -c "$$E2E"
4657

58+
59+
.PHONY: messaging
60+
messaging:
61+
bash -c "$$messaging"
62+
63+
4764
.PHONY: examples
48-
examples: e2e
49-
65+
examples: e2e messaging
5066

5167

5268
.PHONY: package

examples/message/README.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Introduction
2+
3+
This is an e2e example that uses messages, including a sample implementation of a message handler.
4+
5+
## Consumer
6+
7+
A Consumer is the system that will be reading a message from a queue or some intermediary. In this example, the consumer is a Lambda function that handles the message.
8+
9+
From a Pact testing point of view, Pact takes the place of the intermediary (MQ/broker etc.) and confirms whether or not the consumer is able to handle a request.
10+
11+
```
12+
+-----------+ +-------------------+
13+
| (Pact) | message |(Message Consumer) |
14+
| MQ/broker |--------->|Lambda Function |
15+
| | |check valid doc |
16+
+-----------+ +-------------------+
17+
```
18+
19+
Below is a sample message handler that only accepts that the key `documentType` would only be `microsoft-word`. If not, the message handler will throw an exception (`CustomError`)
20+
21+
```python
22+
class CustomError(Exception):
23+
def __init__(self, *args):
24+
if args:
25+
self.topic = args[0]
26+
else:
27+
self.topic = None
28+
29+
def __str__(self):
30+
if self.topic:
31+
return 'Custom Error:, {0}'.format(self.topic)
32+
33+
class MessageHandler(object):
34+
def __init__(self, event):
35+
self.pass_event(event)
36+
37+
@staticmethod
38+
def pass_event(event):
39+
if event.get('documentType') != 'microsoft-word':
40+
raise CustomError("Not correct document type")
41+
```
42+
43+
Below is a snippet from a test where the message handler has no error.
44+
Since the expected event contains a key `documentType` with value `microsoft-word`, message handler does not throw an error and a pact file `f"pacts/{expected_json}"` is expected to be generated.
45+
46+
```python
47+
def test_generate_new_pact_file(pact):
48+
cleanup_json(PACT_FILE)
49+
50+
expected_event = {
51+
'documentName': 'document.doc',
52+
'creator': 'TP',
53+
'documentType': 'microsoft-word'
54+
}
55+
56+
(pact
57+
.given('A document create in Document Service')
58+
.expects_to_receive('Description')
59+
.with_content(expected_event)
60+
.with_metadata({
61+
'Content-Type': 'application/json'
62+
}))
63+
64+
with pact:
65+
# handler needs 'documentType' == 'microsoft-word'
66+
MessageHandler(expected_event)
67+
68+
progressive_delay(f"{PACT_FILE}")
69+
assert isfile(f"{PACT_FILE}") == 1
70+
```
71+
72+
For a similar test where the event does not contain a key `documentType` with value `microsoft-word`, a `CustomError` is generated and there there is no generated json file `f"pacts/{expected_json}"`.
73+
74+
```python
75+
def test_throw_exception_handler(pact):
76+
cleanup_json(PACT_FILE)
77+
wrong_event = {
78+
'documentName': 'spreadsheet.xls',
79+
'creator': 'WI',
80+
'documentType': 'microsoft-excel'
81+
}
82+
83+
(pact
84+
.given('Another document in Document Service')
85+
.expects_to_receive('Description')
86+
.with_content(wrong_event)
87+
.with_metadata({
88+
'Content-Type': 'application/json'
89+
}))
90+
91+
with pytest.raises(CustomError):
92+
with pact:
93+
# handler needs 'documentType' == 'microsoft-word'
94+
MessageHandler(wrong_event)
95+
96+
progressive_delay(f"{PACT_FILE}")
97+
assert isfile(f"{PACT_FILE}") == 0
98+
```
99+
100+
Otherwise, no pact file is generated.
101+
102+
## Provider
103+
104+
Note: The current example only tests the consumer side.
105+
In the future, provider tests will also be included.
106+
107+
```
108+
+-------------------+ +-----------+
109+
|(Message Provider) | message | (Pact) |
110+
|Document Upload |--------->| MQ/broker |
111+
|Service | | |
112+
+-------------------+ +-----------+
113+
```
114+
115+
## E2E Messaging
116+
117+
Note: The current example only tests the consumer side.
118+
In the future, provider tests will also be included.
119+
120+
```
121+
+-------------------+ +-----------+ +-------------------+
122+
|(Message Provider) | message | (Pact) | message |(Message Consumer) |
123+
|Document Upload |--------->| MQ/broker |--------->|Lambda Function |
124+
|Service | | | |check valid doc |
125+
+-------------------+ +-----------+ +-------------------+
126+
```
127+
128+
# Setup
129+
130+
## Virtual Environment
131+
132+
Go to the `example/message` directory Create your own virtualenv for this. Run
133+
134+
```bash
135+
pip install -r requirements.txt
136+
pip install -e ../../
137+
pytest
138+
```
139+
140+
## Message Consumer
141+
142+
From the root directory run:
143+
144+
```bash
145+
pytest
146+
```
147+
148+
Or you can run individual tests like:
149+
150+
```bash
151+
pytest tests/consumer/test_message_consumer.py::test_generate_new_pact_file
152+
```
153+
154+
## With Broker
155+
156+
The current consumer test can run even without a local broker,
157+
but this is added for demo purposes.
158+
159+
Open a separate terminal in the `examples/broker` folder and run:
160+
161+
```bash
162+
docker-compose up
163+
```
164+
165+
Open a browser to http://localhost and see the broker you have succeeded.
166+
If needed, log-in using the provided details in tests such as:
167+
168+
```
169+
PACT_BROKER_USERNAME = "pactbroker"
170+
PACT_BROKER_PASSWORD = "pactbroker"
171+
```
172+
173+
To get the consumer to publish a pact to broker,
174+
open a new terminal in the `examples/message` and run the following (2 is an arbitary version number). The first part makes sure that the an existing json has been generated:
175+
176+
```bash
177+
pytest tests/consumer/test_message_consumer.py::test_publish_to_broker
178+
pytest tests/consumer/test_message_consumer.py::test_publish_to_broker --publish-pact 2
179+
```

examples/message/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask==1.1.1
2+
pytest==5.4.1
3+
requests==2.23.0

examples/message/run_pytest.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
set -o pipefail
3+
4+
pytest
5+
6+
# publish to broker assuming broker is active
7+
# pytest tests/consumer/test_message_consumer.py::test_publish_to_broker --publish-pact 2

examples/message/src/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class CustomError(Exception):
2+
def __init__(self, *args):
3+
if args:
4+
self.topic = args[0]
5+
else:
6+
self.topic = None
7+
8+
def __str__(self):
9+
if self.topic:
10+
return 'Custom Error:, {0}'.format(self.topic)
11+
12+
class MessageHandler(object):
13+
def __init__(self, event):
14+
self.pass_event(event)
15+
16+
@staticmethod
17+
def pass_event(event):
18+
if event.get('documentType') != 'microsoft-word':
19+
raise CustomError("Not correct document type")

examples/message/tests/__init__.py

Whitespace-only changes.

examples/message/tests/conftest.py

Lines changed: 6 additions & 0 deletions
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/message/tests/consumer/__init__.py

Whitespace-only changes.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""pact test for a message consumer"""
2+
3+
import logging
4+
import pytest
5+
import time
6+
7+
from os import remove
8+
from os.path import isfile
9+
10+
from pact import MessageConsumer, Provider
11+
from src.message_handler import MessageHandler, CustomError
12+
13+
log = logging.getLogger(__name__)
14+
logging.basicConfig(level=logging.INFO)
15+
16+
PACT_BROKER_URL = "http://localhost"
17+
PACT_BROKER_USERNAME = "pactbroker"
18+
PACT_BROKER_PASSWORD = "pactbroker"
19+
PACT_DIR = 'pacts'
20+
21+
CONSUMER_NAME = 'DetectContentLambda'
22+
PROVIDER_NAME = 'ContentProvider'
23+
PACT_FILE = (f"{CONSUMER_NAME.lower().replace(' ', '_')}_message-"
24+
+ f"{PROVIDER_NAME.lower().replace(' ', '_')}_message.json")
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)
35+
36+
# current pact does not consider the PACT_DIR argument, assumes none
37+
yield pact
38+
39+
40+
def cleanup_json(file):
41+
"""
42+
Remove existing json file before test if any
43+
"""
44+
if (isfile(f"{file}")):
45+
remove(f"{file}")
46+
47+
48+
def progressive_delay(file, time_to_wait=10, second_interval=0.5, verbose=False):
49+
"""
50+
progressive delay
51+
defaults to wait up to 5 seconds with 0.5 second intervals
52+
"""
53+
time_counter = 0
54+
while not isfile(file):
55+
time.sleep(second_interval)
56+
time_counter += 1
57+
if verbose:
58+
print(f'Trying for {time_counter*second_interval} seconds')
59+
if time_counter > time_to_wait:
60+
if verbose:
61+
print(f'Already waited {time_counter*second_interval} seconds')
62+
break
63+
64+
65+
def test_throw_exception_handler(pact):
66+
cleanup_json(PACT_FILE)
67+
wrong_event = {
68+
'documentName': 'spreadsheet.xls',
69+
'creator': 'WI',
70+
'documentType': 'microsoft-excel'
71+
}
72+
73+
(pact
74+
.given('Another document in Document Service')
75+
.expects_to_receive('Description')
76+
.with_content(wrong_event)
77+
.with_metadata({
78+
'Content-Type': 'application/json'
79+
}))
80+
81+
with pytest.raises(CustomError):
82+
with pact:
83+
# handler needs 'documentType' == 'microsoft-word'
84+
MessageHandler(wrong_event)
85+
86+
progressive_delay(f"{PACT_FILE}")
87+
assert isfile(f"{PACT_FILE}") == 0
88+
89+
90+
def test_generate_new_pact_file(pact):
91+
cleanup_json(PACT_FILE)
92+
93+
expected_event = {
94+
'documentName': 'document.doc',
95+
'creator': 'TP',
96+
'documentType': 'microsoft-word'
97+
}
98+
99+
(pact
100+
.given('A document create in Document Service')
101+
.expects_to_receive('Description')
102+
.with_content(expected_event)
103+
.with_metadata({
104+
'Content-Type': 'application/json'
105+
}))
106+
107+
with pact:
108+
# handler needs 'documentType' == 'microsoft-word'
109+
MessageHandler(expected_event)
110+
111+
progressive_delay(f"{PACT_FILE}")
112+
assert isfile(f"{PACT_FILE}") == 1
113+
114+
115+
def test_publish_to_broker(pact):
116+
"""
117+
This test does not clean-up previously generated pact.
118+
Sample execution where 2 is an arbitrary version:
119+
120+
`pytest tests/consumer/test_message_consumer.py::test_publish_pact_to_broker`
121+
122+
`pytest tests/consumer/test_message_consumer.py::test_publish_pact_to_broker --publish-pact 2`
123+
"""
124+
expected_event = {
125+
'documentName': 'document.doc',
126+
'creator': 'TP',
127+
'documentType': 'microsoft-word'
128+
}
129+
130+
(pact
131+
.given('A document create in Document Service with broker')
132+
.expects_to_receive('Description with broker')
133+
.with_content(expected_event)
134+
.with_metadata({
135+
'Content-Type': 'application/json'
136+
}))
137+
138+
with pact:
139+
MessageHandler(expected_event)
140+
141+
progressive_delay(f"{PACT_FILE}")
142+
assert isfile(f"{PACT_FILE}") == 1

0 commit comments

Comments
 (0)