Простой, но показательный пример использования TDD

Я, как и многие программисты, довольно много слышал и читал о практиках TDD. О пользе хорошего покрытия кода юнит-тестами — и о вреде его отсутствия — я знаю по собственному опыту в коммерческих проектах, но применять TDD в чистом виде не получалось по разным причинам. Начав на днях писать свой игровой проект, я решил, что это хорошая возможность попробовать. Как оказалось, разницу по сравнению с обычным подходом можно почувствовать даже при реализации простейшего класса. Я распишу этот пример по шагам и в конце опишу результаты, которые для себя увидел. Думаю топик будет полезен тем, кто интересуется TDD. От более опытных коллег хотелось бы услышать комментарии и критику.

Теорию описывать не буду, ее можно легко найти самостоятельно. Пример написан на Java, в качестве Unit-test фреймворка использован TestNG.

Задача


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

Казалось бы, что может быть проще:

public class Unit {

    private int health;
    private int damage;

    public int getHealth() {
        return health;
    }

    public int setHealth(int health) {
        this.health = health;
    }

    public int getDamage() {
        return damage;
    }

    public int setDamage(int damage) {
        this.damage = damage;
    }

}


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

Итак, это то, что получилось традиционным методом «в лоб». Теперь попробуем реализовать тот же класс через TDD.

Применяем TDD


В реальности я не писал приведенную выше реализацию, изначально никакого класса Unit не существует. Мы начинаем с создания класса теста.

@Test
public class UnitTest {

}


Начинаем думать о требованиях к классу юнита. Первое, что приходит в голову — неплохо бы уметь создавать юнит, задавая его здоровье и урон. Так и пишем.

@Test
public class UnitTest {

    @Test
    public void youCreateAUnitGivenItsHealthAndDamage() {
        new Unit(100, 25);
    }

}


Тест, понятное дело, даже не компилируется — делаем так чтобы он прошел.

public class Unit {

    public Unit(int health, int damage) {
    }

}


Рефакторить пока нечего. Пишем следующий тест — я хочу иметь возможность узнать текущее здоровье юнита.

@Test
public class UnitTest {

    @Test
    public void youCreateAUnitGivenItsHealthAndDamage() {
        new Unit(100, 25);
    }

    @Test
    public void youCheckUnitHealthWithGetter() {
        Unit unit = new Unit(100, 25);
        assertEquals(100, unit.getHealth());
    }

}


Тест падает из-за ошибки компиляции — метода getHealth у класса Unit нет. Правим код, чтобы тест прошел.

public class Unit {

    private int health;

    public Unit(int health, int damage) {
        this.health = health;
    }

    public int getHealth() {
        return health;
    }

}


Рефакторить опять нечего. Думаем дальше — наверное было бы неплохо, чтобы юнит умел получать урон.

@Test
public class UnitTest {

    @Test
    public void youCreateAUnitGivenItsHealthAndDamage() {
        new Unit(100, 25);
    }

    @Test
    public void youCheckUnitHealthWithGetter() {
        Unit unit = new Unit(100, 25);
        assertEquals(100, unit.getHealth());
    }

     @Test
    public void unitCanTakeDamage() {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
    }

}


Правим код, чтобы тест прошел.

public class Unit {

    private int health;

    public Unit(int health, int damage) {
        this.health = health;
    }

    public int getHealth() {
        return health;
    }

    public void takeDamage(int damage) {
    }

}


Ах да, полученный урон должен вычитаться из здоровья юнита. Напишу отдельный тест для этого.

@Test
public class UnitTest {

    @Test
    public void youCreateAUnitGivenItsHealthAndDamage() {
        new Unit(100, 25);
    }

    @Test
    public void youCheckUnitHealthWithGetter() {
        Unit unit = new Unit(100, 25);
        assertEquals(100, unit.getHealth());
    }

    @Test
    public void unitCanTakeDamage() {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
    }

    @Test
    public void damageTakenReducesUnitHealth()  {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
        assertEquals(75, unit.getHealth());
    }

}


Первый тест, который падает из-за поведения класса. Правим.

public class Unit {

    private int health;

    public Unit(int health, int damage) {
        this.health = health;
    }

    public int getHealth() {
        return health;
    }

    public void takeDamage(int damage) {
        health -= damage;
    }

}


Тут уже можно немного порефакторить. Тут можно оставить и так, но я привык что геттеры находятся в конце класса.

public class Unit {

    private int health;

    public Unit(int health, int damage) {
        this.health = health;
    }

    public void takeDamage(int damage) {
        health -= damage;
    }

    public int getHealth() {
        return health;
    }

}


Двигаемся дальше. Наш юнит уже имеет запас здоровья и ему можно наносить урон. Научим его наносить урон другим юнитам!

@Test
public class UnitTest {

    @Test
    public void youCreateAUnitGivenItsHealthAndDamage() {
        new Unit(100, 25);
    }

    @Test
    public void youCheckUnitHealthWithGetter() {
        Unit unit = new Unit(100, 25);
        assertEquals(100, unit.getHealth());
    }

    @Test
    public void unitCanTakeDamage() {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
    }

    @Test
    public void damageTakenReducesUnitHealth()  {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
        assertEquals(75, unit.getHealth());
    }

    @Test
    public void unitCanDealDamageToAnotherUnit() {
        Unit damageDealer = new Unit(100, 25);
        Unit damageTaker = new Unit(100, 25);
        damageDealer.dealDamage(damageTaker);
    }

}


Дорабатываем класс юнита.

public class Unit {

    private int health;

    public Unit(int health, int damage) {
        this.health = health;
    }

    public void takeDamage(int damage) {
        health -= damage;
    }

    public void dealDamage(Unit damageTaker) {
    }

    public int getHealth() {
        return health;
    }

}


Понятное дело, если наш юнит нанес урон другому юниту, у того должно уменьшиться здоровье.

@Test
public class UnitTest {

    @Test
    public void youCreateAUnitGivenItsHealthAndDamage() {
        new Unit(100, 25);
    }

    @Test
    public void youCheckUnitHealthWithGetter() {
        Unit unit = new Unit(100, 25);
        assertEquals(100, unit.getHealth());
    }

    @Test
    public void unitCanTakeDamage() {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
    }

    @Test
    public void damageTakenReducesUnitHealth()  {
        Unit unit = new Unit(100, 25);
        unit.takeDamage(25);
        assertEquals(75, unit.getHealth());
    }

    @Test
    public void unitCanDealDamageToAnotherUnit() {
        Unit damageDealer = new Unit(100, 25);
        Unit damageTaker = new Unit(100, 25);
        damageDealer.dealDamage(damageTaker);
    }

    @Test
    public void unitThatDamageDealtToTakesDamageDealerUnitDamage() {
        Unit damageDealer = new Unit(100, 25);
        Unit damageTaker = new Unit(100, 25);
        damageDealer.dealDamage(damageTaker);
        assertEquals(75, damageTaker.getHealth());
    }

}


Свеженаписаный тест падает — поправим класс юнита.

public class Unit {

    private int health;
    private int damage;

    public Unit(int health, int damage) {
        this.health = health;
        this.damage = damage;
    }

    public void takeDamage(int damage) {
        health -= damage;
    }

    public void dealDamage(Unit damageTaker) {
        damageTaker.takeDamage(damage);
    }

   public int getHealth() {
        return health;
    }

}


Наведем немного блеска: переменная damage может быть final, параметр в методе takeDamage неплохо бы переименовать чтобы не путать с переменной класса.

public class Unit {

    private int health;
    private final int damage;

    public Unit(int health, int damage) {
        this.health = health;
        this.damage = damage;
    }

    public void takeDamage(int incomingDamage) {
        health -= incomingDamage;
    }

    public void dealDamage(Unit damageTaker) {
        damageTaker.takeDamage(damage);
    }

    public int getHealth() {
        return health;
    }

}


Дальше нужно писать тесты на то, что здоровье не может упасть ниже нуля, если оно на нуле юнит должен уметь сказать что он мертв и т.д. Чтобы не добавлять лишнего объема, я остановлюсь тут. Думаю для понимания примера достаточно и можно сделать некоторые выводы.

Выводы


  1. На реализацию простейшего класса потрачено времени в несколько раз больше, чем при реализации «в лоб» — это то, что так часто пугает менеджеров и не владеющих данной техникой программистов в TDD.
  2. Можно сравнить первую наивную реализацию и последнюю, полученную через TDD. Главное отличие в том, что последняя реализация действительно объектно-ориентированная, с юнитом можно работать как с самостоятельным объектом, спрашивать его состояние и просить выполнить определенные действия. Код, который будет работать с этим классом также будет более объектно-ориентированным.
  3. Кроме самого класса мы получили полный набор тестов к нему. Знаю по своему опыту, что большего блага для разработчика, чем полностью покрытый тестами код, сложно представить. Из того же опыта, если тесты пишутся после кода, бывает сложно обеспечить полное покрытие — на что-то обязательно забудут написать тест, что-то будет выглядеть слишком простым, чтобы тестировать и т.п. Сами тесты часто получаются сложными и громоздкими, т.к. велик соблазн одним тестом проверить несколько аспектов работы тестируемого кода. Здесь же мы получили набор простых, легких в понимании тестов, которые будет гораздо проще поддерживать.
  4. Мы получили живую документацию к коду! Любому человеку достаточно прочитать названия методов в тесте, чтобы понять задумку автора, назначение и поведение класса. Разбираться в коде, имея эту информацию, будет на порядок проще и не нужно отвлекать коллег с просьбами объяснить что тут к чему.
  5. Предыдущие пункты и так хорошо известны, но я сделал для себя один новый вывод — при разработке через TDD гораздо лучше продумывается что хочется получить от класса, его поведение и варианты использования. Имея хорошее понимание уже разработанных компонент, будет понятнее как писать более сложные.
  6. Не относится к данному примеру, но захотелось добавить сюда еще один пункт. При разработке через тесты адекватнее оцениваешь трудоемкость задач — всегда знаешь что уже работает, что осталось доделать. Без тестов часто возникает ощущение, что все уже написано, но оказывается, что на отладку и доработку нужно еще значительное количество времени.


Я понимаю что пример очень простой и прошу к этому не придираться. Топик не о том как написать класс из двух полей, а о том что можно увидеть преимущества TDD даже на таком элементарном примере.

Всем спасибо за внимание. Изучайте прогрессивные техники программирования и получайте удовольствие от работы!
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 70

  • UFO just landed and posted this here
      +4
      Это всего лишь одна из граней. После 1го проекта с тестами — чувствуешь себя без них голым.

      Плюсов огромное количество и те, что в выводе просто самые очевидные, но к ним можно ещё дописать много бонусов. Можно собраться спроектировать, нарисовать, задокументировать и написать. А потом внести изменение и надеяться, что не упадёт (или проверять весь функционал). А можно писать начиная с тестов (это фактически вместо проектирования) и потом после изменений если упадёт — то ты узнаешь об этом до сдачи заказчику.

      Истерии никакой нет, просто когда человек открывает для себя какой-то удобный инструмент — он пытается поделиться им и знанием с коллегами. Видимо инструмент действительно достойный, раз это выглядит как истерия…
      • UFO just landed and posted this here
          +3
          Никто и не говорит о панацее. Я лично для себя их использую чтоб быть уверенным, что после рефакторинга всё будет работать. Мне не нужно будет что-то проверять, я просто запущу тесты и узнаю работает или нет. Я пишу тест один раз и потом всегда (весь жизненный цикл программы/модуля/класса) уверен в этом участке кода. В итоге это выливается в экономию того самого времени поиска неработающих частей, но да, это всё будет когда потом — сейчас вы действительно тратите время на очевидные и ненужные в данный момент вещи…
            –3
            При разработке в Agile конец спринта предполагает наличие готового, рабочего, самостоятельного кода/модуля. Т.е. вот если сейчас работает, все — дальше его не трогают, не рефакторят. Все риски минимизируются качественным планированием, ревью кода, гейтед чекинами.

            Вопрос: зачем тратить время на написание тестов для кода, который работает?

            Лично я, глубоко внутри, осознаю потенциальную полезность юнит-тестов, но моя практическая часть отказывается тратить на них время. Тесты пишу, но пользы от них не вижу. Как-то так.
              +4
              я смотрю вы и команда идеальные программисты! Пишите без ошибок всегда. А agile та еще фигня, больше трепа, чем действий
                0
                Неустойчивость требований влечет необходимость обсуждений и внесения изменений.
                Чем выше степень неустойчивости — тем органичнее смотрится Agile. И наоборот.
                  +1
                  Про Agile все-таки наоборот — минимум трепа, максимум действий.
                    0
                    При agile — максимум трепа, максимум действий.
                    Гляньте agile manifest на тему communication.
                      0
                      Максимум трепа — это о чем?
                  0
                  Ничего не имею против Agile, но как в одной из статей заметили:
                  «В русской расскладке „Agile“, звучит как „Фпшду“, что как-бы настораживает»(с) из одной из множества хабро статей.
                    0
                    Как вы распознаете код как рабочий? Держу пари, вы его тестируете. Раз не пишете тесты, значит тестируете руками. Ок, вы провели новую итерацию, вы гарантируете, что код рабочий? Нет, пока не протестируете. Или вы переписали кусок кода (да-да, такое случается, когда проект развивается). Где гарантии, что где-то что-то не отвалилось? Опять нужно тестировать. А теперь вопрос на смекалку: кто быстрее проверит код, вы или RSpec?
                      0
                      Увидел в начале комментария слово «Agile», и подумал, что он будет в поддержку тестирования. Наверное, я совсем не понимаю, что такое Agile. Как можно практиковать Agile без тестирования? Agile ведь подразумевает, что код будут рефакторить?.. Или вы просто упомянули Agile, но сами его не практикуете?

                      Да, я знаю, что в манифесте Agile нет про тестирование, но не понимаю, как можно строить процесс разработки, основанный на непрерывном рефакторинге, без использования тестов. Сам я в тех моментах, где приходится делать рефакторинг, уже сильно запарился ручками проводить тесты, и всё-равно вылезают баги. На собственном опыте убедился в необходимости тестирования.
                +3
                Спасибо, что обратили внимание на этот момент. Действительно, выглядит так, что я сравниваю самую тупую реализацию с более продуманной и отношу это к преимуществам техники TDD. Безусловно можно было бы немного напрячься и написать сразу то же самое без тестов, но
                1. Если я уже продумываю юз-кейсы, почему бы их тут же не записать?
                2. Лично я бы не стал выдумывать юз-кейсы класса, а начал бы писать класс-пользователь юнита — боевую сцену, выясняя что ей требуется от юнита и добавляя по ходу. Это провоцирует писать сначала процедурный код типа damageTakerUnit.setHealth(damageTakerUnit.getHealth() — damageDealerUnit.getDamage()) вместо damageDealerUnit.dealDamage(damageTakerUnit), а потом уже рефакторить. На реальных проектах я достаточно часто видел как написав работающий процедурный говнокод, на рефакторинг забивают. Последствия в долгосрочной перспективе весьма печальные.
                3. У этого класса сначала 3 юз-кейса: создать, продамажить, попросить продамажить другой юнит. Их действительно просто продумать и закодить без тестов. Когда нужно будет добавить еще несколько, это уже не будет так просто. Наличие тестов позволит добавлять юз-кейсы инкрементально, не беспокоясь, что предыдущие сломаются


                Я ни в коем случае не пытаюсь сказать что TDD это единственно правильный способ писать программы, тем более что ни одной программы через TDD в чистом виде я не написал. Я пробую инструмент, вижу определенные его преимущества и мне этот опыт показался достаточно интересным, чтобы им поделиться.
                • UFO just landed and posted this here
                    0
                    Это вопрос уже про процедурное vs ООП. Мне ООП однозначно ближе. В описанном случае я бы просто раздавал глобальный модификатор всем юнитам — в конкретном случае моей боевой системы может быть максимум 16 юнитов в бою. В абстрактном случае, когда бы это не подошло (сотни или тысячи юнитов, глобальный модификатор часто меняется) завел бы отдельный объект глобальных модификаторов и при создании юнита давал бы на него ссылку — это как пример, но абсолютно точно я бы не стал выносить расчет урона из юнита. Процедурный подход дает некую гибкость, пока проект маленький, но когда проект вырастет я хочу чтобы всё, что может произойти с юнитом, было описано в классе юнита, а не в пяти разных местах, где он используется.
                    • UFO just landed and posted this here
                      • UFO just landed and posted this here
                          0
                          Мы применяли такой подход в одном из проектов, писали тесты только на веб-сервисы, не трогая более мелкие классы. Опыт вполне успешный — отлавливалась куча ошибок, облегчалась поддержка.
                          Я сейчас хочу попробовать, хотя бы в качестве эксперимента, другой подход, когда весь код пишется через тесты. Когда (и если: ) доведу проект до какого-то рабочего состояния, обязательно напишу про этот опыт.
                            0
                            Соглашусь насчет параноидальности тестирования. Но выскажу пару моментов как я на них смотрю:
                            1) нужно и думать что тестировать, то есть под приоритет тестирования подпадает внешний интерфейс класса, ну или максимум еще и защищенный, такой который используется из вне, и лишь в редких случаях можно писать тесты на особо сложные внутренние методы, с особенно сложной логикой, где вероятна ошибка. Это позволяет сократить количество тестов, продумать именно внешний API класса, который важен для системы в целом, так как через него другие классы взаимодействуют с тестируемым, и если была допущена ошибка во внутренних методах, используемых во внешнем — это будет заметно по падению тестов на внешние классы;
                            2) автор конечно привел пример разработки совершенно простого класса по методике TDD. Он писал unit-тесты, но ведь кроме unit-тестирования есть и другие методы тестирования кода, кроме unit. Так что более глобальное взаимодействие можно тестировать написанием к примеру интеграционных тестов.
                            3) как вариант использовать не более приземленный TDD подход, а более абстрагированный BDD подход, для более высокоуровневых тестов и собственно тестирования приложения с точки зрения пользователя к прмиеру.
                              0
                              Насчет BDD Вы совершенно правы.
                              Этот подход действительно приносит большую пользу, в отличие от более слабого TDD.
                              Документирующие тесты, которые можно читать непрограммисту — это стоит того, чтобы применять данный подход.
                              Вот только это должны быть не тесты реализации, а тесты спецификации, протокола взаимодействия.
                              Иначе это не BDD, и коэффициент полезности будет недалек от нуля.
                                0
                                Cпасибо за поправку насчет спецификации (=
                              0
                              Есть огромная куча типов тестирования, в данном случае функциональное тестирование как нельзя лучше подходит. Можно делать более «высокоуровневое » тестирование, но тогда возрастает вероятность пропустить баг. Для такого типа тестирования обычно нанимаются тестировщики и тест дизайнеры.
                            0
                            Когда проект вырастет я хочу чтобы всё, что может произойти с юнитом, было описано в классе юнита, а не в пяти разных местах, где он используется.


                            IMHO, Вы еще не достигли просветления.
                            Все, что может произойти с unit'ом, в один класс может оказаться очень трудно упихать.
                            И это будет напоминать именно упихивание, а не архитектуру.
                            ООП придуман для распиливания обязанностей на части.
                            Если обязанностей становится много — необходимо разделить по принципу единой ответственности.
                              0
                              IMHO, Вы еще не достигли просветления.

                              Я и не претендую =) С каждым новым проектом понимание растет и меняется.

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

                              Тут речь идет не о том, чтобы запихать весь код в один класс, но все логические операции с юнитом должны делаться через интерфейс юнита. Мой опыт говорит за то, что такой подход лучше всего помогает понимать и поддерживать код.
                                0
                                Насчет интерфейса тоже не совсем верно.
                                Раздувание интерфейса часто приводит к разрушению его целостности.
                                Хотя имеются примеры очень больших, но продуманных целостных интерфейсов (например, ResultSet в Java).
                        0
                        Чтобы продумать конечно писать тесты не обязательно. Можно писать комментарии или следовать не TDD, а BDD, но суть одна. Если человек действительно привык и приучился покрывать код тестами перед началом написания кода, то он уже автоматически перед написанием теста задумается, что должен делать его код. Помимо немного потраченного времени на тесты сейчас, можно получить такие профиты как: код покрыт тестами, значит легче рефакторить проект (как бы идеально проект не был написан, проект развивается и так или иначе будет переписан тот или иной кусок кода, причем не факт, что изначальным автором кода), мы получаем более продуманный код, и конечно же мы получаем своеобразную документацию, которая описывает, что этот код сделает.

                        Так что, я сказал бы, что да, можно просто хорошо продумывать код до его написания и без тестов, просто при TDD/BDD мы получаем еще некоторые плюшки и самодисциплина написать тест будет лишним мотиватором продумать код.
                        +2
                        я пожалуй выскажусь насчет темы, а то вдруг кто-то и правду решит использовать как есть.

                        самое больное:
                        0) BDD/TDD и прочее — ориентируйтесь на голову, а не тактику. я понимаю, что пример простой. но прежде всего — голова должна быть. BDD должно помогать, а не тормозить и связывать руки. так что ищите аргументы за/против в каждом локальном случае.
                        1) не аргумент в пользу. менеджер будет паниковать независимо от выбранного метода работы
                        2) опять ООП. после знакомства с mockito обычно приходит понимание что ООП не то чтобы не критично, а просто неважно. а в реальной жизни (т.е. проектах) ООП используется чаще для абстракции (я имею ввиду миксины). не беру в расчет разработку API — это не тот уровень.
                        3) покрытие кода проверяется. это не сложно. и если вы напишете сеттер не правильно — то упасть должен не тест «на сеттер», а половина остальных тестов. плюс один простой тест погоды не сделает.
                        4) живая документация. как много в жтом слове. я очень рад что jdk писалось с использованием javadocs. Вы правда верите что человек полезет смотреть «все» тесты для этого класса или метода, особенно если каждый тест должен быть простым и проверять отдельный аспект? документация должна быть, если она требуется.
                        5) требования рулят. добавить нечего.
                        6)… и это никак не поможет объяснить менеджеру, почему опять задержка. сарказм, что ли. это никак не относится к BDD, потому что — в любом подходе у тебя есть код и тесты, неважно что из них написано раньше по времени

                        поясню — смотрю на это не с точки зрения сферического коня в вакууме, а в контексте реальных сделанных и текущих проектов. в конце концов — все там будем.

                        а вообще — развлекайтесь, что уж.
                          0
                          Всё правильно, именно поэтому, как я и написал, в чистом виде TDD применять не приходилось. На своем проекте можно попробовать, это же хобби. Когда проект станет достаточно большим, отпишусь об опыте.
                          +2
                          Разве это BDD? Я думал, BDD — это описание действий пользователя, причем оперируя сущностями, доступными пользователю. А в топике — TDD.
                            0
                            На мой взгляд в тестах как раз описания возможных действий с классом пользовательским кодом. Можете написать пару тестовых методов на класс Unit для примера, как они выглядели бы в вашем понимании BDD?
                              +2
                              Да нет же, BDD оперирует не классами, а действиями пользователя.

                              image
                              TDD тестирует то, что под капотом, BDD — то, что снаружи. TDD проверяет методы, BDD — user stories.

                              TDD является частью BDD:
                              image
                              Картинка кликабельна
                                0
                                Ради красоты картинки нарисовали какой-то чудовищный процесс, когда в одном цикле постоянно пишем падающие тесты, а во втором параллельно пытаемся их починить %)

                                Но вы правы (почитал внимательно другие источники), BDD работает на более высоком уровне. Я был свято уверен, что BDD — тот же TDD, но с другой нотацией названий тестов и их написания (given-when-then). Вы открыли мне глаза, спасибо! Уберу упоминание BDD из топика.
                              –1
                              Не обязательно. Тут где-то была статься от Саши Косса (а, вот она — habrahabr.ru/company/evilmartians/blog/149335/ ), где он довольно наглядно указал на отличия TDD от BDD.
                              При юнит-тестировании чаще всего мы имеем дело с объектами и функциями, с которыми пользователь напрямую не соприкасается. Но описывая в тестах их _поведение_, мы используем именно BDD-подход.
                                0
                                Ответил предыдущему оратору. Дополняю.

                                В свою поддержку цитирую приведенную вами статью:
                                В BDD мы смотрим на это под другим углом:

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

                                Для TDD и BDD используются разные инструменты. К примеру, книга The RSpec Book описывает использование TestUnit (MiniTest) или RSpec для TDD и Cucumber для BDD — в едином процессе, как на майрософтовской даграммке.
                                  0
                                  Найти в статье слово «пользователь» и привести цитату с ним — не аргумент. В той же статье описывается BDD-подход к тестированию класса, где действиями пользователя и не пахнет.
                                  Если бы BDD-практика оперировала лишь действиями пользователя, ее бы назвали Use Flow Driven Development, или как-то так. BDD — это не «когда Cucumber», для BDD можно с тем же успехом использовать RSpec. Коль скоро мы переходим от test(«call function with n arguments») к it(«should do blablabla»), мы переходим от TDD к BDD.
                                  Другое дело, что тот же Cucumber для TDD не приспособлен в принципе, но это уже другой разговор.
                                    0
                                    Замена синтаксиса test(«call function with n arguments») к it(«should do blablabla») вообще ничего не значит. Те же яйца, только в профиль. Что же касается смены подхода, описанной в сабже, — это уже значимо, но тем не менее это лишь более продуманный подход к TDD.

                                    BDD — это нечто большее. И Cucumber для TDD именно потому и не приспособлен, что решает принципиально иную задачу.

                                    Если не верите статье Microsoft, вот картинка «Цикл BDD» из The RSpec Book: Behaviour-Driven Development with RSpec, Cucumber, and Friends:
                                    image
                                    (кликабле)

                                    Почитайте ну хотя бы на Википедии, как в BDD описываются тест-кейсы. Юнит-тестированием там и не пахнет.
                                    en.wikipedia.org/wiki/Behavior-driven_development#Behavioral_specifications
                                    Если надо, могу перевести выдержку из этого параграфа на русский.
                                      0
                                      Картинка показывает использование RSpec для.юнит-тестирования и Cucumber для интеграционных тестов. Почти в самом начале The RSpec Book дано определение BDD, и оно никоим образом не относится к «user story».

                                      В приведенной же статье из вики приведен пример BDD-спеки для RSpec: en.wikipedia.org/wiki/Behavior-driven_development#Story_versus_Specification
                                        0
                                        > Почти в самом начале The RSpec Book дано определение BDD, и оно никоим образом не относится к «user story».

                                        Эта книга сначала рассказывает о юнит-тестировании и только во второй главе начинается речь о BDD. После подробной постановки проблемы, на 121-й странице дается «A Description of BDD» (описание BDD), и звучит оно так:

                                        Behaviour-Driven Development is about implementing an application by
                                        describing its behavior from the perspective of its stakeholders.


                                        BDD — это разработка приложения через описание его поведения с точки зрения заинтересованных лиц.
                                        0
                                        Стоит сказать, что ни в коем случае не отвергаю использование «user stories» для интеграционных тестов (там они более чем удобны). Но я категорически не согласен с мнением, что BDD — это только интеграционные тесты.
                                          0
                                          Ну ок, а что тогда BDD такое? Это интеграционные тесты с использованием Capybara (к примеру), и все остальные, где используется такой синтаксис?
                                          it "should..." do
                                          # ...
                                          end
                                          

                                          Тогда у нас какая-то нечеткая разница получается.
                                            0
                                            А разница тут действительно довольно нечеткая, отсюда и столько споров. Из той же вики:

                                            > At its core, Behavior-driven development is a specialized version of test-driven development which focuses on behavioral specification of software units.

                                            Основной причиной создания BDD послужило то, что глядя на TDD-спеки не всегда понятно, что должен выполнять тот или иной модуль. Глядя же на BDD-спеки, мы видим практически документацию, руководство к действию.
                                              0
                                              Основной причиной создания BDD послужило то, что глядя на TDD-спеки не всегда понятно, что должен выполнять тот или иной модуль.

                                              Не понятно для кого, для клиента? Хм… В этом случае, ему будут понятны действительно только интеграционные тесты, если не использовать «второй круг» с Cucumber.
                                              fill_in 'e-mail', with: 'test@test.com'
                                              fill_in 'password', with: 'password'
                                              click_link 'Log In'
                                              page.should have_content 'Logged in!'
                                              

                                              Это должно быть понятно абсолютно всем, и можно показывать заказчику. А вот тесты контроллеров, моделей, напичканные проверками на внутренние методы, использующие моки, стабы — там заказчик точно не разберется. Получается, BDD — понятно написанные интеграционные тесты + Cucumber, если он есть.
                                                0
                                                Да даже самому программисту. Много вот Вы поймете из теста, где описаны результаты без объяснений, откуда они взялись (собственно, описания _поведения_)?
                                                  0
                                                  Не могу ответить на этот вопрос к сожалению, тестированием до RoR не занимался, а с использованием RSpec и общепринятых практик как-то все само понятно пишется, и таких вопросов не возникает. Просто в голове была ассоциация, BDD = TDD + тесты, которые можно показать заказчику.
                                                    +1
                                                    BDD не включает в себя TDD, а является его потомком. Оперируя терминами Ruby, TDD — это родительский класс для BDD, а не подключаемый модуль :)
                                                      0
                                                      Теперь более-менее понятно, спасибо :-)
                                            0
                                            Я нигде не говорил, что «BDD — это только интеграционные тесты»! При чем тут вообще интеграционные тесты? Cucumber к ним никакого отношения не имеет.

                                            Я пытался объяснить, что BDD — это не просто иной, более продуманный способ делать TDD. BDD — это методика разработки, включающая TDD, а также еще один слой тестов, оперирующий концепциями, доступными пользователю.

                                            Первый абзац статьи о BDD в Википедии:
                                            Behavior-driven development … provides software developers and business analysts with shared tools and a shared process to collaborate on software development.
                                            BDD позволяет бизнес-аналитикам принимать участие в разработке вместе с программистами, предлагая им для этого общие инструменты и единый процесс.

                                            Это главное в BDD и является его сутью.

                                            BDD включает в себя TDD в качестве одного из компонентов процесса. Конечно, стиль юнит-тестов в TDD при этом меняется и становится направленным на поведение (сабж как раз расказывает о таком изменении стиля TDD). Именно об этом и говорится в той части статьи, на которую вы ссылаетесь как на «пример BDD-спеки для RSpec»:

                                            …specification-based testing is seen in BDD practice as a complement to story-based testing and operates at a lower level. Specification testing is often seen as a replacement for free-format unit testing.
                                            В методике BDD, тестирование при помощи спецификаций дополняет тестирование при помощи историй и работает на более низком уровне. Тестирование при помощи спецификаций часто рассматривается как замена юнит-тестированию в свободной форме.
                                              0
                                              От того, что юнит-тесты пишутся не в виде user stories, они не перестали быть BDD. Как я написал в комменте выше, BDD — это развитие TDD, а не дополнение.
                                                0
                                                То, что вы называете BDD, в терминах статьи Википедии назвается разработкой через тестирование при помощи спецификаций. Тестирование при помощи спецификаций, несомненно, является важным развитием юнит-тестирования.

                                                Но TDD от этого не превращается в BDD, как не превратится в конвейер автомеханик от того, что упорядочит процесс сборки автомобиля.
                                                0
                                                На мой взгляд, не корректно говорить, что BDD включает TDD. BDD — это скорее частный случай TDD, с особым стилем написания тестов, ориентированным на тестирование поведения. Написание спецификаций низкого уровня (те что аналогичны юнит тестам) на RSpec-е это тоже часть BDD, а вы называете это почему-то уже TDD.

                                                На картинке что вы приводили, изображены внешний и внутренний цикл BDD, использующиеся при подходе outside-in, применяемом в BDD. Вы пытаетесь выдать внешний цикл за BDD, а внутренний за TDD, но на самом деле, оба они представляют BDD. По большей части, я согласен с DarthSim, не понятно почему его комментарии минусуют.
                                                  0
                                                  Да нет же.

                                                  Я не говорил, что BDD — это внешний цикл. Но я согласен, что говорить «BDD включает в себя TDD» — не совсем корректно. Исправляюсь.

                                                  Внутренний цикл — это юнит-тестирование (тестирование при помощи спецификаций). Внешний цикл — это тестирование при помощи историй. Все вместе — BDD.

                                                  Тестирование при помощи историй оперирует концепциями пользователя, а не кода, и позволяет включить в процесс разработки бизнес-аналитиков. Это, по моему мнению, и является существенным.

                                                  Если исключить из BDD внешний цикл, получится TDD.

                                                  DarthSim же пытается доказать (если я, конечно, правильно понял его точку зрения), что внешний цикл несущественен, а определяющей чертой BDD является стиль написания юнит-тестов во внутреннем цикле.

                                                  PS Я ставлю минус тем комментам, с которыми совершенно не согласен. В карму поставил плюсы.
                                                    0
                                                    Может я не правильно понял, но создалось такое ощущение по вашим предыдущим комментариям.

                                                    С тем что вы сейчас говорите, я почти согласен. Но вот мне кажется, что не стоит завязывать внешний цикл на тестирование при помощи именно историй и Cucumber.

                                                    Мне думается, это могут быть любые acceptance tests (для Руби — rspec, rspec + capybara/steak), описывающие высокоуровневые фичи (feature) в терминологии заказчика. Т.е. где название теста (плюс всякий сахар типа вложенности контекстов, сценариев и т.д.) как бы создает описание высокоуровневой фичи или целой пользовательской истории. Но, в моем понимании, BDD не требует того, чтобы бизнес аналитик мог читать (или еще хуже писать) код таких тестов (хотя, возможно, существуют и такие радикальные конфессии в BDD). Так как, по-моему, тут основная ценность в том, чтобы увидев, какие тесты внешнего круга падают, а какие работают, заказчик (или бизнес аналитик) мог понять, какие фичи в приложении реализованы, а какие нет. Т.е. чтобы вместо ошибки «падает какой-то ассерт в классе отправки почты», он видел что фича «Отправлять пользователю письмо с при регистрации» не работает. А как код такого теста реализован внутри (Cucumber или хоть читый RSpec), уже вторично.

                                                    Поэтому я думаю, что BDD не перестанет быть BDD если вместо Cucumber для тестов внешнего круга использовать даже чистый RSpec. Главное, чтобы эти тесты действительно оставались тестами внешнего круга.

                                                    PS: И, действительно, такие acceptance тесты очень похожи на интегральные тесты. Я бы даже сказал, что это что-то вроде интегральных тестов для заказчика (бизнес аналитика).
                                                      0
                                                      Но вы согласны, что без внешнего круга BDD превращается в TDD, пускай в новом качестве?

                                                      В подтверждение этого утверждения привожу цитату в комменте выше.
                                                        0
                                                        Хмм, а как быть с разработкой, предположим, gem'ов или бандлов? Имхо тут внешний круг отойдет сам собой без ущерба концепции. Хотя, смотря что понимать под внешним кругом.
                                                          0
                                                          И да и нет. Просто сами понимаете, что на практике круг может быть не всегда круглым, и не совсем внешним. Например, может быть вообще только один круг, а может быть три круга или пять кругов. Это противоречит BDD или принципу outside-in? На мой взгляд это все условности. Поэтому может превратиться в TDD, может в недоBDD, а может вообще превратиться в новую методологию.

                                                          А определение из книжки, которое вы привели, мне очень нравится: короткое, точное, емкое. А, главное, исходит от создателя(ей) BDD. Предлагаю сосредоточиться на нем, а не на условностях типа фреймворков и всяких там кругов. Фреймворки и круги просто позволяют на практике это быстро понять, попробовать и почуствовать.
                                                            0
                                                            Не могу возразить!

                                                            Но рассудите нас, пожалуйста, в вопросе, использует ли топикстартер BDD или нет.
                                                              0
                                                              Да я топик особо не читал. Так, увидел начинающийся срач в комментах и решил вписаться.

                                                              Если серьезно, я не берусь вас рассуждать, просто выскажу свое мнение: BDD тут и не пахнет. Однако, не все так просто. На мой взгляд, на таких синтетических примерах нельзя однозначно ничего утверждать. Тут можно слишком многое домыслить. Например, если предположить, что автор — единственное заинтересованное лицо (и будущий пользователь, заказчик), а система которую он разрабатывает — это один единственный юнит, то получается что это уже BDD по всем формальным признакам. Я бы даже сказал что при таких условиях, в этом частном случае TDD == BDD. Но это уже какая-то демагогия. А по существу я с вами согласен.
                                                                0
                                                                Сори, не совсем туда ответил
                                                          0
                                                          Господи-боже нет!) Мы в итоге друг друга не поняли и доказывали дру другу одно и то же с разных концов. Я понял, что Вы считаете, что BDD — это Cucumber и баста, а все прочее — уже «не щитово».

                                                          Собственно, непонимание началось после утверждения, что у топиккастера TDD, ибо юнит-тесты.
                                                            0
                                                            Ну так я и утверждаю, что в топике — классический TDD.

                                                            Одним из трех основополагающих принципов BDD (та же 121-я страница The RSpec BooK) является «Up-front planning, analysis, and design» (планирование, анализ и проектирование наперед). Топикстартер же честно признался, что когда он начал кодить, он сам еще не знал, что, собственно будет делать его класс. Он проектировал на ходу, то есть по мере разработки.

                                                            Так что BDD у топик-стартера нет и в помине (как, видимо, его нет и в статье, на которую вы первоначально сослались). Нет ножек — нет мультиков.: Р
                                                              0
                                                              So allow me to retort! © :)… хотел было написать я, но перечитал таки тесты у топиккастера. Да, там даже по моим меркам как-то не BDD.

                                                              А Косс намеренно упростил в статье тесты, чтобы не пугать неокрепшие умы начинающих. Конечно, для проверки поведения нужно было мокать функцию-параметр и проверять вызовы, а не результат (TDD-way).
                                          0
                                          Эх, вот если бы рассмотрел кто-нибудь пример корпоративного приложения, где логики не так много, а данных — море…
                                            0
                                            Кстати, за холиваром забыл сказать: спасибо за замечательную статью. Когда я впервые вникал в TDD, мне такой статьи очень нехватало.
                                              0
                                              И вам спасибо за ценный фидбек. Честно говоря не расчитывал на такое внимание, думал топик будет любопытным, но судя по количеству добавивших в избранное тема людям действительно интересна. Когда созреют новые интересные мысли, обязательно напишу еще.
                                                0
                                                Смотрите мой комментарий выше! Смотрите мой комментарий выше!
                                              0
                                              Спасибо за хорошую статью! Просто, но понятно, и задаёт направление для дальнейшего развития. Я сам пока только примеряюсь к юнит-тесированию, и тоже хочу попробовать начать с чего-то такого вот.

                                              Я бы пару тестов объединил в один, например, unitCanTakeDamage и damageTakenReducesUnitHealth. Не понимаю, зачем отдельный первый тест? Он же тестирует только наличие метода takeDamage(), но не его работу. Второй тест проверит и то и другое. Не будет ли разделение на два отдельных теста чрезмерным стремлением затестировать всё подряд? Или именно в этом и заключается TDD? У того же Кента Бека во всех тестах есть assert-ы.

                                              Интересно, что в комментариях больше говорили о тестировании в целом, чем о примере применения тестирования в статье, нет комментариев по делу от гуру TDD. А именно этого просил автор, и хотел бы увидеть и я.
                                                0
                                                С другой стороны, тест unitCanTakeDamage тестирует не только наличие метода, но и фиксирует его интерфейс, и если вдруг кто-то добавит ещё один параметр к методу, то тест непременно упадёт, и сразу будет понятно, в чём именно дело. Что довольно ценно, на мой взгляд.
                                                  0
                                                  Не понимаю, зачем отдельный первый тест? Он же тестирует только наличие метода takeDamage(), но не его работу. Второй тест проверит и то и другое.
                                                  Тесты ведь не только проверяют работу кода, но и являются документацией к нему. В этом смысле наличие отдельного теста, фиксирующего наличие и сигнатуру метода, делает чтение этой «документации» более плавным — сначала вы узнаете, что есть такая фича, потом читаете о ней подробнее.
                                                  Это, конечно, не принципиальный момент, я никогда не пишу такие тесты специально, они появляются сами собой «мне тут нужен такой-то метод — напишу тест, теперь этот метод делает такую-то штуку — напишу следующий тест».
                                                    0
                                                    Нужно уменьшать количество тестируемого. Точнее, нужно выкидывать тесты на очевидные и проверяемые другими тестами данные. Например, не нужно проверять наличие класса и возможность его инициализировать, т.к. без класса отвалятся все остальные тесты, а значит, этот факт уже протестирован. то же и с методами. Если у вас есть хоть 1 тест на проверку поведения метода, тот этот тест уже проверяет его наличие.
                                                    Из документации это еще очевиднее. Если в документации появляется описание метода, очевидно, что он существует. Я лично ни в одной доке не видел строчки «этот метод есть».
                                                    Если же вы являетесь адептом хардкорного ни-строчки-кода-без-теста TDD, то пишите тесты на наличие, а после написания кода — удаляйте их.

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