1

I am trying to test DRF endpoints, and trying to add mixings dynamically to a test in order to execute tests again each method allowed in the endpoint (get, post, put, patch, delete)

So, my idea is to make a base test class that will automatically add some mixings to test endpoints if they are allowed. And I can create the actual test that will inherit from this base class.

The code:

from rest_framework.test import APITestCase

class GetTestMixin:
    def test_get_all(self):
        response = self.client.get(self.uri)
        self.assertEqual(response.status_code,status.HTTP_200_OK)


class AutoMixinMeta(type):
    def __call__(cls, *args, **kwargs):
        allowed_methods = ['get', 'post']
        # cls would be the Test class, for example TestExample
        # cls.__bases__ is a tuple with the classes inherited in the Test class, for example:
        # (<class 'unit_tests.endpoints.base_test.RESTTestCase'>, <class 'rest_framework.test.APITestCase'>)
        bases = cls.__bases__
        for method in allowed_methods:
            bases += (cls.method_mixins[method.lower()],)
        # Create a new instance with all the mixins
        cls = type(cls.__name__, bases, dict(cls.__dict__))
        return type.__call__(cls, *args, **kwargs)

class RESTTestCase(metaclass=AutoMixinMeta):
    uri = None
    method_mixins = {
        'post': PostTestMixin,
        'get': GetTestMixin,
    }

class TestExample(RESTTestCase, APITestCase):
    uri = reverse('somemodel-list')

I was expecting test_get_all to be executed, but it is not.

Mixings are in place. I made a dummy method inside TestExample and put a debugger in place, and checked it, like this:

(Pdb) self.__class__
<class 'TestExample'>
(Pdb) self.__class__.__bases__
(<class 'RESTTestCase'>, <class 'rest_framework.test.APITestCase'>, <class 'GetTestMixin'>)
Gonzalo
  • 752
  • 8
  • 23
  • Have a read at: https://stackoverflow.com/a/10100114/6759844 – Brian Destura Jun 17 '21 at 05:55
  • This basically means that `dir(TestExample)` will never have `test_get_all` because it is never instantiated at the time that the test_cases are collected (i.e. never gets to `AutoMixinMeta.__call__`) – Brian Destura Jun 17 '21 at 05:56
  • @bdbd thank you. Is there a way to re collect them or make it work? Or should I look to another approach? – Gonzalo Jun 17 '21 at 13:43

1 Answers1

0

The problem there is that the code that collects the classes to be tested will never "see" the class as an instance of the Test classes, or as a subclass of: the class inheriting from the test cases only exist when an instance is created.

The only way for this to work is to create the derived classes at import time, and to bind the desired dynamic classes as top-level names on the module.

To do that, you can do away with the metaclass, and just place the statements in the module body, assigning the new class or classes to names using globals(). Or, if you want just the subclasses, rather than at module top level, the code can be placed in the __init_subclass__ method. This methd is called when the class is created, not when it is instantiated, and it should work.

from rest_framework.test import APITestCase

class GetTestMixin:
    def test_get_all(self):
        response = self.client.get(self.uri)
        self.assertEqual(response.status_code,status.HTTP_200_OK)




class RESTTestCase():
    uri = None
    method_mixins = {
        'post': PostTestMixin,
        'get': GetTestMixin,
    }
    
    def __init_subclass__(cls, *args, **kw):
        super.__init_subclass__(*args, **kw)
        if "Dynamic" in cls.__name__:
            return
        allowed_methods = ['get', 'post']
        
        bases = list(cls.__bases__)
        for method in allowed_methods:
            bases.append(cls.method_mixins[method.lower()])
        # Create a new instance with all the mixins
        new_cls = type(cls.__name__ + "Dynamic", bases, dict(cls.__dict__))
        globals()[new_cls.__name__] = new_cls


class TestExample(RESTTestCase, APITestCase):
    uri = reverse('somemodel-list')

# class TestExampleDynamic is created automatically when the `class` statement above resolves

jsbueno
  • 99,910
  • 10
  • 151
  • 209