Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: added v3 http interaction examples #773

Merged
merged 2 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/.ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ ignore = [

[lint.per-file-ignores]
"tests/**.py" = [
"INP001", # Forbid implicit namespaces
"INP001", # Forbid implicit namespaces
"PLR2004", # Forbid magic values
]
2 changes: 1 addition & 1 deletion examples/tests/v3/provider_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def redirect() -> NoReturn:
if __name__ == "__main__":
import sys

if len(sys.argv) < 5: # noqa: PLR2004
if len(sys.argv) < 5:
sys.stderr.write(
f"Usage: {sys.argv[0]} <state_provider_module> <state_provider_function> "
f"<handler_module> <handler_function>"
Expand Down
133 changes: 133 additions & 0 deletions examples/tests/v3/test_00_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
HTTP consumer test using Pact Python v3.

This module demonstrates how to write a consumer test using Pact Python's
upcoming version 3. Pact, being a consumer-driven testing tool, requires that
the consumer define the expected interactions with the provider.

In this example, the consumer defined in `src/consumer.py` is tested against a
mock provider. The mock provider is set up by Pact and is used to ensure that
the consumer is making the expected requests to the provider. Once these
interactions are validated, the contracts can be published to a Pact Broker
where they can be re-run against the provider to ensure that the provider is
compliant with the contract.

A good source for understanding the consumer tests is the [Pact Consumer Test
section](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test)
of the Pact documentation.
"""

import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Generator

import pytest
import requests

from pact.v3 import Pact


@pytest.fixture
def pact() -> Generator[Pact, None, None]:
"""
Set up the Pact fixture.

This fixture configures the Pact instance for the consumer test. It defines
where the pact file will be written and the consumer and provider names.
This fixture also sets the Pact specification to `V4` (the latest version).

The use of `yield` allows this function to return the Pact instance to be
used in the test cases, and then for this function to continue running after
the test cases have completed. This is useful for writing the pact file
after the test cases have run.

Yields:
The Pact instance for the consumer tests.
"""
pact_dir = Path(Path(__file__).parent.parent.parent / "pacts")
pact = Pact("v3_http_consumer", "v3_http_provider")
yield pact.with_specification("V4")
pact.write_file(pact_dir, overwrite=True)


def test_get_existing_user(pact: Pact) -> None:
"""
Retrieve an existing user's details.

This test defines the expected interaction for a GET request to retrieve
user information. It sets up the expected request and response from the
provider and verifies that the response status code is 200.

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.

The use of the `given` method allows the consumer to define the state of the
provider before the interaction. In this case, the provider is in a state
where the user exists and can be retrieved. By contrast, the same HTTP
request with a different `given` state is expected to return a 404 status
code as shown in
[`test_get_non_existent_user`](#test_get_non_existent_user).
"""
expected_response_code = 200
expected: Dict[str, Any] = {
"id": 123,
"name": "Verna Hampton",
"created_on": {
# This structure is using the Integration JSON format as described
# in the link below. The preview of V3 currently does not have
# built-in support for matchers and generators, though this is on
# the roadmap and will be available before the final release.
#
# <https://docs.pact.io/implementation_guides/rust/pact_ffi/integrationjson>
"pact:matcher:type": "regex",
"regex": r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(Z|(\+|-)\d{2}:\d{2})",
"value": datetime.now(tz=timezone.utc).isoformat(),
},
}
(
pact.upon_receiving("a request for user information")
.given("user exists")
.with_request(method="GET", path="/users/123")
.will_respond_with(200)
.with_body(json.dumps(expected))
)

with pact.serve() as srv:
response = requests.get(f"{srv.url}/users/123", timeout=5)

assert response.status_code == expected_response_code
assert expected["name"] == "Verna Hampton"
datetime.fromisoformat(expected["created_on"]["value"])


def test_get_non_existent_user(pact: Pact) -> None:
"""
Test the GET request for retrieving user information.

This test defines the expected interaction for a GET request to retrieve
user information when that user does not exist in the provider's database.
It is the counterpart to the
[`test_get_existing_user`](#test_get_existing_user) and showcases how the
same request can have different responses based on the provider's state.

It is up to the specific use case to determine whether negative scenarios
should be tested, and to what extent. Certain common negative scenarios
include testing for non-existent resources, unauthorized access attempts may
be useful to ensure that the consumer handles these cases correctly; but it
is generally infeasible to test all possible negative scenarios.
"""
expected_response_code = 404
(
pact.upon_receiving("a request for user information")
.given("user doesn't exists")
.with_request(method="GET", path="/users/2")
.will_respond_with(404)
)

with pact.serve() as srv:
response = requests.get(f"{srv.url}/users/2", timeout=5)

assert response.status_code == expected_response_code
Loading
Loading