Description
If an exception is thrown after entering the Pact
context, the _interactions
list still contains the expected interaction for that test. This causes all subsequently run tests to fail.
Small test file illustrating this issue:
import os
import pytest
import requests
from pactman import Consumer, Provider
PACTS_DIR = 'pacts'
@pytest.fixture(scope="session")
def pact_session():
if not os.path.exists(PACTS_DIR):
os.mkdir(PACTS_DIR)
return Consumer("TestConsumer").has_pact_with(
Provider("TestProvider"),
pact_dir=PACTS_DIR
)
@pytest.fixture
def pact(pact_session):
# Comment out this line to "fix" interactions not being cleared
# pact_session._interactions[:] = []
return pact_session
def test_1(pact):
expected = {"test": "test_1"}
(
pact.given(None)
.upon_receiving("Test request 1")
.with_request("GET", "/")
.will_respond_with(200, body=expected)
)
with pact:
raise Exception("Simulate fail")
resp = requests.get(pact.uri + "/")
assert resp.status_code == 200
def test_2(pact):
expected = {"test": "test_2"}
(
pact.given(None)
.upon_receiving("Test request 2")
.with_request("POST", "/")
.will_respond_with(200, body=expected)
)
with pact:
resp = requests.post(pact.uri + "/")
assert resp.status_code == 200
def test_3(pact):
expected = {"test": "test_3"}
(
pact.given(None)
.upon_receiving("Test request 3")
.with_request("PATCH", "/")
.will_respond_with(200, body=expected)
)
with pact:
resp = requests.patch(pact.uri + "/")
assert resp.status_code == 200
To Test
Save the above snipped to a file and run with pytest. Note that all three tests fail.
There won't be a generated pact file since no tests passed.
Now, uncomment the line pact_session._interactions[:] = []
, so that the interactions list gets cleared before each test case.
Run the tests, and verify that:
- Two tests pass
- A pact file is generated containing interactions for the passing tests.
Finally, comment out the raise Exception("Simulate fail")
line and run the test again. All three tests will pass, and the generated pact file contains three interactions.
Expected behavior (of unmodified code snipped)
1 test fails, 2 test pass
Actual behavior
3 tests fail
Suggested fix
Either clear the interactions list entirely, or pop the most recent (failed) interaction.
# mock/pact.py
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Handler for exiting a Python context.
Calls the mock service to verify that all interactions occurred as
expected, and has it write out the contracts to disk.
"""
self._enter_count -= 1
if (exc_type, exc_val, exc_tb) != (None, None, None):
# Remove failed interaction
self._interactions.pop(0)
# let the exception go through to the keeper
return
# don't invoke teardown until all interactions for this pact are exited
if self._enter_count:
return
self.verify()
if not self.use_mocking_server and self._auto_mocked:
self.stop_mocking()