0

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()
foraidt
  • 5,519
  • 5
  • 52
  • 80
  • 2
    [This](https://stackoverflow.com/questions/1323455/python-unit-test-with-base-and-sub-class) is the best solution I think. I believe that is one of the things you describe, but where pycharm said these methods don't exist. Ignore that or add these functions to the class with `raise NotImplementetError`, which will let pycharm believe this is a abstract class. – MegaIng Feb 23 '18 at 21:09
  • @MegaIng Sorry for the delay, I've been rather busy: I followed your advice and silenced PyCharm by defining the assert functions of `TestCase` that' I'm using. If you created an answer from your comment I'd be happy to accept it. – foraidt Feb 28 '18 at 11:04
  • I answered you can now accept it. – MegaIng Feb 28 '18 at 21:06

2 Answers2

1

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!

G_M
  • 3,342
  • 1
  • 9
  • 23
  • Just to let you know, while my problem got initially solved by @MegaIng's answer, I later needed further combinations and doing this with inheritance just wasn't feasible anymore. I now am using pytest with parametrized fixtures as you suggested. – foraidt Mar 12 '18 at 15:46
  • @foraidt Glad to hear, there are many benefits to using pytest. Seems like this should be the accepted answer then. – G_M Mar 12 '18 at 15:51
  • You're right, I didn't think it was possible to change the accepted answer; thanks for the hint! – foraidt Mar 13 '18 at 09:33
1

You can get pycharm to ignore these not existing function by creating abstract methods with raise NotImplementetError:

class GroupOfTests:
    session_id = None

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

    def assertGreater(self, a, b): # Pycharm treats these like abstract methods from the ABC module
        raise NotImplementetError

This will let python believe this is an abstract class and will make pycharm raise errors if a subclass doesn't define these functions.

MegaIng
  • 7,361
  • 1
  • 22
  • 35