Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Plugin how to get Subtest Details? #140

Closed
mkmoisen opened this issue Jul 12, 2024 · 7 comments
Closed

Custom Plugin how to get Subtest Details? #140

mkmoisen opened this issue Jul 12, 2024 · 7 comments

Comments

@mkmoisen
Copy link

I have a custom plugin that takes the results of my tests and saves them to a database.

When I use pytest, the custom plugin doesn't seem to get the subtest details.

Here is an example of the hooks that I'm using.

def pytest_collection_modifyitems(session, config, items):
    for item in items:
        print(item.nodeid, str(item.path), item.parent.name)
 

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield

    report = outcome.get_result()

    print(item.nodeid, report.outcome)

Each of these items is for a top level test, not a subtest.

Is it possible to get access to the subtest level details?

@nicoddemus
Copy link
Member

If you want to access the report, implement the pytest_runtest_logreport hook instead, this is the mechanism plugins use in general to receive test reports (for example the internal terminal plugin implements this hook in order to print test progress).

@mkmoisen
Copy link
Author

@nicoddemus Ok it seems like the hooks like pytest_runtest_logreport will not show SubTestReports when I subclass TestCase and use self.subTest(). It only works when I use pytests style with the subtests fixture.

Here is an example test:

from unittest import TestCase

class TestFoo(TestCase):
    def test_all(self):
        with self.subTest('test_foo'):
            raise Exception('foo')
        with self.subTest('test_bar'):
            raise Exception('bar')


def test_baz(subtests):
    with subtests.test('test_baz_first'):
        raise Exception('first')
    with subtests.test('test_baz_second'):
        raise Exception('second')

And a plugin:

def pytest_runtest_logreport(report):
    print(type(report), report)

Running my test results in:

test_foo.py <class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='setup' outcome='passed'>

.<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='call' outcome='passed'>

<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='teardown' outcome='passed'>

<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='setup' outcome='passed'>

u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_baz_first', kwargs={}))

u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_baz_second', kwargs={}))

.<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='call' outcome='passed'>

<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='teardown' outcome='passed'>

Note how this will only let me get access to subtests created with the pytest subtests fixtures, but it will not let me get access to subclasses of UnitTest that use self.subTest().

Do you have any suggestions?

I have some old code that is using TestCase which I would prefer not to rewrite in the pytest style if possible.

@nicoddemus
Copy link
Member

Not sure, this works for me:

λ pytest .tmp\test_ut_report.py -s
============================================================================================= test session starts ==============================================================================================
platform win32 -- Python 3.12.2, pytest-8.2.2, pluggy-1.5.0
rootdir: e:\projects\pytest-subtests
configfile: pytest.ini
plugins: subtests-0.13.1.dev3+gcbff3e1.d20240713
collected 1 item

.tmp\test_ut_report.py <class '_pytest.reports.TestReport'> .tmp/test_ut_report.py::TestFoo::test_all
u<class 'pytest_subtests.plugin.SubTestReport'> .tmp/test_ut_report.py::TestFoo::test_all
u<class 'pytest_subtests.plugin.SubTestReport'> .tmp/test_ut_report.py::TestFoo::test_all
.<class '_pytest.reports.TestReport'> .tmp/test_ut_report.py::TestFoo::test_all
<class '_pytest.reports.TestReport'> .tmp/test_ut_report.py::TestFoo::test_all

🤔

@mkmoisen
Copy link
Author

@nicoddemus

Interestingly, it was not printing because I was not running with -s flag. But the same is not true when I use a pytest style function with the subtests fixture - it prints with or without the -s flag.

Thanks for your help on this.

If it is convenient, it might be a good idea to somehow make this -s behavior consistent with pytest style functions and UnitTest subclasses.

conftest.py

def pytest_runtest_logreport(report):
    print(type(report), report)

test_foo.py

from unittest import TestCase

def pytest_runtest_logreport(report):
    print(type(report), report)
    print(dir(report))
    print('')

class TestFoo(TestCase):
    def test_all(self):
        with self.subTest('test_foo'):
            raise Exception('foo')
        with self.subTest('test_bar'):
            raise Exception('bar')


def test_baz(subtests):
    with subtests.test('test_baz_first'):
        raise Exception('first')
    with subtests.test('test_baz_second'):
        raise Exception('second')

pytest test_foo.py

sh-4.4$ pytest test_foo.py 
===================================================================================== test session starts ======================================================================================
platform linux -- Python 3.12.4, pytest-8.2.1, pluggy-1.5.0
rootdir: /opt/app/src/foo
plugins: cov-5.0.0, html-4.1.1, metadata-3.1.1, rerunfailures-14.0, subtests-0.13.0, timer-1.0.0, xdist-3.5.0
collected 2 items                                                                                                                                                                              

test_foo.py <class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='setup' outcome='passed'>
.<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='call' outcome='passed'>
<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='teardown' outcome='passed'>
<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='setup' outcome='passed'>
u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_baz_first', kwargs={}))
u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_baz_second', kwargs={}))
.<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='call' outcome='passed'>
<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='teardown' outcome='passed'>

pytest test_foo.py -s

sh-4.4$ pytest test_foo.py -s
===================================================================================== test session starts ======================================================================================
platform linux -- Python 3.12.4, pytest-8.2.1, pluggy-1.5.0
rootdir: /opt/app/src/foo
plugins: cov-5.0.0, html-4.1.1, metadata-3.1.1, rerunfailures-14.0, subtests-0.13.0, timer-1.0.0, xdist-3.5.0
collected 2 items                                                                                                                                                                              

test_foo.py <class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='setup' outcome='passed'>
u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_foo', kwargs={}))
u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_bar', kwargs={}))
.<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='call' outcome='passed'>
<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::TestFoo::test_all' when='teardown' outcome='passed'>
<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='setup' outcome='passed'>
u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_baz_first', kwargs={}))
u<class 'pytest_subtests.plugin.SubTestReport'> SubTestReport(context=SubTestContext(msg='test_baz_second', kwargs={}))
.<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='call' outcome='passed'>
<class '_pytest.reports.TestReport'> <TestReport 'test_foo.py::test_baz' when='teardown' outcome='passed'>

@nicoddemus
Copy link
Member

If it is convenient, it might be a good idea to somehow make this -s behavior consistent with pytest style functions and UnitTest subclasses.

Indeed we suspend the output capture before reporting in subtests fixtures:

with self.suspend_capture_ctx():
self.ihook.pytest_runtest_logreport(report=sub_report)

But do not suspend for self.subTest():

sub_report = SubTestReport._from_test_report(report)
sub_report.context = SubTestContext(msg, dict(test.params)) # type: ignore[attr-defined]
self.ihook.pytest_runtest_logreport(report=sub_report)

The former was implemented in #10, might have been just an oversight that the same handling was not done for self.subTest.

Would you be so kind as to open a separate issue with the difference in handling -s between the fixture and TestCase? Thanks!

@mkmoisen
Copy link
Author

@nicoddemus

Done, please see here

@nicoddemus
Copy link
Member

Thanks @mkmoisen appreciate it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants