Трудности с TDD и рефакторингом (или - Почему это так больно, чем должно быть?)

20

Я хотел научить себя использовать подход TDD, и у меня был проект, над которым я давно хотел работать. Это был не большой проект, поэтому я подумал, что это будет хорошим кандидатом на TDD. Однако я чувствую, что что-то пошло не так. Позвольте мне привести пример:

На высоком уровне мой проект является надстройкой для Microsoft OneNote, которая позволит мне более легко отслеживать и управлять проектами. Теперь я также хотел сохранить бизнес-логику для этого максимально изолированной от OneNote на тот случай, если я когда-нибудь решу создать собственное хранилище и серверную часть.

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

  1. Пользователь нажимает создать проект
  2. Типы пользователей в заголовке проекта
  3. Убедитесь, что проект создан правильно

Пропустив пользовательский интерфейс и промежуточное планирование, я прихожу к своему первому модульному тесту:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

Все идет нормально. Красный, зеленый, рефакторинг и т. Д. Хорошо, сейчас нужно реально сохранить вещи. Вырезая некоторые шаги здесь я заканчиваю с этим.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

Я все еще чувствую себя хорошо в этот момент. У меня пока нет конкретного хранилища данных, но я создал интерфейс так, как ожидал.

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

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

Это было хорошо, пока я не попытался это реализовать:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

И есть проблема именно там, где "...". Теперь я понимаю, что CreatePage требует ID раздела. Я не осознавал этого, когда думал на уровне контроллера, потому что меня интересовало только тестирование битов, относящихся к контроллеру. Тем не менее, теперь я понимаю, что должен попросить пользователя указать место для сохранения проекта. Теперь мне нужно добавить идентификатор местоположения в хранилище данных, затем добавить один в проект, затем добавить один в контроллер и добавить его ко ВСЕМ тестам, которые уже написаны для всех этих вещей. Это очень быстро стало утомительно, и я не могу помочь, но чувствую, что поймал бы это быстрее, если бы набросал дизайн раньше, чем позволил бы его разрабатывать во время процесса TDD.

Может кто-нибудь объяснить мне, если я сделал что-то не так в этом процессе? Есть ли в любом случае такого рода рефакторинг можно избежать? Или это распространено? Если это распространено, есть ли способы сделать его более безболезненным?

Спасибо всем!

Приземляться
источник
Если вы разместите эту тему на этом дискуссионном форуме, вы получите очень полезные комментарии: groups.google.com/forum/#!forum/…, которая специально предназначена для тем TDD.
Чак Круцингер,
1
Если вам нужно добавить что-то во все ваши тесты, это звучит так, как будто ваши тесты написаны плохо. Вы должны провести рефакторинг ваших тестов и рассмотреть возможность использования разумного приспособления.
Дейв Хиллиер

Ответы:

19

Хотя TDD (справедливо) рекламируется как способ разработки и развития вашего программного обеспечения, все же стоит заранее подумать о дизайне и архитектуре. ИМО, «делать наброски дизайна раньше времени» - это честная игра. Однако, зачастую это будет на более высоком уровне, чем проектные решения, к которым вы будете обращаться через TDD.

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

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

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

jhewlett
источник
Я читал о преимуществах, основанных на состоянии, по сравнению с взаимодействием и большую часть времени понимаю. Тем не менее, я не вижу, как это возможно в каждом случае, не выставляя свойства EXPLICITLY для теста. Возьми мой пример выше. Я не уверен, как проверить, что хранилище данных было действительно вызвано без использования подтверждения для "MustHaveBeenCalled". Что касается пункта 2, вы абсолютно правы. Я сделал это после всех правок, но мне просто хотелось убедиться, что мой подход в целом соответствует принятым методам TDD. Благодарность!
Лэндон
@Landon Есть случаи, когда тестирование взаимодействия является более подходящим. Например, проверка того, что был сделан вызов базе данных или веб-службе. По сути, всякий раз, когда вам нужно изолировать свой тест, особенно от внешней службы.
Jhewlett
@Landon Я «убежденный классик», поэтому я не очень разбираюсь в интерактивном тестировании ... Но вам не нужно делать утверждение для «MustHaveBeenCalled». Если вы тестируете вставку, вы можете использовать запрос, чтобы увидеть, была ли она вставлена. PS: я использую заглушки из соображений производительности при тестировании всего, кроме слоя базы данных.
Hbas
@jhewlett К такому выводу я тоже пришел. Благодарность!
Лэндон
@Hbas Нет базы данных для запроса. Я согласен, что это был бы самый прямой путь, если бы он у меня был, но я добавляю это в блокнот OneNote. Лучшее, что я могу сделать вместо этого, - это добавить метод Get в мой вспомогательный класс взаимодействия, чтобы попытаться перетащить страницу. Я мог бы написать тест, чтобы сделать это, но я чувствовал, что буду тестировать две вещи одновременно: я сохранил это? и правильно ли мой вспомогательный класс получает страницы? Хотя, я думаю, в какой-то момент ваши тесты, возможно, будут полагаться на другой код, протестированный в другом месте. Благодарность!
Landon
10

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

Может быть нет

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

С другой стороны, возможно, если бы вы начали с высокоуровневой функции (SaveProject) вместо низкоуровневой (CreateProject), вы бы заметили пропущенные параметры раньше.

Опять же, может быть, вы бы не стали. Это неповторимый эксперимент.

Но если вы ищете урок в следующий раз: начните сверху. И подумайте о дизайне столько, сколько хотите в первую очередь.

Стивен А. Лоу
источник
0

https://frontendmasters.com/courses/angularjs-and-code-testability/ Примерно с 2:22:00 до конца (около 1 часа). Извините, что видео не бесплатное, но я не нашел бесплатного, который так хорошо это объясняет.

На этом уроке представлена ​​одна из лучших презентаций написания тестируемого кода. Это класс AngularJS, но часть тестирования посвящена Java-коду, в первую очередь потому, что то, о чем он говорит, не имеет ничего общего с языком, и в первую очередь связано с написанием хорошего тестируемого кода.

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

Он также тратит некоторое время на написание спецификации в форме тестовых утверждений.

boatcoder
источник