BDD наоборот

    Я читал про BDD, и понял одну вещь: BDD это блаблабла блабла бла. Нету у него нормального определения. Вот, например, написано:
    BDD совмещает в себе основные техники и практики TDD и идеи DDD для того чтобы предоставить программистам, тестерам, аналитикам и менеджерам общий процесс взаимодействия по ходу разработки ПО.
    Все понятно? Мне — ничего. Поэтому я расскажу, что мы делаем и зачем, из того, что может иметь отношение к BDD.
    Приступая к планированию фичи, мы описываем ее в терминах «поведения системы», например:
    Given Jira Issue with several bugs
    When all that bugs are fixed
    Then Issue is closed
    Мы это пишем прямо на доске, по ходу планинга, вместе с Product Owner, в таком объеме, чтобы было понятно заказчику (product owner) и тестировщикам, и разработчикам: что же нам надо сделать, и как это тестировать. По сути, это требования/тесты, которые записываются в таком «кратком» (и заметьте, не четком) виде, всей командой, не тратя на это много времени. И больше никакой документации, только слова. Понимаю, это довольно «дикий» случай, и не всегда так все просто, но в любом случае, мы стараемся выделить бизнес проблему, и записать наиболее краткое решение, не вдаваясь в детали. Детали идут потом, они могут быть оговорены на планинге или выясняться уже по ходу разработки с PO (да, у нас заказчик быстро отвечает на вопросы), но они не документируются и не тестируются и никто не будет их читать.

    Только не путайте описание поведения и системную документацию. Поведение может (и будет) меняться. Сегодня эта копка делает одно, завтра уже другое. Приведенное описание поведения устаревает с каждым спринтом/итерацией, на него нельзя полагаться и выяснить то, как же устроена фича спустя некоторое время. Поэтому существует системная документация, где описывается весь актуальный функционал. Нет, у нас такой документации нет, у нас все в голове и коде. И пока, лично я, не испытывал неудобств. А что будет если голова попадет под автобус? Я тоже задавал такой вопрос. Мне сказали: будет плохо, но мы не верим что документация спасет, вряд ли ее будут всю читать, и вряд ли она будет столь актуальна и сможет дать ответы так же, как голова.

    Теперь про тесты. Есть замечательная штука — Cucumber (и еще похожее: concordion). Он позволяет сопоставить каждой проcтой строчке текста (такой как: «all that bugs are fixed») свой метод на .Net,Java,Ruby и других языках программирования, и даже позволяет выделить переменные с помощью регулярных выражений, чтобы можно было повторно использовать методы.
    И благодаря cucumber, все тесты имеют отличный читаемый вид, они отражают текущее состояние системы и могут являться своебразной системной документацией, а к тому же они уже написаны на планинге!
    Все вроде замечательно, но мы не используем Cucumber, потому что:
    1. Наши тестовые методы, (вренее тестовое API, а не те методы, что с аннотацией @Test), принимают на вход сложные значения/объекты (Map), которые трудно записать в простой текстовой форме и распарсить с помощью регулярных выражений.
    2. Тестовые методы возвращают и используют переменные, например, возвращают Primary Key созданных сущностей, потом этот PK используется в других методах. И хотя поддержка «переменных» возможна в протом текстовом языке, это все равно неудобно и является большим минусом.
    3. Мы используем выражения в качестве перменных. Например, разные builders, чтобы задать дату «прошлого воскресенья» или «следующего понедельника». Кстати, мы генерируем произвольные/рандомные тестовые данные везде, где это только возможно, и все проверки делаем исходя из них, поэтому мы часто используем выражения и переменные.
    4. Контекст. У нас большинство действий привязано к текущей сессии пользователя. Когда понадобилось создать две параллельные сессии, это не вызвало никаких проблем. В случае cucumber, пришлось бы выделять абсолютно новые фразы/методы, которые работали бы, как с указанием сессии, так и без.
    5. Проверки данных. Для этого мы используем hamcrest, и с помощью всего нескольких матчеров, мы можем записывать кучу разнообразных и сложных условий. С cucumber, для этого приходится выделять отдельные методы с одной строчкой кода на каждое сочетание матчеров. А в худшем случае, по одному методу на каждое сочетние матчеров и проверяемого значения.
    6. Рефакторинг. Я рефакторю тесты значительно больше чем код, (ну так получается ) и IDE мне в этом очень сильно помогает, автоматизируя довольно сложные действия. А как рефакторить простой текстовый язык? А что будет, если раньше был один метод, а потом код поменялся и нужно его разделить на два метода, с разными переменными или разными названиями. Можно ли это сделать автоматически, по всей базе простотектового кода?
    Вместо того, чтобы тратить усилия на разработку простотекстовых сценариев с помощью Cucumber, мы пишем их на Java (как и остальной проект) и тратим усилия на то, чтобы код был «чистым» и читаемым.
    Вышесказанное не является критикой Cucumber, это отличная и нужная штука, просто в нашем случае она создала бы больше проблем чем решений (по-моему субъективному мнению).

    Рассмотрим пример кода, соответcтвующий, приведенному выше, описанию поведения:

    String issueId = issueHelper().createIssue();
    
    List bugIds = new ArrayList();
    int numberOfBugs = Random.getNumberBetween(1, 5);
    while(numberOfBugs>0){
        bugIds.add(issueHelper().createBug(issueId));
        numberOfBugs--;
    }
    
    for(String bugId : bugIds){
        navigator().goto(bugId);
        workflowHelper().doAction("fixed");
    }
    
    navigator().goto(issueId);
    assertThat(getField("status"),is("Closed"));
    


    При выполнении теста, этот код выводит следующее:
    Создать Issue с произвольными параметрами (Ключ нового issue HR-17)
    Создать Bug на issue HR-17 (Ключ нового бага BG-26)
    Создать Bug на issue HR-17 (Ключ нового бага BG-27)
    Перейти на страницу по ключу BG-26
    Выполнить действие fixed
    Перейти на страницу по ключу BG-27
    Выполнить действие fixed
    Перейти на страницу по ключу HR-17
    Проверить что Значение поля status is «Closed»
    Такой тестовый вывод очень похож на cucumber сценарий в моем предыдущем проекте, и может читаться аналитиками/менеджерами (я проверял). Они могут даже проводить своеобразное «code review» того, на сколько соответствует лог первоначальному BDD описанию.

    Потому у нас и получается «BDD наоборот».

    Иногда аналитикам или разработчикам по ходу code review может что-то не понравится, тогда мы можем всего за пару кликов отрефакторить код, например объединить методы «Перейти на страницу по ключу» и «Выполнить действие» в один метод.

    Вы могли заметить, что в коде теста нет никаких комментариев или специальных вызовов, которые могли бы выводить такой лог, и это не случайно.
    Весь вывод лога сделан на основе технологии AspectJ, которая позволяет перехватывать любые вызовы методов и оборачивать их своим кодам. В нашем случае, мы выводим в лог описание метода из JavaDoc, подставляя в него значения параметров, с которыми произошел вызов и, если возможно, то возвращаемое методом значение.
    Мы делаем то же самое, что в технологии Cucumber, с точностью до наоборот. В Сucumber, мы каждому методу сопоставляем регулярное выражение (или шаблон), согласно которому мы будем выделять переменные из текстовой строчки, у нас же, мы сопоставляем шаблон, в который мы будем подставлять переменные в выводимую строчку лога.
    Какой в этом всем смысл?
    С точки зрения BDD, документации и совместной работы всех вместе, в обнимку — не знаю.
    Но я знаю точно, в красивом логе тестов, смысл есть:
    1. Это сильно способствует улучшению качества кода тестов. Правда, очень.
      Но только в том случае, если Вы логируете описания вызовов методов, а не принудительно выводите: log("I did something"). Тогда Вам приходится выделять методы, соответствующие бизнес понятиям, а потом, глядишь, и кода меньше дублируется, и структурирован он лучше. И следить за этим может аналитик, без опыта программирования, который скажет — ваш код оперирует бизнес понятиями, или какие-то непонятные кнопки нажимает, т.е. скатился куда-то, на нижний уровень интерфейсных галочек. Соответственно тесты короче и понятнее.
    2. Это позволяет понять, почему тест «упал».
      Причем мы можем логгировать как вызовы методов первого уровня, т.е. те, что написаны в тестовом сценарии, так и вызовы внутренних методов. И не просто вызовы, а со всеми параметрами и возвращенными значениями.
    3. Иногда проще понять лог, чем код, чтоб разобраться, что же тест делает.
      Я сам в шоке от этого, вроде стараешься, пишешь красиво, рефакторишь, а все равно, через какое-то время код не понятный, а «русский» лог помогает. Ну, значит, есть еще куда стараться и улучшать код. Иногда, кстати, помогает посмотреть на BDD описание, которое мы с доски переписываем в аннотацию к каждому тесту. Эти описания идут в тестовый отчет, потому что они все-таки компактнее и понятнее. Это еще раз меня убеждает, что было правильно разделять такие описания и тестовый сценарий.
    4. Это заставляет писать JavaDoc к тестовому API, хоть какой-то. А это, в свою очередь, упрощает написание тестов, особенно если их пишет не один человек. Интересно, а при использовании Сucumber, можно по Ctrl+пробелу получить подсказки доступных методов, с описанием параметров? Пошел на<Ctrl+Space>
    5. Можно отвлечься и поизучать Aspect Oriented Programming. В качестве идеи на будущее: хочется сделать так, чтобы при вызове doSomething(getSomething()) в лог doSomething() подставлялся не просто результат метода getSomething() (например просто 5), а с описанием из JavaDoc метода getSomething(), чтобы было понятно, что означает этот результат (5) и откуда он взялся. Я этим обязательно займусь, когда меня в очередной раз «все задолбает».

    Если кому-то интересны технические детали: как парсить JavaDoc, как работать c AspectJ, напишите коммент, и я подготовлю об этом отдельный пост, в котором также расскажу, как можно в JavaDoc запихнуть таблицу с тестовыми данными в простой текстовой форме (скопипастив ее из Excel), и сделать так, чтобы тестовый метод вызывался для каждой строчки этой таблицы — это, как раз то, как работает Cucumber, аналитикам это нравится, я же не вижу в этом прелести. А вы что думаете?

    Мораль сей басни такова: экспериментируйте, пишите тесты, и лог вам в помощь.
    image
    Luxoft
    think. create. accelerate.

    Comments 5

      0
      Интересно, конечно.
      Голосую за JavaDoc, AspectJ. А библиотека в виде plug-n-work была бы вобще супер
        0
        Спасибо за простыню, было интересно почитать. У меня есть несколько комментариев:

        1. Согласен, что cucmber для разработчиков неудобен. Возможно он хорошо подходит командам, в которых есть бизнес аналитики желающие поддерживать список историй.

        2. Для девелоперов Cucumber просто и легко заменяется в любом java тесте. Для этого достаточно писать тесты в стиле given-when-then. Например ваш пример я бы отрефакторил так (возможно вы так и пишите в реальных проктах):

        String issue = givenJiraIssueWithBugs();
        whenAllBugsAreFixedIn(issue);
        thenIssueIsClosed(issue);
        

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

        3. Hamcrest, правда, очень удачно вписывается в BDD стиль разработки, так как проверки на нем выглядят почти как предложения на человеческом языке. Я бы добавил в список классных библиотек еще Mockito, которая использует интерфейсы из Hamcrest. В библиотеке LambdaJ тоже много потрясных матчеров, например матчер having-on:
        assertThat(sandwich, having(on(Burger.class).getSauce(), “chili”)
        
          0
          Having хороший матчер, но как разработчику, мне бы хотелось просто написать:
          assertThat(sandwich.getSauce(),is("chili"))
          

          Я бы сделал это так: перехватывал бы вызов sandwitch.getSauce() с помощью AspectJ, брал бы JavaDoc метода getSauce(), его @return аттрибут, и клал бы в специальный map по ключу «chili».
          Потом AspectJ перехватывает метод assertThat, и понимает, что в него передали «chili», лезем в наш специальный map, и распознаем, что это не просто «chili», а соус сэндвича, и выводим соответствующий лог, который более того, может показать, кто этот сэндвич сделал и на какой строчке лога и кода.
          Или так не сработает? Просто мне так делать еще не пришлось, потому что почти все мои проверки записываются assertThatFieldValue( fieldName, matcher);
            0
            Ммм, я согласен, что в данном примере правда проще написать

            assertThat(sandwich.getSauce(), is("chili"))
            

            Удобство появляется тогда, когда трудно добраться до самого поля. Например при работе с коллекциями:

            assertThat(sandwiches, hasItem(having(on(Burger.class).getSauce(), “chili”))
            

            Еще в hamcrest есть родной матчер hasProperty предназначенный для тех же целей, но берущий в качестве аргумента строку. Having получается более refactoring safe, так как работает непосредственно с кодом.
            Использование аспектов, наверное, личное дело каждого разработчика/команды. Я предпочитаю использовать их максимально редко, ибо они создают ощущение магии: видно что что-то происходит, а почему порой не понятно. Хотя в вашем случае, может быть, это самое оно.
          0
          К слову, для .NET есть проект BDDfy, на который я перешел со SpecFlow.

          http://www.mehdi-khalili.com/bddify-in-action/introduction

          Все Given/When/Then хранятся в коде в виде отдельных методов. Сам фреймворк умеет парсить имена методов и выводить лог в виде Given/When/Then при завершении.
          Также в результате формируется HTML отчет, пример которого есть в статье по ссылке выше

          Only users with full accounts can post comments. Log in, please.