Skip to content

Commit

Permalink
chore: address pr comments
Browse files Browse the repository at this point in the history
Signed-off-by: JP-Ellis <[email protected]>
  • Loading branch information
JP-Ellis committed Sep 27, 2023
1 parent e86b7eb commit 9896320
Show file tree
Hide file tree
Showing 14 changed files with 139 additions and 136 deletions.
111 changes: 30 additions & 81 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,106 +1,55 @@
DOCS_DIR := ./docs

PROJECT := pact-python
PYTHON_MAJOR_VERSION := 3.11

sgr0 := $(shell tput sgr0)
red := $(shell tput setaf 1)
green := $(shell tput setaf 2)

help:
@echo ""
@echo " clean to clear build and distribution directories"
@echo " examples to run the example end to end tests (consumer, fastapi, flask, messaging)"
@echo " consumer to run the example consumer tests"
@echo " fastapi to run the example FastApi provider tests"
@echo " flask to run the example Flask provider tests"
@echo " messaging to run the example messaging e2e tests"
@echo " package to create a distribution package in /dist/"
@echo " package to build a wheel and sdist"
@echo " release to perform a release build, including deps, test, and package targets"
@echo " test to run all tests"
@echo ""
@echo " test to run all tests on the current python version"
@echo " test-all to run all tests on all supported python versions"
@echo " example to run the example end to end tests (requires docker)"
@echo " lint to run the lints"
@echo " ci to run test and lints"
@echo ""
@echo " help to show this help message"
@echo ""
@echo "Most of these targets are just wrappers around hatch commands."
@echo "See https://hatch.pypa.org for information to install hatch."


.PHONY: release
release: test package
release: clean test package


.PHONY: clean
clean:
hatch clean


define CONSUMER
echo "consumer make"
cd examples/consumer
pip install -q -r requirements.txt
pip install -e ../../
./run_pytest.sh
endef
export CONSUMER


define FLASK_PROVIDER
echo "flask make"
cd examples/flask_provider
pip install -q -r requirements.txt
pip install -e ../../
./run_pytest.sh
endef
export FLASK_PROVIDER


define FASTAPI_PROVIDER
echo "fastapi make"
cd examples/fastapi_provider
pip install -q -r requirements.txt
pip install -e ../../
./run_pytest.sh
endef
export FASTAPI_PROVIDER


define MESSAGING
echo "messaging make"
cd examples/message
pip install -q -r requirements.txt
pip install -e ../../
./run_pytest.sh
endef
export MESSAGING


.PHONY: consumer
consumer:
bash -c "$$CONSUMER"


.PHONY: flask
flask:
bash -c "$$FLASK_PROVIDER"
.PHONY: package
package:
hatch build


.PHONY: fastapi
fastapi:
bash -c "$$FASTAPI_PROVIDER"
.PHONY: test
test:
hatch run test
hatch run coverage report -m --fail-under=100


.PHONY: messaging
messaging:
bash -c "$$MESSAGING"
.PHONY: test-all
test-all:
hatch run test:test


.PHONY: examples
examples: consumer flask fastapi messaging
.PHONY: example
example:
hatch run example


.PHONY: package
package:
hatch build
.PHONY: lint
lint:
hatch run lint


.PHONY: test
test:
hatch run all
hatch run test:all
coverage report -m --fail-under=100
.PHONY: ci
ci: test lint
4 changes: 2 additions & 2 deletions examples/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from __future__ import annotations

from pathlib import Path
from typing import Any, Generator
from typing import Any, Generator, Union

import pytest
from testcontainers.compose import DockerCompose
Expand Down Expand Up @@ -45,7 +45,7 @@ def broker(request: pytest.FixtureRequest) -> Generator[URL, Any, None]:
Otherwise, the Pact broker is started in a container. The URL of the
containerised broker is then returned.
"""
broker_url: str | None = request.config.getoption("--broker-url")
broker_url: Union[str, None] = request.config.getoption("--broker-url")

# If we have been given a broker URL, there's nothing more to do here and we
# can return early.
Expand Down
1 change: 1 addition & 0 deletions examples/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:
- postgres
ports:
- "9292:9292"
restart: always
environment:
# Basic auth credentials for the Broker
PACT_BROKER_ALLOW_PUBLIC_READ: "true"
Expand Down
9 changes: 5 additions & 4 deletions examples/src/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
This modules defines a simple
[consumer](https://docs.pact.io/getting_started/terminology#service-consumer)
with Pact. As Pact is a consumer-driven framework, the consumer defines the
interactions which the provider must then satisfy.
which will be tested with Pact in the [consumer
test](../tests/test_00_consumer.py). As Pact is a consumer-driven framework, the
consumer defines the interactions which the provider must then satisfy.
The consumer is the application which makes requests to another service (the
provider) and receives a response to process. In this example, we have a simple
Expand All @@ -20,7 +21,7 @@

from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, Dict

import requests

Expand Down Expand Up @@ -95,7 +96,7 @@ def get_user(self, user_id: int) -> User:
uri = f"{self.base_uri}/users/{user_id}"
response = requests.get(uri, timeout=5)
response.raise_for_status()
data: dict[str, Any] = response.json()
data: Dict[str, Any] = response.json()
return User(
id=data["id"],
name=data["name"],
Expand Down
18 changes: 10 additions & 8 deletions examples/src/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
This modules defines a simple
[provider](https://docs.pact.io/getting_started/terminology#service-provider)
with Pact. As Pact is a consumer-driven framework, the consumer defines the
contract which the provider must then satisfy.
which will be tested with Pact in the [provider
test](../tests/test_01_provider_fastapi.py). As Pact is a consumer-driven
framework, the consumer defines the contract which the provider must then
satisfy.
The provider is the application which receives requests from another service
(the consumer) and returns a response. In this example, we have a simple
Expand All @@ -17,7 +19,7 @@

from __future__ import annotations

from typing import Any
from typing import Any, Dict

from fastapi import FastAPI
from fastapi.responses import JSONResponse
Expand All @@ -28,15 +30,15 @@
As this is a simple example, we'll use a simple dict to represent a database.
This would be replaced with a real database in a real application.
When testing the provider in a real application, the calls to the database
would be mocked out to avoid the need for a real database. An example of this
can be found in the test suite.
When testing the provider in a real application, the calls to the database would
be mocked out to avoid the need for a real database. An example of this can be
found in the [test suite](../tests/test_01_provider_fastapi.py).
"""
FAKE_DB: dict[int, dict[str, Any]] = {}
FAKE_DB: Dict[int, Dict[str, Any]] = {}


@app.get("/users/{uid}")
async def get_user_by_id(uid: int) -> dict[str, Any]:
async def get_user_by_id(uid: int) -> Dict[str, Any]:
"""
Fetch a user by their ID.
Expand Down
14 changes: 8 additions & 6 deletions examples/src/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
This modules defines a simple
[provider](https://docs.pact.io/getting_started/terminology#service-provider)
with Pact. As Pact is a consumer-driven framework, the consumer defines the
contract which the provider must then satisfy.
which will be tested with Pact in the [provider
test](../tests/test_01_provider_flask.py). As Pact is a consumer-driven
framework, the consumer defines the contract which the provider must then
satisfy.
The provider is the application which receives requests from another service
(the consumer) and returns a response. In this example, we have a simple
Expand All @@ -17,7 +19,7 @@

from __future__ import annotations

from typing import Any
from typing import Any, Dict, Union

from flask import Flask

Expand All @@ -29,13 +31,13 @@
When testing the provider in a real application, the calls to the database
would be mocked out to avoid the need for a real database. An example of this
can be found in the test suite.
can be found in the [test suite](../tests/test_01_provider_flask.py).
"""
FAKE_DB: dict[int, dict[str, Any]] = {}
FAKE_DB: Dict[int, Dict[str, Any]] = {}


@app.route("/users/<uid>")
def get_user_by_id(uid: int) -> dict[str, Any] | tuple[dict[str, Any], int]:
def get_user_by_id(uid: int) -> Union[Dict[str, Any], tuple[Dict[str, Any], int]]:
"""
Fetch a user by their ID.
Expand Down
6 changes: 3 additions & 3 deletions examples/src/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from typing import Any, Dict, Union


class Filesystem:
Expand Down Expand Up @@ -42,7 +42,7 @@ def __init__(self) -> None:
"""
self.fs = Filesystem()

def process(self, event: dict[str, Any]) -> str | None:
def process(self, event: Dict[str, Any]) -> Union[str, None]:
"""
Process an event from the queue.
Expand All @@ -67,7 +67,7 @@ def process(self, event: dict[str, Any]) -> str | None:
raise ValueError(msg)

@staticmethod
def validate_event(event: dict[str, Any] | Any) -> None: # noqa: ANN401
def validate_event(event: Union[Dict[str, Any], Any]) -> None: # noqa: ANN401
"""
Validates the event received from the queue.
Expand Down
19 changes: 16 additions & 3 deletions examples/tests/test_00_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
is responding with the expected responses. Once these interactions are
validated, the contracts can be published to a Pact Broker. The contracts can
then be used to validate the provider's interactions.
A good resource for understanding the consumer tests is the [Pact Consumer
Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test)
section of the Pact documentation.
"""

from __future__ import annotations

import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Generator
from typing import TYPE_CHECKING, Any, Dict, Generator

import pytest
import requests
Expand All @@ -40,6 +44,11 @@ def user_consumer() -> UserConsumer:
the consumer to use Pact's mock provider. This allows us to define what
requests the consumer will make to the provider, and what responses the
provider will return.
The ability for the client to specify the expected response from the
provider is critical to Pact's consumer-driven approach as it allows the
consumer to declare the minimal response it requires from the provider (even
if the provider is returning more data than the consumer needs).
"""
return UserConsumer(str(MOCK_URL))

Expand All @@ -53,7 +62,7 @@ def pact(broker: URL, pact_dir: Path) -> Generator[Pact, Any, None]:
the provider. This mock provider will expect to receive defined requests
and will respond with defined responses.
The fixture here simply defines the Consumer and Provide, and sets up the
The fixture here simply defines the Consumer and Provider, and sets up the
mock provider. With each test, we define the expected request and response
from the provider as follows:
Expand Down Expand Up @@ -90,7 +99,11 @@ def test_get_existing_user(pact: Pact, user_consumer: UserConsumer) -> None:
This test defines the expected request and response from the provider. The
provider will be expected to return a response with a status code of 200,
"""
expected: dict[str, Any] = {
# When setting up the expected response, the consumer should only define
# what it needs from the provider (as opposed to the full schema). Should
# the provider later decide to add or remove fields, Pact's consumer-driven
# approach will ensure that interaction is still valid.
expected: Dict[str, Any] = {
"id": Format().integer,
"name": "Verna Hampton",
"created_on": Format().iso_8601_datetime(),
Expand Down
17 changes: 13 additions & 4 deletions examples/tests/test_01_provider_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
additional endpoint on the provider, in this case `/_pact/provider_states`.
Calls to this endpoint mock the relevant database calls to set the provider into
the correct state.
A good resource for understanding the provider tests is the [Pact Provider
Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test)
section of the Pact documentation.
"""

from __future__ import annotations

from multiprocessing import Process
from typing import Any, Generator
from typing import Any, Dict, Generator, Union
from unittest.mock import MagicMock

import pytest
Expand All @@ -42,7 +46,9 @@ class ProviderState(BaseModel):


@app.post("/_pact/provider_states")
async def mock_pact_provider_states(state: ProviderState) -> dict[str, str | None]:
async def mock_pact_provider_states(
state: ProviderState,
) -> Dict[str, Union[str, None]]:
"""
Define the provider state.
Expand Down Expand Up @@ -102,10 +108,13 @@ def mock_user_123_exists() -> None:
You may notice that the return value here differs from the consumer's
expected response. This is because the consumer's expected response is
guided by what the consumer users.
guided by what the consumer uses.
By using consumer-driven contracts and testing the provider against the
consumer's contract, we can ensure that the provider is only providing what
consumer's contract, we can ensure that the provider is what the consumer
needs. This allows the provider to safely evolve their API (by both adding
and removing fields) without fear of breaking the interactions with the
consumers.
"""
import examples.src.fastapi

Expand Down
Loading

0 comments on commit 9896320

Please sign in to comment.