Skip to content

Commit

Permalink
Add pytest_before_assert hook.
Browse files Browse the repository at this point in the history
This hook will run before every assert, in contrast to the
pytest_assertrepr_compare hook which only runs for failing asserts.

As of now, no parameters are passed to the hook.

We use this hook to print the assertion code of passed assertions to
collect test evidence. This is done using inspect::

    inspect.stack()[7].code_context[0].strip()

Signed-off-by: Schweizer, Robert <rschweizer@definiens.com>
  • Loading branch information
Schweizer, Robert committed Sep 27, 2018
1 parent 5d8467b commit 21d056a
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 11 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -179,6 +179,7 @@ Raphael Pierzina
Raquel Alegre
Ravi Chandra
Roberto Polli
Robert Schweizer
Romain Dorgueil
Roman Bolshakov
Ronny Pfannschmidt
Expand Down
27 changes: 27 additions & 0 deletions doc/en/assert.rst
Expand Up @@ -255,6 +255,33 @@ the conftest file::
.. _assert-details:
.. _`assert introspection`:

Collecting information about passing assertions
-----------------------------------------------

The ``pytest_assertrepr_compare`` hook only runs for failing assertions. Information
about passing assertions can be collected with the ``pytest_before_assert`` hook.

.. autofunction:: _pytest.hookspec.pytest_before_assert
:noindex:

For example, to report every encountered assertion, the following hook
needs to be added to :ref:`conftest.py <conftest.py>`::

# content of conftest.py
def pytest_before_assert():
print("Before-assert hook is executed.")

now, given this test module::

# content of test_sample.py
def test_answer():
assert 1 == 1

the following stdout is captured, e.g. in an HTML report::

----------------------------- Captured stdout call -----------------------------
Before-assert hook is executed.

Advanced assertion introspection
----------------------------------

Expand Down
5 changes: 3 additions & 2 deletions doc/en/reference.rst
Expand Up @@ -619,9 +619,10 @@ test execution:

.. autofunction:: pytest_runtest_logreport

You can also use this hook to customize assertion representation for some
types:
You can also use these hooks to output assertion information or customize
assertion representation for some types:

.. autofunction:: pytest_before_assert
.. autofunction:: pytest_assertrepr_compare


Expand Down
17 changes: 12 additions & 5 deletions src/_pytest/assertion/__init__.py
Expand Up @@ -98,12 +98,14 @@ def pytest_collection(session):


def pytest_runtest_setup(item):
"""Setup the pytest_assertrepr_compare hook
"""Setup the pytest_assertrepr_compare and pytest_before_assert hooks.
The newinterpret and rewrite modules will use util._reprcompare if
it exists to use custom reporting via the
pytest_assertrepr_compare hook. This sets up this custom
comparison for the test.
The rewrite module will use util._reprcompare and util._before_assert
if they exist to enable custom reporting. Comparison representation is
customized via the pytest_assertrepr_compare hook. Before every assert,
the pytest_before_assert hook is run.
This sets up the custom assert hooks for the test.
"""

def callbinrepr(op, left, right):
Expand Down Expand Up @@ -133,11 +135,16 @@ def callbinrepr(op, left, right):
res = res.replace("%", "%%")
return res

def call_before_assert():
item.ihook.pytest_before_assert(config=item.config)

util._reprcompare = callbinrepr
util._before_assert = call_before_assert


def pytest_runtest_teardown(item):
util._reprcompare = None
util._before_assert = None


def pytest_sessionfinish(session):
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/assertion/rewrite.py
Expand Up @@ -533,6 +533,11 @@ def _call_reprcompare(ops, results, expls, each_obj):
return expl


def _call_before_assert():
if util._before_assert is not None:
util._before_assert()


unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}

binop_map = {
Expand Down Expand Up @@ -822,6 +827,8 @@ def visit_Assert(self, assert_):
self.stack = []
self.on_failure = []
self.push_format_context()
# Run before assert hook.
self.statements.append(ast.Expr(self.helper("call_before_assert")))
# Rewrite assert into a bunch of statements.
top_condition, explanation = self.visit(assert_.test)
# Create failure message.
Expand Down
1 change: 1 addition & 0 deletions src/_pytest/assertion/util.py
Expand Up @@ -14,6 +14,7 @@
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare = None
_before_assert = None


# the re-encoding is needed for python2 repr
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/hookspec.py
Expand Up @@ -473,6 +473,13 @@ def pytest_assertrepr_compare(config, op, left, right):
"""


def pytest_before_assert(config):
""" called before every assertion is evaluated.
:param _pytest.config.Config config: pytest config object
"""


# -------------------------------------------------------------------------
# hooks for influencing reporting (invoked from _pytest_terminal)
# -------------------------------------------------------------------------
Expand Down
42 changes: 38 additions & 4 deletions testing/test_assertion.py
Expand Up @@ -285,7 +285,7 @@ def test_register_assert_rewrite_checks_types(self):
)


class TestBinReprIntegration(object):
class TestAssertHooksIntegration(object):
def test_pytest_assertrepr_compare_called(self, testdir):
testdir.makeconftest(
"""
Expand All @@ -310,6 +310,30 @@ def test_check(list):
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])

def test_pytest_before_assert_called(self, testdir):
testdir.makeconftest(
"""
import pytest
values = []
def pytest_before_assert():
values.append(True)
@pytest.fixture
def list(request):
return values
"""
)
testdir.makepyfile(
"""
def test_hello():
assert 0 == 1
def test_check(list):
assert list == [True, True] # The hook is run before the assert
"""
)
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])


def callequal(left, right, verbose=False):
config = mock_config()
Expand Down Expand Up @@ -824,27 +848,37 @@ def test_hello():
)


def test_assertrepr_loaded_per_dir(testdir):
def test_assert_hooks_loaded_per_dir(testdir):
testdir.makepyfile(test_base=["def test_base(): assert 1 == 2"])
a = testdir.mkdir("a")
a_test = a.join("test_a.py")
a_test.write("def test_a(): assert 1 == 2")
a_conftest = a.join("conftest.py")
a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]')
a_conftest.write(
'def pytest_before_assert(): print("assert hook a")\n'
+ 'def pytest_assertrepr_compare(): return ["summary a"]'
)
b = testdir.mkdir("b")
b_test = b.join("test_b.py")
b_test.write("def test_b(): assert 1 == 2")
b_conftest = b.join("conftest.py")
b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]')
b_conftest.write(
'def pytest_before_assert(): print("assert hook b")\n'
+ 'def pytest_assertrepr_compare(): return ["summary b"]'
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(
[
"*def test_base():*",
"*E*assert 1 == 2*",
"*def test_a():*",
"*E*assert summary a*",
"*Captured stdout call*",
"*assert hook a*",
"*def test_b():*",
"*E*assert summary b*",
"*Captured stdout call*",
"*assert hook b*",
]
)

Expand Down

0 comments on commit 21d056a

Please sign in to comment.