Skip to content

Commit 02643d4

Browse files
authored
test(examples-fastapi): tidy FastAPI example, making consistent with Flask (#274)
* test(examples-fastapi): tidy FastAPI example, making consistent with Flask re #270 * test(examples-fastapi): tidy FastAPI example, making consistent with Flask re #270 * test(examples-fastapi): tidy FastAPI example, making consistent with Flask re #270 * test(examples-fastapi): tidy FastAPI example, making consistent with Flask re #270
1 parent bf110e2 commit 02643d4

File tree

14 files changed

+297
-187
lines changed

14 files changed

+297
-187
lines changed

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ messaging:
9494

9595

9696
.PHONY: examples
97-
examples: consumer flask messaging
98-
# TODO: Fix fastapi, to run all examples this should be: consumer flask fastapi messaging
97+
examples: consumer flask fastapi messaging
9998

10099

101100
.PHONY: package

examples/README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ The following file(s) will be created when the tests are run:
8282
The Flask [Provider] example consists of a basic Flask app, with a single endpoint route.
8383
This implements the service expected by the [consumer](#consumer).
8484

85+
Functionally, this provides the same service and tests as the [fastapi_provider](#fastapi_provider). Both are included to
86+
demonstrate how Pact can be used in different environments with different technology stacks and approaches.
87+
8588
The [Provider] side is responsible for performing the tests to verify if it is compliant with the [Pact file] contracts
8689
associated with it.
8790

@@ -132,7 +135,55 @@ The following file(s) will be created when the tests are run
132135
133136
## fastapi_provider
134137
135-
TODO
138+
The FastAPI [Provider] example consists of a basic FastAPI app, with a single endpoint route.
139+
This implements the service expected by the [consumer](#consumer).
140+
141+
Functionally, this provides the same service and tests as the [flask_provider](#flask_provider). Both are included to
142+
demonstrate how Pact can be used in different environments with different technology stacks and approaches.
143+
144+
The [Provider] side is responsible for performing the tests to verify if it is compliant with the [Pact file] contracts
145+
associated with it.
146+
147+
As such, the tests use the pact-python Verifier to perform this verification. Two approaches are demonstrated:
148+
- Testing against the [Pact broker]. Generally this is the preferred approach, see information on [Sharing Pacts].
149+
- Testing against the [Pact file] directly. If no [Pact broker] is available you can verify against a static [Pact file].
150+
-
151+
### Running
152+
153+
To avoid package version conflicts with different applications, it is recommended to run these tests from a
154+
[Virtual Environment]
155+
156+
The following commands can be run from within your [Virtual Environment], in the `examples/fastapi_provider`.
157+
158+
To perform the python tests:
159+
```bash
160+
pip install -r requirements.txt # Install the dependencies for the FastAPI example
161+
pip install -e ../../ # Using setup.py in the pact-python root, install any pact dependencies and pact-python
162+
./run_pytest.sh # Wrapper script to first run FastAPI, and then run the tests
163+
```
164+
165+
To perform verification using CLI to verify the [Pact file] against the FastAPI [Provider] instead of the python tests:
166+
```bash
167+
pip install -r requirements.txt # Install the dependencies for the FastAPI example
168+
./verify_pact.sh # Wrapper script to first run FastAPI, and then use `pact-verifier` to verify locally
169+
```
170+
171+
To perform verification using CLI, but verifying the [Pact file] previously provided by a [Consumer], and publish the
172+
results. This example requires that the [Pact broker] is already running, and the [Consumer] tests have been published
173+
already, described in the [consumer](#consumer) section above.
174+
```bash
175+
pip install -r requirements.txt # Install the dependencies for the FastAPI example
176+
./verify_pact.sh 1 # Wrapper script to first run FastAPI, and then use `pact-verifier` to verify and publish
177+
```
178+
179+
### Output
180+
181+
The following file(s) will be created when the tests are run
182+
183+
| Filename | Contents |
184+
|-------------------------------| ----------|
185+
| fastapi_provider/log/pact.log | All Pact interactions with the FastAPI Provider. Every interaction example retrieved from the Pact Broker will be performed during the Verification test; the request/response logged here. |
186+
136187
137188
## message
138189
@@ -153,4 +204,4 @@ without a [Pact Broker].
153204
[Pact verification]: https://docs.pact.io/getting_started/terminology#pact-verification]
154205
[Virtual Environment]: https://docs.python.org/3/tutorial/venv.html
155206
[Sharing Pacts]: https://docs.pact.io/getting_started/sharing_pacts/]
156-
[How to Run a Flask Application]: https://www.twilio.com/blog/how-run-flask-application
207+
[How to Run a Flask Application]: https://www.twilio.com/blog/how-run-flask-application

examples/fastapi_provider/pythonclient-pythonservice.json

Lines changed: 0 additions & 81 deletions
This file was deleted.

examples/fastapi_provider/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ fastapi==0.67.0
22
pytest==5.4.1
33
requests>=2.26.0
44
uvicorn>=0.14.0
5+
testcontainers==3.3.0
Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
#!/bin/bash
22
set -o pipefail
33

4-
uvicorn src.provider:app --host 0.0.0.0 --port 8080 & &>/dev/null
5-
FASTAPI_PID=$!
6-
7-
function teardown {
8-
echo "Tearing down FastAPI server: ${FASTAPI_PID}"
9-
kill -9 $FLASK_PID
10-
}
11-
trap teardown EXIT
12-
13-
sleep 1
14-
15-
pytest
4+
# Unlike in the Flask example, here the FastAPI service is started up as a pytest fixture. This is then including the
5+
# main and pact routes via fastapi_provider.py to run the tests against
6+
pytest --run-broker True --publish-pact 1
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
import logging
2+
13
from fastapi import FastAPI, HTTPException, APIRouter
4+
from fastapi.logger import logger
25

36
fakedb = {} # Use a simple dict to represent a database
7+
8+
logger.setLevel(logging.DEBUG)
49
router = APIRouter()
510
app = FastAPI()
611

712

813
@app.get("/users/{name}")
914
def get_user_by_name(name: str):
15+
"""Handle requests to retrieve a single user from the simulated database.
16+
17+
:param name: Name of the user to "search for"
18+
:return: The user data if found, HTTP 404 if not
19+
"""
1020
user_data = fakedb.get(name)
1121
if not user_data:
22+
logger.error(f"GET user for: '{name}', HTTP 404 not found")
1223
raise HTTPException(status_code=404, detail="User not found")
13-
24+
logger.error(f"GET user for: '{name}', returning: {user_data}")
1425
return user_data

examples/fastapi_provider/tests/__init__.py

Whitespace-only changes.
Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,105 @@
1+
import pathlib
2+
import sys
3+
from multiprocessing import Process
4+
5+
import docker
6+
import pytest
7+
from testcontainers.compose import DockerCompose
8+
9+
from .pact_provider import run_server
10+
11+
12+
@pytest.fixture(scope="module")
13+
def server():
14+
proc = Process(target=run_server, args=(), daemon=True)
15+
proc.start()
16+
yield proc
17+
18+
# Cleanup after test
19+
if sys.version_info >= (3, 7):
20+
# multiprocessing.kill is new in 3.7
21+
proc.kill()
22+
else:
23+
proc.terminate()
24+
125

226
def pytest_addoption(parser):
327
parser.addoption(
4-
"--publish-pact", type=str, action="store",
5-
help="Upload generated pact file to pact broker with version"
28+
"--publish-pact", type=str, action="store", help="Upload generated pact file to pact broker with version"
629
)
730

8-
parser.addoption(
9-
"--provider-url", type=str, action="store",
10-
help="The url to our provider."
31+
parser.addoption("--run-broker", type=bool, action="store", help="Whether to run broker in this test or not.")
32+
33+
34+
@pytest.fixture(scope="session", autouse=True)
35+
def publish_existing_pact(broker):
36+
"""Publish the contents of the pacts folder to the Pact Broker.
37+
38+
In normal usage, a Consumer would publish Pacts to the Pact Broker after
39+
running tests - this fixture would NOT be needed.
40+
.
41+
Because the broker is being used standalone here, it will not contain the
42+
required Pacts, so we must first spin up the pact-cli and publish them.
43+
44+
In the Pact Broker logs, this corresponds to the following entry:
45+
PactBroker::Pacts::Service -- Creating new pact publication with params \
46+
{:consumer_name=>"UserServiceClient", :provider_name=>"UserService", \
47+
:revision_number=>nil, :consumer_version_number=>"1", :pact_version_sha=>nil, \
48+
:consumer_name_in_pact=>"UserServiceClient", :provider_name_in_pact=>"UserService"}
49+
"""
50+
source = str(pathlib.Path.cwd().joinpath("..", "pacts").resolve())
51+
pacts = [f"{source}:/pacts"]
52+
envs = {
53+
"PACT_BROKER_BASE_URL": "http://broker_app:9292",
54+
"PACT_BROKER_USERNAME": "pactbroker",
55+
"PACT_BROKER_PASSWORD": "pactbroker",
56+
}
57+
58+
client = docker.from_env()
59+
60+
print("Publishing existing Pact")
61+
client.containers.run(
62+
remove=True,
63+
network="broker_default",
64+
volumes=pacts,
65+
image="pactfoundation/pact-cli:latest",
66+
environment=envs,
67+
command="publish /pacts --consumer-app-version 1",
1168
)
69+
print("Finished publishing")
70+
71+
72+
# This fixture is to simulate a managed Pact Broker or Pactflow account.
73+
# For almost all purposes outside this example, you will want to use a real
74+
# broker. See https://github.com/pact-foundation/pact_broker for further details.
75+
@pytest.fixture(scope="session", autouse=True)
76+
def broker(request):
77+
version = request.config.getoption("--publish-pact")
78+
publish = True if version else False
79+
80+
# If the results are not going to be published to the broker, there is
81+
# nothing further to do anyway
82+
if not publish:
83+
yield
84+
return
85+
86+
run_broker = request.config.getoption("--run-broker")
87+
88+
if run_broker:
89+
# Start up the broker using docker-compose
90+
print("Starting broker")
91+
with DockerCompose("../broker", compose_file_name=["docker-compose.yml"], pull=True) as compose:
92+
stdout, stderr = compose.get_logs()
93+
if stderr:
94+
print("Errors\\n:{}".format(stderr))
95+
print("{}".format(stdout))
96+
print("Started broker")
97+
98+
yield
99+
print("Stopping broker")
100+
print("Broker stopped")
101+
else:
102+
# Assuming there is a broker available already, docker-compose has been
103+
# used manually as the --run-broker option has not been provided
104+
yield
105+
return

examples/fastapi_provider/tests/fastapi_provider.py

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)