Тесты вперед?

26 октября 2008 г.

Многие меня спрашивают: "Не жалко тебе тратить время на написание тестов?". Могу ответить коротко: "Нет".

Почему я могу позволить тебе половину своего рабочего времени посвятить тестированию? Неужели не лучше сразу писать код? Подождите! Не сразу, конечно. Надо накидать архитектуру. Должны же мы понимать, как и что мы хотим сделать. На сколько детально надо проработать архитектуру? Опишем основные "слои" системы и их взаимодействия? Или будем разбивать эти слои на объекты и решать, как этим объектам себя вести?

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

Мы будем используем понятные нам всем термины. Общий язык, на котором мы сможем договориться. К примеру - UML.

Создадим консольный калькулятор?

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

Я говорю: "Я вижу объект Calculator. Ему на вход передается строка. Он может совершать операции над операндами. Операнды поступают в виде массива строк, которые ввел пользователь."

Мне возражают: "Как он распарсивать будет? А давайте ему добавим метод Parse?..." При этом на доске уже начали появляться первый диаграммы взаимодействия. Работа идёт полным ходом...

Что мы делаем? Договариваемся о том, какой будет код? Кое будет API? А где код? Как его увидеть?

Я вижу его по-своему. Эти объекты и их поля, как они взаимодействует. В моей голове уже все в движении, сообщения передаются, исключения перехватываются, работа идёт полным ходом. Мои коллеги сидят и вкушают триумф разбора строки пользователя. Кто-то уже начал думать над форматом выдачи результата. О том, как лучше пользователю это показать, что написать при делении на ноль.

Как мне передать мои "ощущения" кода коллегам? Нужно показать им этот код! Тогда уже начнётся более предметный разговор. После разговоров и рисования диаграмм мы расселись по местам и начали кодировать.

Кто-то скажет: "Да нет, это же не эстетично вызывать Calculate с тремя параметрами. Давайте создадим отдельную функцию Add, которая будет принимать два операнда?"

Зачем мы рисовали диаграммы, где все уже расписано и так красиво выглядит? После первой строчки кода они потеряли свою актуальность.

Альтернативный подход.

Забудем на время все диаграммы со стрелочками и условными обозначениями. Сейчас нам пока не о чем уславливаться. Ещё ничего нет. Давай синхронизируем наше "видение" проблемы.

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

Я говорю: "Хочу, чтобы был объект Calculator и он принимал на вход 3 параметра".

Мой коллега (К) отвечает: "Покажи мне код, который ты хотел бы использовать"

[Fact]
public void AddTest()
{
    var calculator = new Calculator();
    calculator.Calculate(?
}

(Я): "А откуда возьмутся аргументы?"

(К): "Мне кажется их надо передать в конструктор".

Я продолжаю печатать.

[Fact]
public void AddTest()
{
    string[] args = new {"1", "+", "2"};
    var calculator = new Calculator(args);
    calculator.Calculate(?
}

(К): "Я бы предпочел вызвать конкретную операцию. Например для сложения Add"

(Я): "Я поймал себя на той же мысли =)..."

[Fact]
public void AddTest()
{
    string[] args = new {"1", "+", "2"};
    var calculator = new Calculator(args);
    calculator.Add();
}

(К): "А почему именно Add? Может я передал знак вычитания. Вообще говоря я вижу, что твой класс Calculator имеет определенное поведение, но не могу понять где его состояние?"

(Я): "Т.е. нет смысла создавать его экземпляр, когда нам надо складывать?"

(К): "Я вижу это так" (берет себе клавиатуру)

[Fact]
public void AddTest()
{
    string[] args = new {"1", "+", "2"};
    Calculator.Add(args);
}

(Я): (косо глядя на напарника) "Передвать аргументы в Add? Ты же не хочешь сказать, что каждая функция калькулятора, будет разбирать этот массив строк и решать, что с этим делать дальше? Вообще почему ты решил, что разбираться с вводом пользователя - это работа для калькулятора?"

(К): (ударил себя по лбу) "Нам нужен парсер этих аргументов!"

(Я): "Покажи мне код!"

[Fact]
public void AddTest()
{
    string[] args = new {"1", "+", "2"};
    var parser = new ArgumentParser(args);
    double result = Calculator.Add(parser.FirstOperand, parser.SecondOperand);
    Assert.Equal(3, result);
}

И так далее...

Обратите внимание, что все программирование велось в сборке UnitTests и класс Calculator создан не был. Мы просто придумывали каким мы хотим видеть его API.

Результаты:

  1. Приложение работает на "красивом" API
  2. Дизайн системы намного лучше, чем если бы мы вылили свое воображение на бумагу, а уже потом, придерживаясь его, начали программировать.
  3. Код получается изначально тестабильным (testable).

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

Обсуждение этой темы на blogs.gotdotnet.ru

19 комментариев:

  1. @Idsa
    Это обычный подход, одна из практик XP, называется приемочное тестирование

    К TDD отношения не имеет.

    ОтветитьУдалить
  2. Эм... Такое ощущение, что ты через строчку прочитал :)
    >>Это обычный подход, одна из практик XP, называется приемочное тестирование

    Да, acceptance-тесты - обычная практика. Но разве XP говорит, что acceptance тесты должны писаться до unit-тестов?

    >>К TDD отношения не имеет.

    Да, TDD касается unit-тестов, а не acceptance-тестов. Но ты посмотри на название своей статьи - "Тесты вперед" (просто тесты). Вот я и решил поинтересоваться, как ты относишься к идее создания acceptance-тестов до unit-тестов.

    ОтветитьУдалить
  3. @Idsa
    > Но разве XP говорит, что acceptance тесты должны писаться до unit-тестов?
    Да, как раз для этого они и придуманы. Могу сослаться на любую книжку по XP.

    > как ты относишься к идее создания acceptance-тестов до unit-тестов
    Хорошо отношусь, только это далеко не всегда возможно. Как ты например напишешь эти тесты для веб-приложения?

    ОтветитьУдалить
  4. >>Да, как раз для этого они и придуманы. Могу сослаться на любую книжку по XP.

    Сошлись, пожалуйста (мне для ликбеза).

    >>Хорошо отношусь, только это далеко не всегда возможно. Как ты например напишешь эти тесты для веб-приложения?

    А в чем разница между десктопными и веб-приложениями в контексте acceptance-тестирования? На входе у нас некое user-friendly представление юзкейсов, которым должно удовлетворять наше приложение. Ни один тест не проходит (мы еще ничего не написали). Далее начинаем писать в режиме TDD, уже отталкиваясь от acceptance-тестов. Со временем acceptance-тесты начинают проходить.

    ОтветитьУдалить
  5. @Idsa
    Проблема в том, что приемочные тесты можно писать для библиотек, у которых нет UI представления.

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

    ОтветитьУдалить
  6. А зачем привязываться к элементам интерфейса на раннем этапе? Главное создать нечто вроде Fit'овских табличек с входными и выходными значениями и на основании их начинать работу. А со временем уже получится привязываться к UI - acceptance тесты начнут проходить.

    И ты обещал сослаться на литературу... :)

    ОтветитьУдалить
  7. @Idsa
    Ок, давай самый простой сценарий.

    Пользователь вводит логин/пароль и если они неверные должно выводится сообщение об ошибке.

    Как будет выглядеть тест в Fitness?

    Книжка например эта http://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658/ref=sr_1_1?ie=UTF8&s=books&qid=1279256094&sr=8-1

    ОтветитьУдалить
  8. >>Как будет выглядеть тест в Fitness?

    Сам я с Fitness никогда не работал... Но навскидку принципиальной разницы между acceptance тестированием библиотеки и приложения с интерфейсом не вижу. Если в первом случае мы передаем параметры и смотрим на результат, то во втором запускаем какую-нибудь кликалку, и анализируем интерфейс.

    P. S. А Кент Бек критикует Acceptance-TDD

    ОтветитьУдалить
  9. @Idsa
    > Сам я с Fitness никогда не работал
    А ты попробуй.

    > P. S. А Кент Бек критикует Acceptance-TDD
    Приемочное тестирование - одна из практик XP. Не знаю, что ты подразумеваешь под многозначительным "Кент Бек критикует Acceptance-TDD"

    ОтветитьУдалить
  10. >>Приемочное тестирование - одна из практик XP. Не знаю, что ты подразумеваешь под многозначительным "Кент Бек критикует Acceptance-TDD"

    Цитирую аннотацию к этой статье:
    "In his recent book ”Test-Driven Development” [1], Kent Beck
    describes briefly the concept of ”Acceptance-Test Driven Development”,
    and is broadly sceptical to whether it will work."

    ОтветитьУдалить
  11. @Idsa
    Что ты понимаешь под Acceptance-Test Driven Development?

    ОтветитьУдалить
  12. @Александр
    Написание Acceptance-тестов до реализации

    ОтветитьУдалить
  13. @Idsa
    Можешь конкретнее, что такое Acceptance-тестов и кто их пишет. Как ты вообще представляешь себе этот процесс?

    ОтветитьУдалить
  14. Я, пожалуй, воздержусь от описания процесса, ибо, как я говорил, для меня это новая концепция, и по ней я не работал. На данный момент мое понимание этого вопроса ограничивается тем, что выдает гугл по запросу "Acceptance Test Driven Development" и носит теоретический характер.

    ОтветитьУдалить
  15. @Idsa
    Ок, я тогда я скажу пару слов :)

    В идеале приемочные тесты пишет заказчик или менеджер проекта. Отсюда и возникает "простой" интерфейс написание тестов, такой как Fitness. Перед началом итерации они садятся и пишут своебразное Definition of Done. По мере реализации задач приемочные тесты проходят. Когда они все прошли, значит все задачи готовы.

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

    ОтветитьУдалить
  16. Спасибо Александр, очень интересно.

    Со своей стороны, хотел бы попросить совета: допустим я пишу некий класс http-клиент, ориентированный на работу с одним веб-порталом. Как его лучше тестировать, не взаимодействуя с сетью?

    PS Пока не вел ни одного проекта на TDD, но это так все интересно, что вот хочется следующий целиком писать на тестах.

    ОтветитьУдалить
  17. @G-Host

    > Как его лучше тестировать, не взаимодействуя с сетью?

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

    В целом любое приложение можно написать TDD-стиле. Советую вас посмотреть видео
    Видео. Пример разработки приложения с помощью TDD и прочитать TDD для начинающих. Ответы на популярные вопросы.

    ОтветитьУдалить
  18. Спасибо. Осваиваю, пока не привычно.

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...