Skip to content

Improve subtests clean up #1719

Closed
Closed

Description

@sechkova

Description of issue or feature request:

In tests we use the home-made decorator @utils.run_sub_tests_with_dataset when we need to execute the same test case consecutively for many datasets. The decorator works perfectly fine when no clean up is needed after each sub test but in the cases where clean up code needs to be executed, the whole sub test needs to be included in a "try - finally" block in order to run the clean up code even if a sub test fails.

Expected behavior:
Improve the decorator by possibly including the clean up logic in it to avoid the big "try" blocks in the test code.
See #1689 (comment)

Activity

  1. jku commented on Dec 13, 2021

    @jku
    Member

    another related thing is case_name that is set in the decorator now: it's quite useful but leads to multiple workarounds with both pylint and mypy correctly complaining about it. I think if the decorator is part of a class (something derived from unittest.TestCase) then this would probably be a fixable situation.

  2. abs007 commented on May 26, 2022

    @abs007
    Contributor

    @jku I went through your comment and thought of this.

    Have a SubTestCase(unittest.TestCase) class which would use an __init__() method to initialize the original function (the one that uses the decorator) and tie it to the class instance and then use a __call__() method to allow for calling the class.
    __call__() would essentially contain the decorator code and cls.setup_subtest(), cls.teardown_subtest() (which I believe is the cleanup code) would also be managed inside this class.

    I would like your thoughts on this.

  3. MVrachev commented on Jun 9, 2022

    @MVrachev
    Collaborator

    Sounds like a reasonable idea.
    By implementing the __call__ method you will call the decorator and the cls.setup_subtest() logic, but when and how do you call the cls.teardown_subtest() logic?
    As I understand it we will call the __call__ method with each test case we are given and that's why I am wondering how do you imagine fitting the teardown_subtest logic in that?

  4. abs007 commented on Jun 11, 2022

    @abs007
    Contributor

    Would something like this work? (please excuse me if I'm not making sense logically)

    class SubTestCase(unittest.TestCase):
        def __init__(self, orginal_function):
            self.orginal_function = orginal_function
            
        def wrapper():      
            #decorator code
        
        def __call__(self):
            wrapper()
            cls.setup_subtest()
            cls.teardown_subtest()
    

    What I'm thinking is that __call__() won't be called multiple times now since we're putting the decorator code in the wrapper() func.

  5. MVrachev commented on Jun 20, 2022

    @MVrachev
    Collaborator

    Would something like this work? (please excuse me if I'm not making sense logically)

    class SubTestCase(unittest.TestCase):
        def __init__(self, orginal_function):
            self.orginal_function = orginal_function
            
        def wrapper():      
            #decorator code
        
        def __call__(self):
            wrapper()
            cls.setup_subtest()
            cls.teardown_subtest()
    

    What I'm thinking is that __call__() won't be called multiple times now since we're putting the decorator code in the wrapper() func.

    It seems to make sense. You call the decorator and then the setup function.
    I haven't tested what is first now - the decorator or the setUp function.

    Can you choose a testing function using the decorator and link to an example how what it would look like?
    It will be great if you push an example branch with this change in your fork, so we can have a look closer.
    Then it would be easier to validate that idea.

  6. abs007 commented on Jul 2, 2022

    @abs007
    Contributor

    Sorry, it looks like I got things mixed up. I thought @jku meant a decorator class by his comment (A class mimicking a decorator function). Some things that I'd like to ask first.

    My question is that do we need to pass setup_subtest() inside the setUpClass() method (I'm guessing this is where he meant to put it) since we seem to use it after calling the decorator like in here. I'm saying this since setUpClass() will pass only once before all the test cases are run. Also, will setUpClass() run in the first place if we don't have any test functions inside the class? (which I don't think we're supposed to have since we only have our decorator logic)

  7. mhb8898 commented on May 14, 2026

    @mhb8898

    Hi! I'd like to work on this issue. This would be my first contribution to the project and I'm excited to start helping out.

    My proposed fix is to update the run_sub_tests_with_dataset decorator in tests/utils.py to automatically call teardown_subtest() in a finally block after each subtest case runs:

    def wrapper(test_cls: unittest.TestCase) -> None:
        for case, data in dataset.items():
            with test_cls.subTest(case=case):
                test_cls.case_name = case.replace(" ", "_")
                try:
                    function(test_cls, data)
                finally:
                    if hasattr(test_cls, "teardown_subtest"):
                        test_cls.teardown_subtest()

    This removes the need for manual try/finally blocks in the test methods. The hasattr check keeps it backward compatible with test classes that don't define teardown_subtest.

    This would clean up the boilerplate in test_updater_delegation_graphs.py (5 methods) and test_updater_consistent_snapshot.py (3 methods). All existing tests pass after the change.

    I also read @jku's suggestion about wrapping this into a SubTestCase class with setup_subtest/teardown_subtest as class methods. I think that's a great idea for a more complete solution, but I wanted to start smaller — this decorator change already removes all the try/finally boilerplate and improves the code meaningfully. We can always build on top of it later.

    Happy to open a PR if this approach looks good to you!

  8. Pratik060921 commented on Aug 12, 2026

    @Pratik060921

    Hi! I'd like to work on this issue. This would be my first contribution to the project and I'm excited to start helping out.

    My proposed fix is to update the run_sub_tests_with_dataset decorator in tests/utils.py to automatically call teardown_subtest() in a finally block after each subtest case runs:

    def wrapper(test_cls: unittest.TestCase) -> None:
    for case, data in dataset.items():
    with test_cls.subTest(case=case):
    test_cls.case_name = case.replace(" ", "_")
    try:
    function(test_cls, data)
    finally:
    if hasattr(test_cls, "teardown_subtest"):
    test_cls.teardown_subtest()

    This removes the need for manual try/finally blocks in the test methods. The hasattr check keeps it backward compatible with test classes that don't define teardown_subtest.

    This would clean up the boilerplate in test_updater_delegation_graphs.py (5 methods) and test_updater_consistent_snapshot.py (3 methods). All existing tests pass after the change.

    Happy to open a PR if this approach looks good to you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions