Skip to content

Commit 7ad29bd

Browse files
committed
chore(tests): use long-lived pact broker
The initial implementation of the compatibility suite spun up and down the Pact Broker for each scenario, which also resulting in flaky tests in CI. This refactor uses a session pytest fixture which will spin up the broker once, and keep re-using it. Functionality to 'reset' the broker between tests has also been added. Signed-off-by: JP-Ellis <josh@jpellis.me>
1 parent 66ee152 commit 7ad29bd

File tree

5 files changed

+154
-14
lines changed

5 files changed

+154
-14
lines changed

.github/workflows/test.yml

+61-4
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,30 @@ env:
1818
HATCH_VERBOSE: 1
1919

2020
jobs:
21-
test:
21+
test-container:
2222
name: >-
2323
Tests py${{ matrix.python-version }} on ${{ matrix.os }}
2424
2525
runs-on: ${{ matrix.os }}
2626
continue-on-error: ${{ matrix.experimental }}
2727

28+
services:
29+
broker:
30+
image: pactfoundation/pact-broker:latest@sha256:8f10947f230f661ef21f270a4abcf53214ba27cd68063db81de555fcd93e07dd
31+
ports:
32+
- "9292:9292"
33+
env:
34+
# Basic auth credentials for the Broker
35+
PACT_BROKER_ALLOW_PUBLIC_READ: "true"
36+
PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker
37+
PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker
38+
# Database
39+
PACT_BROKER_DATABASE_URL: sqlite:////tmp/pact_broker.sqlite
40+
2841
strategy:
2942
fail-fast: false
3043
matrix:
31-
os: [ubuntu-latest, windows-latest, macos-latest]
44+
os: [ubuntu-latest]
3245
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
3346
experimental: [false]
3447
include:
@@ -51,8 +64,20 @@ jobs:
5164
- name: Install Hatch
5265
run: pip install --upgrade hatch
5366

67+
- name: Ensure broker is live
68+
run: |
69+
i=0
70+
until curl -sSf http://localhost:9292/diagnostic/status/heartbeat; do
71+
i=$((i+1))
72+
if [ $i -gt 120 ]; then
73+
echo "Broker failed to start"
74+
exit 1
75+
fi
76+
sleep 1
77+
done
78+
5479
- name: Run tests
55-
run: hatch run test
80+
run: hatch run test --broker-url=http://pactbroker:pactbroker@localhost:9292 --container
5681

5782
- name: Upload coverage
5883
# TODO: Configure code coverage monitoring
@@ -61,12 +86,44 @@ jobs:
6186
with:
6287
token: ${{ secrets.CODECOV_TOKEN }}
6388

89+
test-no-container:
90+
name: >-
91+
Tests py${{ matrix.python-version }} on ${{ matrix.os }}
92+
93+
runs-on: ${{ matrix.os }}
94+
continue-on-error: ${{ matrix.experimental }}
95+
96+
strategy:
97+
fail-fast: false
98+
matrix:
99+
os: [windows-latest, macos-latest]
100+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
101+
experimental: [false]
102+
103+
steps:
104+
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
105+
with:
106+
submodules: true
107+
108+
- name: Set up Python ${{ matrix.python-version }}
109+
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5
110+
with:
111+
python-version: ${{ matrix.python-version }}
112+
cache: pip
113+
114+
- name: Install Hatch
115+
run: pip install --upgrade hatch
116+
117+
- name: Run tests
118+
run: hatch run test
119+
64120
test-conlusion:
65121
name: Test matrix complete
66122

67123
runs-on: ubuntu-latest
68124
needs:
69-
- test
125+
- test-container
126+
- test-no-container
70127

71128
steps:
72129
- run: echo "Test matrix completed successfully."

conftest.py

+13
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,16 @@ def pytest_addoption(parser: pytest.Parser) -> None:
1919
),
2020
type=str,
2121
)
22+
parser.addoption(
23+
"--container",
24+
action="store_true",
25+
help="Run tests using a container",
26+
)
27+
28+
29+
def pytest_runtest_setup(item: pytest.Item) -> None:
30+
"""
31+
Hook into the setup phase of tests.
32+
"""
33+
if "container" in item.keywords and not item.config.getoption("--container"):
34+
pytest.skip("need --container to run this test")

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s"
193193
log_date_format = "%H:%M:%S"
194194

195195
markers = [
196+
# Marker for tests that require a container
197+
"container",
198+
196199
# Markers for the compatibility suite
197200
"consumer",
198201
"provider",

tests/v3/compatibility_suite/test_v1_provider.py

+58-10
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
import subprocess
1414
import sys
1515
import time
16+
from contextvars import ContextVar
1617
from pathlib import Path
1718
from threading import Thread
18-
from typing import Any, Generator, NoReturn
19+
from typing import Any, Generator, NoReturn, Union
1920

2021
import pytest
2122
import requests
@@ -34,13 +35,52 @@
3435

3536
logger = logging.getLogger(__name__)
3637

38+
reset_broker_var = ContextVar("reset_broker", default=True)
39+
"""
40+
This context variable is used to determine whether the Pact broker should be
41+
cleaned up. It is used to ensure that the broker is only cleaned up once, even
42+
if a step is run multiple times.
43+
44+
All scenarios which make use of the Pact broker should set this to `True` at the
45+
start of the scenario.
46+
"""
47+
3748

3849
@pytest.fixture()
3950
def verifier() -> Verifier:
4051
"""Return a new Verifier."""
4152
return Verifier()
4253

4354

55+
@pytest.fixture(scope="session")
56+
def broker_url(request: pytest.FixtureRequest) -> Generator[URL, Any, None]:
57+
"""
58+
Fixture to run the Pact broker.
59+
60+
This inspects whether the `--broker-url` option has been given. If it has,
61+
it is assumed that the broker is already running and simply returns the
62+
given URL.
63+
64+
Otherwise, the Pact broker is started in a container. The URL of the
65+
containerised broker is then returned.
66+
"""
67+
broker_url: Union[str, None] = request.config.getoption("--broker-url")
68+
69+
# If we have been given a broker URL, there's nothing more to do here and we
70+
# can return early.
71+
if broker_url:
72+
yield URL(broker_url)
73+
return
74+
75+
with DockerCompose(
76+
Path(__file__).parent / "util",
77+
compose_file_name="pact-broker.yml",
78+
pull=True,
79+
) as _:
80+
yield URL("http://pactbroker:pactbroker@localhost:9292")
81+
return
82+
83+
4484
################################################################################
4585
## Scenario
4686
################################################################################
@@ -70,36 +110,44 @@ def test_incorrect_request_is_made_to_provider() -> None:
70110
"""Incorrect request is made to provider."""
71111

72112

113+
@pytest.mark.container()
73114
@scenario(
74115
"definition/features/V1/http_provider.feature",
75116
"Verifying a simple HTTP request via a Pact broker",
76117
)
77118
def test_verifying_a_simple_http_request_via_a_pact_broker() -> None:
78119
"""Verifying a simple HTTP request via a Pact broker."""
120+
reset_broker_var.set(True) # noqa: FBT003
79121

80122

123+
@pytest.mark.container()
81124
@scenario(
82125
"definition/features/V1/http_provider.feature",
83126
"Verifying a simple HTTP request via a Pact broker with publishing results enabled",
84127
)
85128
def test_verifying_a_simple_http_request_via_a_pact_broker_with_publishing() -> None:
86129
"""Verifying a simple HTTP request via a Pact broker with publishing."""
130+
reset_broker_var.set(True) # noqa: FBT003
87131

88132

133+
@pytest.mark.container()
89134
@scenario(
90135
"definition/features/V1/http_provider.feature",
91136
"Verifying multiple Pact files via a Pact broker",
92137
)
93138
def test_verifying_multiple_pact_files_via_a_pact_broker() -> None:
94139
"""Verifying multiple Pact files via a Pact broker."""
140+
reset_broker_var.set(True) # noqa: FBT003
95141

96142

143+
@pytest.mark.container()
97144
@scenario(
98145
"definition/features/V1/http_provider.feature",
99146
"Incorrect request is made to provider via a Pact broker",
100147
)
101148
def test_incorrect_request_is_made_to_provider_via_a_pact_broker() -> None:
102149
"""Incorrect request is made to provider via a Pact broker."""
150+
reset_broker_var.set(True) # noqa: FBT003
103151

104152

105153
@scenario(
@@ -475,6 +523,7 @@ def a_pact_file_for_interaction_is_to_be_verified(
475523
)
476524
def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker(
477525
interaction_definitions: dict[int, InteractionDefinition],
526+
broker_url: URL,
478527
verifier: Verifier,
479528
interaction: int,
480529
temp_dir: Path,
@@ -494,15 +543,14 @@ def a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker(
494543
pacts_dir.mkdir(exist_ok=True, parents=True)
495544
pact.write_file(pacts_dir)
496545

497-
with DockerCompose(
498-
Path(__file__).parent / "util",
499-
compose_file_name="pact-broker.yml",
500-
pull=True,
501-
) as _:
502-
pact_broker = PactBroker(URL("http://pactbroker:pactbroker@localhost:9292"))
503-
pact_broker.publish(pacts_dir)
504-
verifier.broker_source(pact_broker.url)
505-
yield pact_broker
546+
pact_broker = PactBroker(broker_url)
547+
if reset_broker_var.get():
548+
logger.debug("Resetting Pact broker")
549+
pact_broker.reset()
550+
reset_broker_var.set(False) # noqa: FBT003
551+
pact_broker.publish(pacts_dir)
552+
verifier.broker_source(pact_broker.url)
553+
yield pact_broker
506554

507555

508556
@given("publishing of verification results is enabled")

tests/v3/compatibility_suite/util/provider.py

+19
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,25 @@ def _install(self) -> None:
299299
msg = "pact-broker not found"
300300
raise NotImplementedError(msg)
301301

302+
def reset(self) -> None:
303+
"""
304+
Reset the Pact Broker.
305+
306+
This function will reset the Pact Broker by deleting all pacts and
307+
verification results.
308+
"""
309+
requests.delete(
310+
str(
311+
self.url
312+
/ "integrations"
313+
/ "provider"
314+
/ self.provider
315+
/ "consumer"
316+
/ self.consumer
317+
),
318+
timeout=2,
319+
)
320+
302321
def publish(self, directory: Path | str, version: str | None = None) -> None:
303322
"""
304323
Publish the interactions to the Pact Broker.

0 commit comments

Comments
 (0)