Skip to content

Commit 21d056a

Browse files
author
Schweizer, Robert
committed
Add pytest_before_assert hook.
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 <[email protected]>
1 parent 5d8467b commit 21d056a

File tree

8 files changed

+96
-11
lines changed

8 files changed

+96
-11
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ Raphael Pierzina
179179
Raquel Alegre
180180
Ravi Chandra
181181
Roberto Polli
182+
Robert Schweizer
182183
Romain Dorgueil
183184
Roman Bolshakov
184185
Ronny Pfannschmidt

doc/en/assert.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,33 @@ the conftest file::
255255
.. _assert-details:
256256
.. _`assert introspection`:
257257

258+
Collecting information about passing assertions
259+
-----------------------------------------------
260+
261+
The ``pytest_assertrepr_compare`` hook only runs for failing assertions. Information
262+
about passing assertions can be collected with the ``pytest_before_assert`` hook.
263+
264+
.. autofunction:: _pytest.hookspec.pytest_before_assert
265+
:noindex:
266+
267+
For example, to report every encountered assertion, the following hook
268+
needs to be added to :ref:`conftest.py <conftest.py>`::
269+
270+
# content of conftest.py
271+
def pytest_before_assert():
272+
print("Before-assert hook is executed.")
273+
274+
now, given this test module::
275+
276+
# content of test_sample.py
277+
def test_answer():
278+
assert 1 == 1
279+
280+
the following stdout is captured, e.g. in an HTML report::
281+
282+
----------------------------- Captured stdout call -----------------------------
283+
Before-assert hook is executed.
284+
258285
Advanced assertion introspection
259286
----------------------------------
260287

doc/en/reference.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,10 @@ test execution:
619619

620620
.. autofunction:: pytest_runtest_logreport
621621

622-
You can also use this hook to customize assertion representation for some
623-
types:
622+
You can also use these hooks to output assertion information or customize
623+
assertion representation for some types:
624624

625+
.. autofunction:: pytest_before_assert
625626
.. autofunction:: pytest_assertrepr_compare
626627

627628

src/_pytest/assertion/__init__.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,14 @@ def pytest_collection(session):
9898

9999

100100
def pytest_runtest_setup(item):
101-
"""Setup the pytest_assertrepr_compare hook
101+
"""Setup the pytest_assertrepr_compare and pytest_before_assert hooks.
102102
103-
The newinterpret and rewrite modules will use util._reprcompare if
104-
it exists to use custom reporting via the
105-
pytest_assertrepr_compare hook. This sets up this custom
106-
comparison for the test.
103+
The rewrite module will use util._reprcompare and util._before_assert
104+
if they exist to enable custom reporting. Comparison representation is
105+
customized via the pytest_assertrepr_compare hook. Before every assert,
106+
the pytest_before_assert hook is run.
107+
108+
This sets up the custom assert hooks for the test.
107109
"""
108110

109111
def callbinrepr(op, left, right):
@@ -133,11 +135,16 @@ def callbinrepr(op, left, right):
133135
res = res.replace("%", "%%")
134136
return res
135137

138+
def call_before_assert():
139+
item.ihook.pytest_before_assert(config=item.config)
140+
136141
util._reprcompare = callbinrepr
142+
util._before_assert = call_before_assert
137143

138144

139145
def pytest_runtest_teardown(item):
140146
util._reprcompare = None
147+
util._before_assert = None
141148

142149

143150
def pytest_sessionfinish(session):

src/_pytest/assertion/rewrite.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,11 @@ def _call_reprcompare(ops, results, expls, each_obj):
533533
return expl
534534

535535

536+
def _call_before_assert():
537+
if util._before_assert is not None:
538+
util._before_assert()
539+
540+
536541
unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
537542

538543
binop_map = {
@@ -822,6 +827,8 @@ def visit_Assert(self, assert_):
822827
self.stack = []
823828
self.on_failure = []
824829
self.push_format_context()
830+
# Run before assert hook.
831+
self.statements.append(ast.Expr(self.helper("call_before_assert")))
825832
# Rewrite assert into a bunch of statements.
826833
top_condition, explanation = self.visit(assert_.test)
827834
# Create failure message.

src/_pytest/assertion/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# loaded and in turn call the hooks defined here as part of the
1515
# DebugInterpreter.
1616
_reprcompare = None
17+
_before_assert = None
1718

1819

1920
# the re-encoding is needed for python2 repr

src/_pytest/hookspec.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,13 @@ def pytest_assertrepr_compare(config, op, left, right):
473473
"""
474474

475475

476+
def pytest_before_assert(config):
477+
""" called before every assertion is evaluated.
478+
479+
:param _pytest.config.Config config: pytest config object
480+
"""
481+
482+
476483
# -------------------------------------------------------------------------
477484
# hooks for influencing reporting (invoked from _pytest_terminal)
478485
# -------------------------------------------------------------------------

testing/test_assertion.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def test_register_assert_rewrite_checks_types(self):
285285
)
286286

287287

288-
class TestBinReprIntegration(object):
288+
class TestAssertHooksIntegration(object):
289289
def test_pytest_assertrepr_compare_called(self, testdir):
290290
testdir.makeconftest(
291291
"""
@@ -310,6 +310,30 @@ def test_check(list):
310310
result = testdir.runpytest("-v")
311311
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])
312312

313+
def test_pytest_before_assert_called(self, testdir):
314+
testdir.makeconftest(
315+
"""
316+
import pytest
317+
values = []
318+
def pytest_before_assert():
319+
values.append(True)
320+
321+
@pytest.fixture
322+
def list(request):
323+
return values
324+
"""
325+
)
326+
testdir.makepyfile(
327+
"""
328+
def test_hello():
329+
assert 0 == 1
330+
def test_check(list):
331+
assert list == [True, True] # The hook is run before the assert
332+
"""
333+
)
334+
result = testdir.runpytest("-v")
335+
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])
336+
313337

314338
def callequal(left, right, verbose=False):
315339
config = mock_config()
@@ -824,27 +848,37 @@ def test_hello():
824848
)
825849

826850

827-
def test_assertrepr_loaded_per_dir(testdir):
851+
def test_assert_hooks_loaded_per_dir(testdir):
828852
testdir.makepyfile(test_base=["def test_base(): assert 1 == 2"])
829853
a = testdir.mkdir("a")
830854
a_test = a.join("test_a.py")
831855
a_test.write("def test_a(): assert 1 == 2")
832856
a_conftest = a.join("conftest.py")
833-
a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]')
857+
a_conftest.write(
858+
'def pytest_before_assert(): print("assert hook a")\n'
859+
+ 'def pytest_assertrepr_compare(): return ["summary a"]'
860+
)
834861
b = testdir.mkdir("b")
835862
b_test = b.join("test_b.py")
836863
b_test.write("def test_b(): assert 1 == 2")
837864
b_conftest = b.join("conftest.py")
838-
b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]')
865+
b_conftest.write(
866+
'def pytest_before_assert(): print("assert hook b")\n'
867+
+ 'def pytest_assertrepr_compare(): return ["summary b"]'
868+
)
839869
result = testdir.runpytest()
840870
result.stdout.fnmatch_lines(
841871
[
842872
"*def test_base():*",
843873
"*E*assert 1 == 2*",
844874
"*def test_a():*",
845875
"*E*assert summary a*",
876+
"*Captured stdout call*",
877+
"*assert hook a*",
846878
"*def test_b():*",
847879
"*E*assert summary b*",
880+
"*Captured stdout call*",
881+
"*assert hook b*",
848882
]
849883
)
850884

0 commit comments

Comments
 (0)