From 04f09729abbf0b8e75850495a0041b8edb3c3ea9 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 17 Aug 2019 15:41:02 +0100 Subject: [PATCH 1/9] Add hook which is called before every test --- tavern/testutils/pytesthook/__init__.py | 4 ++-- tavern/testutils/pytesthook/hooks.py | 7 +++++++ tavern/testutils/pytesthook/item.py | 13 +++++++++++-- tavern/testutils/pytesthook/newhooks.py | 19 +++++++++++++++++++ tests/unit/conftest.py | 2 ++ 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tavern/testutils/pytesthook/newhooks.py diff --git a/tavern/testutils/pytesthook/__init__.py b/tavern/testutils/pytesthook/__init__.py index 5028b0b5..cb66a34d 100644 --- a/tavern/testutils/pytesthook/__init__.py +++ b/tavern/testutils/pytesthook/__init__.py @@ -1,4 +1,4 @@ -from .hooks import pytest_collect_file, pytest_addoption +from .hooks import pytest_collect_file, pytest_addoption, pytest_addhooks from .util import add_parser_options -__all__ = ["pytest_addoption", "pytest_collect_file", "add_parser_options"] +__all__ = ["pytest_addoption", "pytest_collect_file", "pytest_addhooks", "add_parser_options"] diff --git a/tavern/testutils/pytesthook/hooks.py b/tavern/testutils/pytesthook/hooks.py index 4d991dd1..d45f477c 100644 --- a/tavern/testutils/pytesthook/hooks.py +++ b/tavern/testutils/pytesthook/hooks.py @@ -54,3 +54,10 @@ def pytest_collect_file(parent, path): return YamlFile(path, parent) return None + + +def pytest_addhooks(pluginmanager): + """Add our custom tavern hooks""" + from . import newhooks + + pluginmanager.add_hookspecs(newhooks) diff --git a/tavern/testutils/pytesthook/item.py b/tavern/testutils/pytesthook/item.py index b6f6d376..3676f6d0 100644 --- a/tavern/testutils/pytesthook/item.py +++ b/tavern/testutils/pytesthook/item.py @@ -139,6 +139,8 @@ def runtest(self): load_plugins(self.global_cfg) + self.global_cfg["tavern_internal"] = {"pytest_hook_caller": self.config.hook} + # INTERNAL # NOTE - now that we can 'mark' tests, we could use pytest.mark.xfail # instead. This doesn't differentiate between an error in verification @@ -146,11 +148,18 @@ def runtest(self): xfail = self.spec.get("_xfail", False) try: - verify_tests(self.spec) - fixture_values = self._load_fixture_values() self.global_cfg["variables"].update(fixture_values) + self.global_cfg["tavern_internal"][ + "pytest_hook_caller" + ].pytest_tavern_before_every_test_run( + test_dict=self.spec, + variables=self.global_cfg["variables"] + ) + + verify_tests(self.spec) + run_test(self.path, self.spec, self.global_cfg) except exceptions.BadSchemaError: if xfail == "verify": diff --git a/tavern/testutils/pytesthook/newhooks.py b/tavern/testutils/pytesthook/newhooks.py new file mode 100644 index 00000000..762724bb --- /dev/null +++ b/tavern/testutils/pytesthook/newhooks.py @@ -0,0 +1,19 @@ +# pylint: disable=unused-argument + + +def pytest_tavern_before_every_test_run(test_dict, variables): + """Called: + + - directly after fixtures are loaded for a test + - directly before verifying the schema of the file + - Before formatting is done on values + - After fixtures have been resolved + - After global configuration has been loaded + - After plugins have been loaded + + Modify the test in-place if you want to do something to it. + + Args: + test_dict (dict): Test to run + variables (dict): Available variables + """ diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3a6a1a50..6c0d7bd8 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,3 +1,4 @@ +from mock import Mock import pytest @@ -13,6 +14,7 @@ def fix_example_includes(): }, "backends": {"mqtt": "paho-mqtt", "http": "requests"}, "strict": True, + "tavern_internal": {"pytest_hook_caller": Mock()}, } return includes.copy() From cbbd93425b975341d9eda412aaabbe58ef689b09 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 17 Aug 2019 15:42:48 +0100 Subject: [PATCH 2/9] Reformat with black --- tavern/testutils/pytesthook/__init__.py | 7 ++++++- tavern/testutils/pytesthook/item.py | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tavern/testutils/pytesthook/__init__.py b/tavern/testutils/pytesthook/__init__.py index cb66a34d..fc3c6921 100644 --- a/tavern/testutils/pytesthook/__init__.py +++ b/tavern/testutils/pytesthook/__init__.py @@ -1,4 +1,9 @@ from .hooks import pytest_collect_file, pytest_addoption, pytest_addhooks from .util import add_parser_options -__all__ = ["pytest_addoption", "pytest_collect_file", "pytest_addhooks", "add_parser_options"] +__all__ = [ + "pytest_addoption", + "pytest_collect_file", + "pytest_addhooks", + "add_parser_options", +] diff --git a/tavern/testutils/pytesthook/item.py b/tavern/testutils/pytesthook/item.py index 3676f6d0..041625e8 100644 --- a/tavern/testutils/pytesthook/item.py +++ b/tavern/testutils/pytesthook/item.py @@ -154,8 +154,7 @@ def runtest(self): self.global_cfg["tavern_internal"][ "pytest_hook_caller" ].pytest_tavern_before_every_test_run( - test_dict=self.spec, - variables=self.global_cfg["variables"] + test_dict=self.spec, variables=self.global_cfg["variables"] ) verify_tests(self.spec) From 4f5287f83b49b08e3c730b9de76a74c64615b95d Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Wed, 21 Aug 2019 19:13:01 +0100 Subject: [PATCH 3/9] Add utility for calling hooks instead --- tavern/testutils/pytesthook/item.py | 10 ++++++---- tavern/testutils/pytesthook/newhooks.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tavern/testutils/pytesthook/item.py b/tavern/testutils/pytesthook/item.py index 041625e8..c51b9867 100644 --- a/tavern/testutils/pytesthook/item.py +++ b/tavern/testutils/pytesthook/item.py @@ -10,6 +10,7 @@ from tavern.plugins import load_plugins from tavern.schemas.files import verify_tests from tavern.util import exceptions +from testutils.pytesthook.newhooks import call_hook from .error import ReprdError from .util import load_global_cfg @@ -151,10 +152,11 @@ def runtest(self): fixture_values = self._load_fixture_values() self.global_cfg["variables"].update(fixture_values) - self.global_cfg["tavern_internal"][ - "pytest_hook_caller" - ].pytest_tavern_before_every_test_run( - test_dict=self.spec, variables=self.global_cfg["variables"] + call_hook( + self.global_cfg, + "pytest_tavern_before_every_test_run", + test_dict=self.spec, + variables=self.global_cfg["variables"], ) verify_tests(self.spec) diff --git a/tavern/testutils/pytesthook/newhooks.py b/tavern/testutils/pytesthook/newhooks.py index 762724bb..1c965e99 100644 --- a/tavern/testutils/pytesthook/newhooks.py +++ b/tavern/testutils/pytesthook/newhooks.py @@ -1,4 +1,7 @@ # pylint: disable=unused-argument +import logging + +logger = logging.getLogger(__name__) def pytest_tavern_before_every_test_run(test_dict, variables): @@ -17,3 +20,20 @@ def pytest_tavern_before_every_test_run(test_dict, variables): test_dict (dict): Test to run variables (dict): Available variables """ + + +def call_hook(test_block_config, hookname, **kwargs): + """Utility to call the hooks""" + try: + hook = getattr( + test_block_config["tavern_internal"]["pytest_hook_caller"], hookname + ) + except AttributeError: + logger.critical("Error getting tavern hook!") + raise + + try: + hook(**kwargs) + except AttributeError: + logger.error("Error calling tavern hook!") + raise From d28685bb275f64175d4005002e77e4334adee73d Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Wed, 21 Aug 2019 19:24:39 +0100 Subject: [PATCH 4/9] Add hook which is run when a response is received --- tavern/_plugins/mqtt/response.py | 11 +++++++++-- tavern/_plugins/rest/response.py | 8 ++++++++ tavern/testutils/pytesthook/item.py | 2 +- tavern/testutils/pytesthook/newhooks.py | 13 +++++++++++++ tests/unit/response/test_mqtt_response.py | 16 ++++++++-------- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/tavern/_plugins/mqtt/response.py b/tavern/_plugins/mqtt/response.py index 0f233a7c..81aacd0b 100644 --- a/tavern/_plugins/mqtt/response.py +++ b/tavern/_plugins/mqtt/response.py @@ -6,6 +6,7 @@ from tavern.response.base import BaseResponse from tavern.util.dict_util import check_keys_match_recursive from tavern.util.loader import ANYTHING +from tavern.testutils.pytesthook.newhooks import call_hook try: LoadException = json.decoder.JSONDecodeError @@ -18,10 +19,9 @@ class MQTTResponse(BaseResponse): def __init__(self, client, name, expected, test_block_config): - # pylint: disable=unused-argument - super(MQTTResponse, self).__init__() + self.test_block_config = test_block_config self.name = name self._check_for_validate_functions(expected.get("payload", {})) @@ -78,6 +78,13 @@ def _await_response(self): # timed out break + call_hook( + self.test_block_config, + "pytest_tavern_after_every_response", + expected=self.expected, + response=msg, + ) + self.received_messages.append(msg) msg.payload = msg.payload.decode("utf8") diff --git a/tavern/_plugins/rest/response.py b/tavern/_plugins/rest/response.py index 3f854d09..f7cdb83f 100644 --- a/tavern/_plugins/rest/response.py +++ b/tavern/_plugins/rest/response.py @@ -9,6 +9,7 @@ from requests.status_codes import _codes +from tavern.testutils.pytesthook.newhooks import call_hook from tavern.schemas.extensions import get_wrapped_response_function from tavern.util.dict_util import recurse_access_key, deep_dict_merge from tavern.util import exceptions @@ -149,6 +150,13 @@ def verify(self, response): """ self._verbose_log_response(response) + call_hook( + self.test_block_config, + "pytest_tavern_after_every_response", + expected=self.expected, + response=response, + ) + self.response = response self.status_code = response.status_code diff --git a/tavern/testutils/pytesthook/item.py b/tavern/testutils/pytesthook/item.py index c51b9867..2b0c1ebe 100644 --- a/tavern/testutils/pytesthook/item.py +++ b/tavern/testutils/pytesthook/item.py @@ -10,7 +10,7 @@ from tavern.plugins import load_plugins from tavern.schemas.files import verify_tests from tavern.util import exceptions -from testutils.pytesthook.newhooks import call_hook +from tavern.testutils.pytesthook.newhooks import call_hook from .error import ReprdError from .util import load_global_cfg diff --git a/tavern/testutils/pytesthook/newhooks.py b/tavern/testutils/pytesthook/newhooks.py index 1c965e99..c7e6a321 100644 --- a/tavern/testutils/pytesthook/newhooks.py +++ b/tavern/testutils/pytesthook/newhooks.py @@ -22,6 +22,19 @@ def pytest_tavern_before_every_test_run(test_dict, variables): """ +def pytest_tavern_after_every_response(expected, response): + """Called after every _response_ - including MQTT/HTTP/etc + + Note: + - The response object type and the expected dict depends on what plugin you're using, and which kind of response it is! + - MQTT responses will call this hook multiple times if multiple messages are received + + Args: + response (object): Response object. + expected (dict): Response block in stage + """ + + def call_hook(test_block_config, hookname, **kwargs): """Utility to call the hooks""" try: diff --git a/tests/unit/response/test_mqtt_response.py b/tests/unit/response/test_mqtt_response.py index 630359d8..4e7bd4b1 100644 --- a/tests/unit/response/test_mqtt_response.py +++ b/tests/unit/response/test_mqtt_response.py @@ -27,7 +27,7 @@ def __init__(self, returned): class TestResponse(object): - def _get_fake_verifier(self, expected, fake_messages): + def _get_fake_verifier(self, expected, fake_messages, includes): """Given a list of messages, return a mocked version of the MQTT response verifier which will take messages off the front of this list as if they were published @@ -51,16 +51,16 @@ def inner(timeout): fake_client = Mock(spec=MQTTClient, message_received=yield_all_messages()) - return MQTTResponse(fake_client, "Test stage", expected, {}) + return MQTTResponse(fake_client, "Test stage", expected, includes) - def test_message_on_same_topic_fails(self): + def test_message_on_same_topic_fails(self, includes): """Correct topic, wrong message""" expected = {"topic": "/a/b/c", "payload": "hello"} fake_message = FakeMessage({"topic": "/a/b/c", "payload": "goodbye"}) - verifier = self._get_fake_verifier(expected, [fake_message]) + verifier = self._get_fake_verifier(expected, [fake_message], includes) with pytest.raises(exceptions.TestFailError): verifier.verify(expected) @@ -68,21 +68,21 @@ def test_message_on_same_topic_fails(self): assert len(verifier.received_messages) == 1 assert verifier.received_messages[0].topic == fake_message.topic - def test_correct_message(self): + def test_correct_message(self, includes): """Both correct matches""" expected = {"topic": "/a/b/c", "payload": "hello"} fake_message = FakeMessage(expected) - verifier = self._get_fake_verifier(expected, [fake_message]) + verifier = self._get_fake_verifier(expected, [fake_message], includes) verifier.verify(expected) assert len(verifier.received_messages) == 1 assert verifier.received_messages[0].topic == fake_message.topic - def test_correct_message_eventually(self): + def test_correct_message_eventually(self, includes): """One wrong messge, then the correct one""" expected = {"topic": "/a/b/c", "payload": "hello"} @@ -91,7 +91,7 @@ def test_correct_message_eventually(self): fake_message_bad = FakeMessage({"topic": "/a/b/c", "payload": "goodbye"}) verifier = self._get_fake_verifier( - expected, [fake_message_bad, fake_message_good] + expected, [fake_message_bad, fake_message_good], includes ) verifier.verify(expected) From 362d1586eefe5aadfe5a88904946151cf3215d87 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Thu, 22 Aug 2019 17:56:01 +0100 Subject: [PATCH 5/9] Add extra examples folder for hooks --- example/hooks/Dockerfile | 9 +++ example/hooks/conftest.py | 16 +++++ example/hooks/docker-compose.yaml | 10 +++ example/hooks/server.py | 21 ++++++ example/hooks/test_server.tavern.yaml | 94 +++++++++++++++++++++++++ tavern/testutils/pytesthook/newhooks.py | 2 +- tox-integration.ini | 7 +- 7 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 example/hooks/Dockerfile create mode 100644 example/hooks/conftest.py create mode 100644 example/hooks/docker-compose.yaml create mode 100644 example/hooks/server.py create mode 100644 example/hooks/test_server.tavern.yaml diff --git a/example/hooks/Dockerfile b/example/hooks/Dockerfile new file mode 100644 index 00000000..57a81fcb --- /dev/null +++ b/example/hooks/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.5-alpine + +RUN pip install flask pyjwt + +COPY server.py / + +ENV FLASK_APP=/server.py + +CMD ["flask", "run", "--host=0.0.0.0"] diff --git a/example/hooks/conftest.py b/example/hooks/conftest.py new file mode 100644 index 00000000..f5c705db --- /dev/null +++ b/example/hooks/conftest.py @@ -0,0 +1,16 @@ +import logging + +import pytest + +logger = logging.getLogger(__name__) + + +@pytest.fixture(autouse=True) +def setup_logging(): + logging.basicConfig(level=logging.INFO) + + +def pytest_tavern_after_every_response(expected, response): + logging.critical(expected) + logging.critical(response) + assert 0 diff --git a/example/hooks/docker-compose.yaml b/example/hooks/docker-compose.yaml new file mode 100644 index 00000000..3d636985 --- /dev/null +++ b/example/hooks/docker-compose.yaml @@ -0,0 +1,10 @@ +--- +version: '2' + +services: + server: + build: + context: . + dockerfile: Dockerfile + ports: + - "5000:5000" diff --git a/example/hooks/server.py b/example/hooks/server.py new file mode 100644 index 00000000..9f965f9b --- /dev/null +++ b/example/hooks/server.py @@ -0,0 +1,21 @@ +from flask import Flask, jsonify, request + + +app = Flask(__name__) + + +@app.route("/double", methods=["POST"]) +def double_number(): + r = request.get_json() + + try: + number = r["number"] + except (KeyError, TypeError): + return jsonify({"error": "no number passed"}), 400 + + try: + double = int(number) * 2 + except ValueError: + return jsonify({"error": "a number was not passed"}), 400 + + return jsonify({"double": double}), 200 diff --git a/example/hooks/test_server.tavern.yaml b/example/hooks/test_server.tavern.yaml new file mode 100644 index 00000000..43e7005a --- /dev/null +++ b/example/hooks/test_server.tavern.yaml @@ -0,0 +1,94 @@ +--- + +test_name: Make sure server doubles number properly + +stages: + - name: Make sure number is returned correctly + request: + url: http://localhost:5000/double + json: + number: 5 + method: POST + headers: + content-type: application/json + response: + status_code: 200 + body: + double: 10 + +--- + +test_name: Check invalid inputs are handled + +stages: + - name: Make sure invalid numbers don't cause an error + request: + url: http://localhost:5000/double + json: + number: dkfsd + method: POST + headers: + content-type: application/json + response: + status_code: 400 + body: + error: a number was not passed + + - name: Make sure it raises an error if a number isn't passed + request: + url: http://localhost:5000/double + json: + wrong_key: 5 + method: POST + headers: + content-type: application/json + response: + status_code: 400 + body: + error: no number passed + +--- + +test_name: Make sure server doubles number properly in series + +stages: + - name: Double the initial number + request: + url: http://localhost:5000/double + json: + number: 5 + method: POST + headers: + content-type: application/json + response: + status_code: 200 + save: + body: + value: double + + - name: Double the number again and save the result + request: + url: http://localhost:5000/double + json: + number: !int "{value:d}" + method: POST + headers: + content-type: application/json + response: + status_code: 200 + save: + body: + doubled_value: double + + - name: Make sure number is the same as before + request: + url: http://localhost:5000/double + json: + number: !int "{value:d}" + method: POST + headers: + content-type: application/json + response: + status_code: 200 + body: + double: !int "{doubled_value:d}" diff --git a/tavern/testutils/pytesthook/newhooks.py b/tavern/testutils/pytesthook/newhooks.py index c7e6a321..73241c48 100644 --- a/tavern/testutils/pytesthook/newhooks.py +++ b/tavern/testutils/pytesthook/newhooks.py @@ -48,5 +48,5 @@ def call_hook(test_block_config, hookname, **kwargs): try: hook(**kwargs) except AttributeError: - logger.error("Error calling tavern hook!") + logger.error("Unexpected error calling tavern hook") raise diff --git a/tox-integration.ini b/tox-integration.ini index 44e8ba58..94e60fe4 100644 --- a/tox-integration.ini +++ b/tox-integration.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py34,py35,py36,py37,pypy,pypy3}-{generic,cookies,mqtt,advanced,components,noextra} +envlist = {py27,py34,py35,py36,py37,pypy,pypy3}-{generic,cookies,mqtt,advanced,components,noextra,hooks} skip_missing_interpreters = true [testenv] @@ -13,6 +13,7 @@ changedir = cookies: example/cookies advanced: example/advanced components: example/components + hooks: example/hooks generic: tests/integration noextra: tests/integration deps = @@ -24,10 +25,10 @@ deps = paho-mqtt mqtt: fluent-logger commands = - docker-compose stop +; docker-compose stop docker-compose build docker-compose up -d - python -m pytest --tb=short --tavern-global-cfg={toxinidir}/tests/integration/global_cfg.yaml + python -m pytest --tb=short --tavern-global-cfg={toxinidir}/tests/integration/global_cfg.yaml --tavern-beta-new-traceback cookies: tavern-ci --stdout test_server.tavern.yaml cookies: python -c "from tavern.core import run; exit(run('test_server.tavern.yaml', pytest_args=[]))" From ffbecfa2db239a07a483ca69d7f437e02fa8f6f0 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Thu, 22 Aug 2019 19:58:34 +0100 Subject: [PATCH 6/9] Change hooks example to work properly --- example/hooks/conftest.py | 32 ++++++++++-- example/hooks/test_server.tavern.yaml | 70 ++------------------------- 2 files changed, 32 insertions(+), 70 deletions(-) diff --git a/example/hooks/conftest.py b/example/hooks/conftest.py index f5c705db..08df1761 100644 --- a/example/hooks/conftest.py +++ b/example/hooks/conftest.py @@ -1,9 +1,13 @@ +import os +import tempfile import logging import pytest logger = logging.getLogger(__name__) +name = None + @pytest.fixture(autouse=True) def setup_logging(): @@ -11,6 +15,28 @@ def setup_logging(): def pytest_tavern_after_every_response(expected, response): - logging.critical(expected) - logging.critical(response) - assert 0 + global name + logging.debug(expected) + logging.debug(response) + with open(name, "a") as tfile: + tfile.write("abc\n") + + +@pytest.fixture(autouse=True) +def after_check_result(): + """Create a temporary file for the duration of the test, and make sure the above hook was called""" + global name + with tempfile.NamedTemporaryFile(delete=False) as tfile: + try: + tfile.close() + + name = tfile.name + yield + + with open(tfile.name, "r") as opened: + contents = opened.readlines() + assert len(contents) == 2 + assert all(i.strip() == "abc" for i in contents) + + finally: + os.remove(tfile.name) diff --git a/example/hooks/test_server.tavern.yaml b/example/hooks/test_server.tavern.yaml index 43e7005a..24366099 100644 --- a/example/hooks/test_server.tavern.yaml +++ b/example/hooks/test_server.tavern.yaml @@ -16,79 +16,15 @@ stages: body: double: 10 ---- - -test_name: Check invalid inputs are handled - -stages: - - name: Make sure invalid numbers don't cause an error - request: - url: http://localhost:5000/double - json: - number: dkfsd - method: POST - headers: - content-type: application/json - response: - status_code: 400 - body: - error: a number was not passed - - - name: Make sure it raises an error if a number isn't passed - request: - url: http://localhost:5000/double - json: - wrong_key: 5 - method: POST - headers: - content-type: application/json - response: - status_code: 400 - body: - error: no number passed - ---- - -test_name: Make sure server doubles number properly in series - -stages: - - name: Double the initial number - request: - url: http://localhost:5000/double - json: - number: 5 - method: POST - headers: - content-type: application/json - response: - status_code: 200 - save: - body: - value: double - - - name: Double the number again and save the result - request: - url: http://localhost:5000/double - json: - number: !int "{value:d}" - method: POST - headers: - content-type: application/json - response: - status_code: 200 - save: - body: - doubled_value: double - - - name: Make sure number is the same as before + - name: Make sure number is returned correctly again request: url: http://localhost:5000/double json: - number: !int "{value:d}" + number: 10 method: POST headers: content-type: application/json response: status_code: 200 body: - double: !int "{doubled_value:d}" + double: 20 From 652332d7e185c9dcfd2cba9a81d7ada34b82688b Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Thu, 22 Aug 2019 20:14:02 +0100 Subject: [PATCH 7/9] Add docs for hooks --- docs/source/basics.md | 57 ++++++++++++++++++++++++- tavern/testutils/pytesthook/newhooks.py | 1 - 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/source/basics.md b/docs/source/basics.md index 8f7033a5..251a367f 100644 --- a/docs/source/basics.md +++ b/docs/source/basics.md @@ -1424,7 +1424,7 @@ values. Say we want to convert a value from an included file to an integer: ```yaml request: json: - an_integer: !!int "{my_integer:d}" # Error + # an_integer: !!int "{my_integer:d}" # Error an_integer: !int "{my_integer:d}" # Works ``` @@ -1939,3 +1939,58 @@ There are some limitations on fixtures: - Fixtures should be 'function' or 'session' scoped. 'module' scoped fixtures will raise an error and 'class' scoped fixtures may not behave as you expect. - Parametrizing fixtures does not work - this is a limitation in Pytest. + + +## Hooks + +As well as fixtures as mentioned in the previous section, since version 0.28.0 +there is a couple of hooks which can be used to extract more information from +tests. + +These hooks are used by defining a function with the name of the hook in your +`conftest.py` that take the same arguments _with the same names_ - these hooks +will then be picked up at runtime and called appropriately + +### Before every test run + +This hook is called after fixtures, global configuration, and plugins have been +loaded, but _before_ formatting is done on the test and the schema of the test +is checked. This can be used to 'inject' extra things into the test before it is +run, such as configurations blocks for a plugin, or just for some kind of +logging. + +Args: +- test_dict (dict): Test to run +- variables (dict): Available variables + +Example usage: + +```python +import logging + +def pytest_tavern_before_every_test_run(test_dict, variables): + logging.info("Starting test %s", test_dict["test_name"]) + + variables["extra_var"] = "abc123" +``` + +### After every response + +This hook is called after every _response_ for each _stage_ - this includes HTTP +responses, but also MQTT responses if you are using MQTT. This means if you are +using MQTT it might be called multiple times for each stage! + +Args: +- response (object): Response object. Could be whatever kind of response object + is returned by whatever plugin is used - if using the default HTTP + implementation which uses Requests, this will be a `requests.Response` object. + If using MQTT, this will be a paho-mqtt `Message` object. +- expected (dict): Formatted response block from the stage. + +Example usage: + +```python +def pytest_tavern_after_every_response(expected, response): + with open("logfile.txt", "a") as logfile: + logfile.write("Got response: {}".format(response.json())) +``` diff --git a/tavern/testutils/pytesthook/newhooks.py b/tavern/testutils/pytesthook/newhooks.py index 73241c48..0bd08755 100644 --- a/tavern/testutils/pytesthook/newhooks.py +++ b/tavern/testutils/pytesthook/newhooks.py @@ -10,7 +10,6 @@ def pytest_tavern_before_every_test_run(test_dict, variables): - directly after fixtures are loaded for a test - directly before verifying the schema of the file - Before formatting is done on values - - After fixtures have been resolved - After global configuration has been loaded - After plugins have been loaded From 5c19aa7636783b678a37bd720838f90377bb8d82 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 24 Aug 2019 16:08:16 +0100 Subject: [PATCH 8/9] Note that current hooks are not stable --- docs/source/basics.md | 10 +++++++--- example/hooks/conftest.py | 2 +- tavern/_plugins/mqtt/response.py | 2 +- tavern/_plugins/rest/response.py | 2 +- tavern/testutils/pytesthook/item.py | 2 +- tavern/testutils/pytesthook/newhooks.py | 4 ++-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/source/basics.md b/docs/source/basics.md index 251a367f..4e8c1ca6 100644 --- a/docs/source/basics.md +++ b/docs/source/basics.md @@ -1949,7 +1949,11 @@ tests. These hooks are used by defining a function with the name of the hook in your `conftest.py` that take the same arguments _with the same names_ - these hooks -will then be picked up at runtime and called appropriately +will then be picked up at runtime and called appropriately. + +**NOTE**: These hooks should be considered a 'beta' feature, they are ready to +use but the names and arguments they take should be considered unstable and may +change in a future release (and more may also be added). ### Before every test run @@ -1968,7 +1972,7 @@ Example usage: ```python import logging -def pytest_tavern_before_every_test_run(test_dict, variables): +def pytest_tavern_beta_before_every_test_run(test_dict, variables): logging.info("Starting test %s", test_dict["test_name"]) variables["extra_var"] = "abc123" @@ -1990,7 +1994,7 @@ Args: Example usage: ```python -def pytest_tavern_after_every_response(expected, response): +def pytest_tavern_beta_after_every_response(expected, response): with open("logfile.txt", "a") as logfile: logfile.write("Got response: {}".format(response.json())) ``` diff --git a/example/hooks/conftest.py b/example/hooks/conftest.py index 08df1761..76164c7b 100644 --- a/example/hooks/conftest.py +++ b/example/hooks/conftest.py @@ -14,7 +14,7 @@ def setup_logging(): logging.basicConfig(level=logging.INFO) -def pytest_tavern_after_every_response(expected, response): +def pytest_tavern_beta_after_every_response(expected, response): global name logging.debug(expected) logging.debug(response) diff --git a/tavern/_plugins/mqtt/response.py b/tavern/_plugins/mqtt/response.py index 81aacd0b..e61f1805 100644 --- a/tavern/_plugins/mqtt/response.py +++ b/tavern/_plugins/mqtt/response.py @@ -80,7 +80,7 @@ def _await_response(self): call_hook( self.test_block_config, - "pytest_tavern_after_every_response", + "pytest_tavern_beta_after_every_response", expected=self.expected, response=msg, ) diff --git a/tavern/_plugins/rest/response.py b/tavern/_plugins/rest/response.py index f7cdb83f..8f3759ff 100644 --- a/tavern/_plugins/rest/response.py +++ b/tavern/_plugins/rest/response.py @@ -152,7 +152,7 @@ def verify(self, response): call_hook( self.test_block_config, - "pytest_tavern_after_every_response", + "pytest_tavern_beta_after_every_response", expected=self.expected, response=response, ) diff --git a/tavern/testutils/pytesthook/item.py b/tavern/testutils/pytesthook/item.py index 2b0c1ebe..4cc61ead 100644 --- a/tavern/testutils/pytesthook/item.py +++ b/tavern/testutils/pytesthook/item.py @@ -154,7 +154,7 @@ def runtest(self): call_hook( self.global_cfg, - "pytest_tavern_before_every_test_run", + "pytest_tavern_beta_before_every_test_run", test_dict=self.spec, variables=self.global_cfg["variables"], ) diff --git a/tavern/testutils/pytesthook/newhooks.py b/tavern/testutils/pytesthook/newhooks.py index 0bd08755..078e0263 100644 --- a/tavern/testutils/pytesthook/newhooks.py +++ b/tavern/testutils/pytesthook/newhooks.py @@ -4,7 +4,7 @@ logger = logging.getLogger(__name__) -def pytest_tavern_before_every_test_run(test_dict, variables): +def pytest_tavern_beta_before_every_test_run(test_dict, variables): """Called: - directly after fixtures are loaded for a test @@ -21,7 +21,7 @@ def pytest_tavern_before_every_test_run(test_dict, variables): """ -def pytest_tavern_after_every_response(expected, response): +def pytest_tavern_beta_after_every_response(expected, response): """Called after every _response_ - including MQTT/HTTP/etc Note: From 028dd26915cff61e6bb71446fafcbf54e7606ae0 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 24 Aug 2019 16:09:38 +0100 Subject: [PATCH 9/9] Add hook build to CI --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index cbd38224..3955bb49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -102,6 +102,9 @@ jobs: - python: 3.7 env: TOXENV=py37-advanced TOXCFG=tox-integration.ini stage: full-tests + - python: 3.7 + env: TOXENV=py37-hooks TOXCFG=tox-integration.ini + stage: full-tests - stage: deploy script: skip deploy: