I am writing some unit tests with Pytest. If I want them to be collected automatically, I have to avoid the __init__ constructor. (If there's a way to make Pytest collect tests with the __init__ constructor I'd take that as an alternate useful answer.)
My unit tests have some variables and methods in common. Right now I have base test class TestFoo, and child test class TestBar(TestFoo), and grandchild test class TestBaz(TestBar). Since I can't have an init method, right now I'm calling a setup() method that assigns a bunch of variables to the class instance as a part of every single test method.
It looks like:
Class TestBaz(TestBar):
def setup():
super().setup()
# do some other stuff
def test_that_my_program_works(self):
self.setup()
my_program_works = do_stuff()
assert my_program_works
But this is ugly and I was wondering if there was a way to get around it. One thing I got working -- I made this decorator function to decorate every method:
def setup(cls):
def inner_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
cls.set_up()
return func(*args, **kwargs)
return wrapper
return inner_function
but
@setup
def test_that_my_program_works():
is not that much better. I was sort of in the weeds reading about metaclasses and trying to figure out how I would more silently wrap every method when I realized that fundamentally I don't want or need to wrap every method. I just want a method that executes automatically on class initialization. I want __init__ without __init__.
Is there a way to do this?
As you had seem, py.test have other means to run a setup for class-scoped methods. You'd probably run those, as they are guaranteed to be run at the right points between each (test) method call - as one won't have control on when py.test instantiate such a class.
For the record, just add a setup method to the class (the method name is all-lower case), like in:
class Test1:
def setup(self):
self.a = 1
def test_blah(self):
assert self.a == 1
However, as you asked about metaclasses, yes, a metaclass can work to create a "custom method equivalent to __init__".
When a new object is created, that is, when class is instantiated in Python, it is as though the class itself was called. What happens internally is that the __call__ method for the metaclass is called, with the parameters passed to create the instance.
This method then runs the class' __new__ and __init__ methods passing those parameters, and returns the value returned by __new__.
A metaclass inheriting from type can override __call__ to add extra __init__- like calls, and the code for that is just:
class Meta(type):
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw)
custom_init = getattr(instance, "__custom_init__", None)
if callable(custom_init):
custom_init(*args, **kw)
return instance
I've tried this with a small class in a file I run with pytest, and it just works:
class Test2(metaclass=Meta):
def __custom_init__(self):
self.a = 1
def test_blah(self):
assert self.a == 1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With