[Из песочницы] Unittest и абстрактные тесты

Вместо вступления


Unittest — наверное самый известный фреймворк для написания тестов в Python. Он очень прост в освоении и его легко начать использовать в вашем проекте. Но ничто не бывает идеальным. В этом посте я хочу рассказать об одной возможности, которой лично мне (думаю, не одному) не хватает в unittest.

Немного о Unit тестах


Прежде чем обсуждать (и осуждать) тестовый фреймворк, считаю нужным немного поговорить о тестировании в целом. Когда я впервые услышал словосочетание «модульное тестирование», то подумал, что это обязанности какой-то службы по контролю качества, которая проверяет программные модули на соответствие требованиям. Каково же было моё удивление, когда я узнал, что эти самые тесты должны писать программисты. Первое время я не писал тесты… вообще. Ничто не заменит тех ощущений, когда просыпаешься утром и читаешь от пользователя «Программа не работает. С этим срочно надо что-то делать». Сперва мне казалось, что это совершенно обычный процесс, пока не написал первый модульный тест, который нашёл ошибку. Модульные тесты вообще кажутся бесполезными ровно до тех пор, пока не обнаруживают проблемы. Теперь я просто не могу закоммитить новую функцию, не написав на неё тест.
Но тесты служат не только для проверки правильности написания кода. Вот список функций, которые, на мой взгляд, выполняют тесты:

  • Обнаружение ошибок в программном коде
  • Придание программистам уверенности в том, что их код работает
  • Следствие из предыдущего пункта: возможность без опаски вносить изменения в программу
  • Тесты — своего рода документация, которая наиболее верно описывает поведение системы


В некотором смысле, тесты повторяют структуру программы. Применимы ли принципы построения программ к тестам? Я считаю, что да — это такая же программа, пусть и проверяющая другую.

Описание проблемы


В какой-то момент мне пришла идея написать абстрактный тест. Что я подразумеваю под этим? Это такой тест, который сам не выполняется, но объявляет методы, зависящие от параметров, определяемых в наследниках. И тут я обнаружил то, что не могу этого сделать по-человечески в unittest. Вот пример:

class SerializerChecker(TestCase):
    model = None
    serializer = None

    def test_fields_creation(self):
        props = TestObjectFactory.get_properties_for_model(self.model)
        obj = TestObjectFactory.create_test_object_for_model(self.model)
        serialized = self.serializer(obj)
        self.check_dict(serialized.data, props)


Думаю, даже не зная реализацию TestObjectFactory и метода check_dict понятно, что props — это словарь свойств для объекта, obj — объект, для которого мы проверяем сериализатор. check_dict проверяет рекурсивно словари на совпадение. Думаю, многие, кто знаком c unittest сразу скажут, что этот тест не соответствует моему определению абстрактного. Почему? Потому что метод test_fields_creation выполнится из этого класса, чего нам абсолютно не надо. После некоторого поиска информации, я пришёл к выводу, что самый адекватный вариант — не наследовать SerializerChecker от TestCase, а наследников реализовывать как-то так:

class VehicleSerializerTest(SerializerChecker, RecursiveTestCase):
    model = Vehicle
    serializer = VehicleSerialize


RecursiveTestCase — потомок TestCase, который реализует метод check_dict.

Это решение некрасиво сразу с нескольких позиций:

  • В классе SerializerChecker мы должны знать, что потомок должен наследоваться от TestCase. Эта зависимость может создать проблемы для тех, кто незнаком с этим кодом
  • Среда разработки упорно считает, что я неправ, так как у SerializerChecker нет метода check_dict


image

Ошибка, которую выдаёт среда разработки

Может показаться, что можно просто добавить заглушку для check_dict и все проблемы решены:

class SerializerChecker:
    model = None
    serializer = None

    def check_dict(self, data, props):
        raise NotImplementedError

    def test_fields_creation(self):
        props = TestObjectFactory.get_properties_for_model(self.model)
        obj = TestObjectFactory.create_test_object_for_model(self.model)
        serialized = self.serializer(obj)
        self.check_dict(serialized.data, props)


Но это не является полноценным решением проблемы:

  • Мы, по сути, создали интерфейс, который реализует не потомок данного класса, а RecursiveTestCase, что создаёт оправданные вопросы к архитектуре.
  • В TestCase много методов assert*. Неужели нам надо писать заглушку для каждого используемого? Всё ещё кажется хорошим решением?


Подводя итог


Unittest не предоставляет вменяемой возможности «отключить» класс, отнаследованный от TestCase. Я был бы очень рад, если бы такую функцию добавили в фреймворк. А как вы решаете данную проблему?

© Habrahabr.ru