Search code examples
pythoninheritancemixinspython-unittest

Use different implementations of `setUp` and `tearDown` of unittest.TestCase instances


I want to run a set of tests under different conditions and therefore share these tests between two different TestCase-derived classes. One creates its own standalone session and the other attaches to an existing session and executes the same tests in there.

I guess I'm kind of abusing the unittest framework when testing an API with it but it doesn't feel like it's too far from its original purpose. Am I good so far?

I hacked a few things together and got it kind of running. But the way it's done, doesn't feel right and I'm afraid will cause problems sooner or later.

These are the problems I have with my solution:

  • When simply running the thing with PyCharm without limiting the tests, it attempts to run not only the intended StandaloneSessionTests and ExistingSessionTests but also GroupOfTests which is only the collection and has no session, i.e. execution context.

  • I can make it not run GroupOfTests by not deriving that one from TestCase but then PyCharm complains that it doesn't know about the assert...() functions. Rightly so, because GroupOfTest only gets indirect access to these functions at runtime when a derived class also inherits from TestCase. Coming from a C++ background, this feels like black magic and I don't think I should be doing this.

I tried passing the session creation classes to the constructor of GroupOfTests like this: __init__(self, session_class). But this causes problems when the unittest framework attempts to instantiate the tests: It doesn't know what to do with the additional __init__ parameter.

I learned about @classmethod, which seems to be a way to get around the "only one constructor" limitation of Python but I couldn't figure out a way to get it running.

I'm looking for a solution that lets me state something as straightforward as this:

suite = unittest.TestSuite()
suite.addTest(GroupOfTests(UseExistingSession))
suite.addTest(GroupOfTests(CreateStandaloneSession))
...

This is what I got so far:

#!/usr/bin/python3

import unittest


def existing_session():
    return "existingsession"


def create_session():
    return "123"


def close_session(session_id):
    print("close session %s" % session_id)
    return True


def do_thing(session_id):
    return len(session_id)


class GroupOfTests(unittest.TestCase):  # GroupOfTests gets executed, which makes no sense.
#class GroupOfTests:  # use of assertGreaterThan() causes pycharm warning
    session_id = None

    def test_stuff(self):
        the_thing = do_thing(self.session_id)
        self.assertGreater(the_thing, 2)

    # Original code contains many other tests, which must not be duplicated


class UseExistingSession(unittest.TestCase):
    session_id = None

    def setUp(self):
        self.session_id = existing_session()

    def tearDown(self):
        pass  # Nothing to do


class CreateStandaloneSession(unittest.TestCase):
    session_id = None

    def setUp(self):
        self.session_id = create_session()

    def tearDown(self):
        close_session(self.session_id)


# unittest framework runs inherited test_stuff()
class StandaloneSessionTests(CreateStandaloneSession, GroupOfTests):
    pass


# unittest framework runs inherited test_stuff()
class ExistingSessionTests(UseExistingSession, GroupOfTests):
    pass


def main():
    suite = unittest.TestSuite()
    suite.addTest(StandaloneSessionTests)
    suite.addTest(ExistingSessionTests)

    runner = unittest.TextTestRunner()
    runner.run(suite())


if __name__ == '__main__':
    main()

Solution

  • I'm not sure if using pytest is an option for you but if so, here is an example which might do what you want.

    import pytest
    
    
    class Session:
        def __init__(self, session_id=None):
            self.id = session_id
    
    
    existing_session = Session(999)
    new_session = Session(111)
    
    
    @pytest.fixture(params=[existing_session, new_session])
    def session_fixture(request):
        return request.param
    
    
    class TestGroup:
        def test_stuff(self, session_fixture):
            print('(1) Test with session: {}'.format(session_fixture.id))
            assert True
    
        def test_more_stuff(self, session_fixture):
            print('(2) Test with session: {}'.format(session_fixture.id))
            assert True
    

    Output:

    $ pytest -v -s hmm.py
    ======================================================= test session starts ========================================================
    platform linux -- Python 3.6.4, pytest-3.4.1, py-1.5.2, pluggy-0.6.0 -- /home/lettuce/Dropbox/Python/Python_3/venv/bin/python
    cachedir: .pytest_cache
    rootdir: /home/lettuce/Dropbox/Python/Python_3, inifile:
    collected 4 items                                                                                                                  
    
    hmm.py::TestGroup::test_stuff[session_fixture0] (1) Test with session: 999
    PASSED
    hmm.py::TestGroup::test_stuff[session_fixture1] (1) Test with session: 111
    PASSED
    hmm.py::TestGroup::test_more_stuff[session_fixture0] (2) Test with session: 999
    PASSED
    hmm.py::TestGroup::test_more_stuff[session_fixture1] (2) Test with session: 111
    PASSED
    
    ===================================================== 4 passed in 0.01 seconds =====================================================
    

    If you are actually going to use pytest you will probably want follow the conventions for Python test discovery rather than using hmm.py as a filename though!