Kettamon
Kettamon

Reputation: 97

How to use an object's __subclasses__() method in bulk pytest execution?

I'm looking to write unit tests for some code that uses an object's __subclasses__() method. Ultimately, I was trying to use __subclassess__() to keep track of classes dynamically imported into my application through a namespace package.

In some cases, my code base has test classes created within a pytest file. My problem is that when I run pytest in bulk on my source directory I find failures or errors in my code due to import polutions from these one-off test classes. This is because the pytest run maintains all of the imports as it runs through the source tree. On their own the tests pass fine, but in a sequence they fail, sometimes depending on the order in which they ran.

In my current code branch these __subclasses__() invocations are in the application code, but I have moved them out to tests here to demonstrate with a MVE:

In my/example.py

class MyClass(object):
    def __init__(self):
        pass


class MySubClass(MyClass):
    def __init__(self):
        super().__init__()

In my/test_subclass.py

from my.example import MyClass


class TestSubClass(MyClass):
    def __init__(self):
        super().__init__()


def test_TestSubClass():
    assert issubclass(TestSubClass, MyClass)

In my/test_subclasses.py

from my.example import MySubClass, MyClass


def test_find_subclasses():
    assert all([cls == MySubClass for cls in MyClass.__subclasses__()])

The result, when run on my machine, is that the test_find_subclasses() test fails due to the discovery of the TestSubClass when running after test_subclass.py:

    def test_find_subclasses():
>       assert all([cls == MySubClass for cls in MyClass.__subclasses__()])
E       assert False
E        +  where False = all([True, False])

What is the best way to maintain a "clean" state during sequenced pytest runs so that I can avoid mangling imports?

Upvotes: 1

Views: 81

Answers (1)

Mad Physicist
Mad Physicist

Reputation: 114488

As discussed in the comments, you probably don't want to hard-code the types that may extend MyClass, since you really can't predict what your application will need in the future. If you want to check subclassing, just check that it works at all:

def test_find_subclasses():
    assert MySubClass in MyClass.__subclasses__()

Even more concisely, you could simply do

def test_find_subclasses():
    assert issubclass(MySubClass, MyClass)

That being said, you can technically filter the classes you are looking through. In your particular case, you have a distinctive naming convention, so you can do something like

def only_production_classes(iterable):
    return [cls for cls in iterable if not cls.__name__.lower().startswith('test')]

def test_find_subclasses():
    assert all([cls == MySubClass for cls in only_production_classes(MyClass.__subclasses__())])

You can define only_production_classes using other criteria, like the module that the class appears in:

def only_production_classes(iterable):
    return [cls for cls in iterable if not cls.__module__.lower().startswith('test_')]

You can't easily unlink class objects that have been loaded, so your idea of a "clean" test environment is not quite feasible. But you do have the option of filtering the data that you work with based on where it was imported from.

Upvotes: 1

Related Questions