I often had to use mixins to group test methods that can run on different implementations of the same interface, for example:
class BackendTestsMixin:
backend: BackendInterface
def test_read(self):
self.assertEqual(self.backend.read(), "42")
class MemoryBackendTests(BackendTestsMixin, TestCase):
def setUp(self):
self.backend = MemoryBackend()
class DiskBackendTests(BackendTestsMixin, TestCase):
def setUp(self):
self.backend = DiskBackend()
Now, mypy rightfully complains on the example above because assertEqual is not a member of the class; however, if I make BackendTestsMixin a subclass of TestCase, unittest will try to run its test methods.
It strikes me as overkill to run mypy on test code, but there could be good reason.
There’s an argument as well, when it comes to simple test code, to not worry about fancy design patterns, nor about making things as DRY as possible. Some kind of duplication is the whole point of the exercise - to independently verify an implementation, and keep the two separate. I.e. minimise, and ideally remove, the dependencies of the test code, on the code under test.
Anyway, an alternative methodology instead of mix-ins, is to use factory functions to generate the test methods. I use this a lot for parametric testing schemes, that work with both unittest and pytest, without requiring pytests’s decorator.
I do not see significant differences between the CPython example and the one I sketched, and unfortunately BackendTestsMixin.test_read would fail type checking in your example the same as it fails in mine
If I understand your proposal correctly, hiding test classes inside a factory method would hide them from the unittest default loader and allow me to add only the classes that I need.
It is indeed, technically, a way to do it with existing tools. It would involve quite a lot of overhaul of how unit tests are usually written, though.
I didn’t mean to propose a way to do something that isn’t currently possible at all: I mean to propose a straightforward, intuitive way to do something that currently involves significant nontrivial runes.
I’m not sure how it works with Pytest, but if Unittest doesn’t automatically run instances of TestSuite already (kind of the point of them), it’s a simple matter of calling the TestSuite’s run method.
Even if none of these solutions are to your liking, that is scant justification to make a breaking change to Python, that will suddenly alter the behaviour of everyone else’s tests, by making loadTestsFromModule ignore Class(TestCase, ABC).
If you really want that it’s fine. Implement your own loader.
If mypy is correct to flag this as an error, then you shouldn’t be writing your code like that. And any of the alternatives proposed are good ways of avoiding the error.
If, on the other hand, you consider your code to be correct, then mypy is incorrect to flag it - you can’t have it both ways. That’s not surprising - mypy by design doesn’t handle the more dynamic runtime behaviours of Python, as it’s a static type checker, and by choice it doesn’t (as far as I know) do global analysis so it may miss “long distance” interactions. So it may be that the mypy developers don’t consider this as a bug they’d be willing to fix. In which case, you have the # type: ignore directive which is explicitly designed to override mypy and tell it you know what you’re doing.
So there are lots of ways of handling this within the existing capabilities of Python and mypy. I don’t think this issue warrants making a special case change to unittest (especiallly as your proposed fix is incomplete - it doesn’t work if BackendTestsMixin is dynamically registered as an ABC via ABC.register).
I don’t understand why you consider it a breaking change: ABC classes are not supposed to be instantiated, and trying to run Class(TestCase, ABC) in a test suite should already fail?
Like @JamesParrott suggested, a class factory is indeed the better solution, but with the dynamically created classes returned and assigned to global names instead so that the default loader can find them at the module level.
A good example can be found in CPython’s test_abc.py:
I appreciate all your efforts trying to give me alternatives, and indeed I learnt a trick or two in the process.
I still do not see any attempt to evaluate the proposal, besides @pf_moore’s mention of ABC.register, which I find relevant.
I have no idea if there can be a general way to detect if a class is registered as abstract that would work well with ABC.register, though without that I still see value in using abc.ABC as an intuitive and effective marker for test cases not to be run.
Alternatively, nose seems to support a __test__ = False attribute to skip test cases, which could be incorprated into unittest for a more general solution, though that would be a different idea to address similar user stories
If you have tests you only want to perform on some of the situations, you need another class that potentially does the same setup again (e.g. if setting up DiskBackend is a decent amount of work), or your factory template classes get even more messy.
Clearly, the intend of OP is to use static typing features in their test files. Whether this is a good idea is something that might be worth discussing, but just suggesting pretty dynamic class creation patterns is missing the point entirely.
You’re somewhat missing the point of the replies. One of the most important things needed with a proposal is an explanation of why it’s needed. That involves exploring what the problem is, and how or if it can be solved without changing the language. Remember, it’s you who needs to persuade us that your proposal is worthwhile.
To that end, I tried your example, and I couldn’t get the error you claimed. I needed to create dummy classes for BackendInterface, MemoryBackend and DiskBackend, but having done so, mypy didn’t give any errors. So the first thing you need to do is give an easier to reproduce example of the problem you’re trying to solve.
Then you need to be prepared to give a fair response to the various alternatives proposed. You’ve not responded to my suggestion of # type: ignore yet, and @JamesParrott suggested a class factory, which you dismissed with (essentially) the reponse that you don’t like that approach. But class factories or subtests are the standard way of writing parametrised tests in unittest, so simply dismissing those isn’t really a fair response.
For reference, using subtests would look something like this (warning: untested code):
backend_types: list[BackendInterface] = [MemoryBackend, DiskBackend]
class BackendTests(TestCase):
def test_read(self):
for backend_type in backend_types:
with self.subTest(backend_type=backend_type)
backend = backend_type()
self.assertEqual(backend.read(), "42")
unittest tries to instantiate the TestCase class, which it cannot do:
... etc ...
File "/usr/local/pyenv/pyenv/versions/3.12.4/lib/python3.12/unittest/suite.py", line 24, in __init__
self.addTests(tests)
File "/usr/local/pyenv/pyenv/versions/3.12.4/lib/python3.12/unittest/suite.py", line 57, in addTests
for test in tests:
TypeError: Can't instantiate abstract class Mixin without an implementation for abstract method 'make_backend'
Since the code as presented is now an error, wouldn’t it be better to update unittest to not attempt to instantiate classes that can’t be instantiated?
That’s a good point (and if it’s what the OP was trying to suggest, I’m sorry I didn’t get that from the way they presented their argument). I agree that fixing this seems like a reasonable thing to do.
I’m still not 100% sure if it’s as trivial as checking for a base class of ABC, though. At a minimum, it would need to check the full MRO, and it probably also needs to look for the ABCMeta metaclass rather than the specific class ABC. And dynamic registration may be relevant as well, although I’ve not been able to consistently trigger the “Can’t instantiate” error in my tests, so I don’t fully understand the complexities there.
I don’t understand why you consider it a breaking change: ABC classes are not supposed to be instantiated
Because there’s a non-zero probability someone else has written class MyTest(TestCase, ABC): for their own reasons, and wants to run that test case with loadTestsFromModule. There’s an awful lot of Python code in the world. Yes indeed ABCs probably should be intended not to be instantiated, even without abstract methods. But sometimes weird hacks are necessary, and people do a lot of things they shouldn’t do. That doesn’t mean we should break their tests.