ВDD и мы

Введение

На протяжении всего времени, что я работаю в этой компании в воздухе витает дух тестирования. Стремление сдвинуть стрелку code coverage с 0% и с тем постепенно повышать надежность ПО знакомо каждому разработчику, и многие из них с пеной у рта готовы доказывать своим менеджерам о необходимости подобного подхода.

Обычно пропагандируется использование подхода TDD (test-driven development): сначала мы пишем тесты, затем уже пишем код приложения и путем прогона тестов выясняем, какие части приложения работают не так, как мы хотим. По сути, мы делаем ставку на равенство суммы корретно работающих компонент корретно работающему приложению. На практике же про этот подход вспоминают слишком поздно и применение его сводится к покрытию тестами уже существующего кода. Но даже перед этим, им необходимо “продать” менеджеру продукта идею выделения части оплачиваемого разработчикам времени на написание тестов.

Вот тут и начинаются до боли знакомые вопросы...

  • “Как мы это сможем продать?”
  • “Какая нам от этого польза?”
  • “Зачем тратить время на старый код, когда мы можем запилить много нового?!”

И они по-своему верны.

Для примера, рассмотрим классический случай теста на основе unittest:

def test_group_counter(self):
   ...
   self.failUnlessEqual(self.grup1.pupil_set.count(), self.pupil_in_gr )
   self.failUnlessEqual(Group.objects.count(), self.group_count)
   self.failUnlessEqual(Pupil.objects.count(), self.pupil_in_gr * self.group_count)
   ...

Если вы покажете такое менеджеру, то он вряд ли сможет разобрать хоть что-то. С другой стороны, разработчик системы без всяких проблем сможет увидеть в этих строчках проверку вычисления определенных внутренних аттрибутов объекта и их соответствие с ситуацией в БД.

И вот здесь заключается одна из основных проблем работы по TDD: навыками написания и чтения тестов владеет только разработчик, менеджер и аналитик выключены из процесса. Соответственно, им трудно оценить приносимую тестами пользу.

Это приводит к целому ряду негативных последствий:

  • Проблемы в общении между членами команды
  • Разные цели в тестировании продукта: разработчик думает про корректность кода, менеджер душой болеет за корректность продукта
  • Покрытие тестами представляется менеджеру как бесполезная трата времени

BDD и с чем его едят

Описанные выше проблемы привели к разработке нового подхода, во многом базирующегося на идеях TDD - BDD (behaviour driven development). Он объединяет основные принципы и техники BDD с идеями из доменно-ориентированного проектирования для предоставления аналитикам и разработчикам единого инструмента для взаимодействия в процессе разработки продукта.

Ниже приведен один из примеров теста, написанного с использованием BDD:

Функция: вычисление очереди
       Предыстория: имеется база с учреждением и заявлениями в разных статусах
               Дано имеется учреждение "ДОУ №1" (вид: "ДОУ")
               И имеются заявления
                       | Желаемый ДОУ | Статус заявления     | Дата и время подачи | Дата выбора ДОУ     |
                       | ДОУ №1           | Зарегистрировано     | 01.01.2011 01:02:03 | 04.05.2011 01:00:00 |
                       | ДОУ №1           | Направлен в ДОУ      | 02.01.2011 01:02:03 | 05.06.2012 02:00:00 |
                       | ДОУ №1           | Подтверждение льгот  | 02.01.2011 01:02:03 | 06.07.2013 03:00:00 |
               И диапазон возрастов от 3 до 7 лет
               И установлены стандартные параметры расчета очереди
               И очередь считается по учреждению "ДОУ №1"
       Сценарий: фильтрация заявлений
               Когда вычисляется очередь
               То в очередь попадают только заявления в статусах
                       | Статус заявления    |
                       | Зарегистрировано    |
                       | Подтверждение льгот |
                       | Выбор желаемого ДОУ |
                       | Желает изменить ДОУ |
               И у заявлений в очереди нет направлений в статусах
                       | Статус направления  |
                       | Предложено системой |
                       | Зачислен                        |
                       | Не явился               |
                       | Желает изменить ДОУ |
       Сценарий: сортировка очереди по тульской модели комплектования
               Когда включена тульская модель комплектования
               И вычисляется очередь
               То заявления отсортированы по возрастанию даты выбора ДОУ

Как можно заметить, тест стал гораздо проще для чтения и понимания всеми участниками проекта. Теперь уже не нужно знать тонкости технической реализации внутренних механизмов системы для написания тестов и этим может заниматься кто-то кроме разработчика. Во многом это достигается использованием “единого языка”, полуформального набора терминов для описания бизнес-логики системы. Этот набор разрабатывается совместно разработчиками и аналитиками, устраняя двойственные трактовки одних и тех же аспектов бизнес-логики.

Формат описания теста напоминает описание User Story, знакомую использующим Agile командам. Действительно, они выполняют схожую функцию - описание поведения, которое представляет ценность для конечного клиента. Также как и в User Story здесь присутствуют Feature (“Функция” и “Предыстория”) и Acceptance Criteria (“Сценарий”, “Дано-Когда-То”), помогающие описать требуемое поведение и условия корретности его реализации.

Но как этот человекочитаемый текст транслируется в проверяющий условия код? За это отвечает файл с “шагами”:

@given(u'диапазон возрастов от {start_age} до {end_age} лет')
def get_age_range(context, start_age=0, end_age=7):
    context.ages = (float(start_age), float(end_age))
...
@when(u'вычисляется очередь {queue_type}')
def calculate_specific_queue(context, queue_type):
    types = {
        u'сводная': -1,
        u'общая': 1,
        u'льготная': 2,
        u'переводников': 3
    }
    context.type = types[queue_type]
    context.queue = DeclarationQueue(context.queue_ctx)
    context.queue_decls = context.queue.get_list()[0]
...
@when(u'вычисляется очередь')
def calculate_default_queue(context):
    context.queue = DeclarationQueue(context.queue_ctx)
    context.queue_decls = context.queue.get_list()[0]
...
@then(u'в очередь попадают только заявления в статусах')
def check_declaration_selection(context):
    allowed_statuses = [row[u'Статус заявления'] for row in context.table]
    for decl in context.queue_decls:
        assert decl['status__name'] in allowed_statuses, \
              u'Status "%s" not allowed in queue!' % decl['status__name']

Каждый шаг в написанном тесте должен соответствовать одному обработчику. Разделение кода выполнения теста облегчает дальнейшую компоновку шагов в сценарии, позволяя также использовать их многократно в качестве библиотеки стандартных действий.