Философские вопросы о разработке через тестирование - PullRequest
18 голосов
/ 19 сентября 2009

Меня постоянно заинтриговала разработка, основанная на тестировании, но я никогда не смогу выполнить ее, когда попробую на реальных проектах. У меня есть пара философских вопросов, которые постоянно возникают, когда я пытаюсь это сделать:

  1. Как вы справляетесь с большими изменениями? Когда дело доходит до тестирования отдельных функций (некоторые параметры, значение результата, мало побочных эффектов), TDD не представляет никакой сложности. Но что делать, когда вам нужно капитально отремонтировать что-то большое, например, переход от библиотеки синтаксического анализа SAX к библиотеке синтаксического анализа DOM? Как вы соблюдаете цикл test-code-refactor, когда ваш код находится в промежуточном состоянии? Как только вы начнете вносить изменения, вы получите множество неудачных тестов, пока не полностью завершите капитальный ремонт (если вы не поддерживаете какой-то класс беспородных, который использует DOM и SAX, пока не закончите конвертацию, но это довольно странно) , Что в этом случае происходит с циклом тестового кода-рефактора с маленьким шагом? В течение всего этого процесса вы больше не будете двигаться небольшими, полностью проверенными шагами. Должен быть какой-то способ, которым люди справляются с этим.
  2. Что вы действительно тестируете при тестировании графического интерфейса или кода базы данных с помощью макетов? Моды созданы так, чтобы возвращать именно тот ответ, который вы хотите, так как вы знаете, что ваш код будет работать с реальной базой данных? В чем преимущество автоматизированных тестов для такого рода вещей? Это несколько повышает уверенность, но а) не дает того уровня уверенности, который необходим для полного модульного теста, и б) в некоторой степени, вы просто проверяете, что ваши предположения работают с вашим кодом, а не что твой код работает с БД или GUI?

Может ли кто-нибудь указать мне на хорошие примеры использования управляемой тестированием разработки в больших проектах? Обидно, что я могу найти только примеры TDD для отдельных классов.

Спасибо!

Ответы [ 8 ]

4 голосов
/ 20 сентября 2009

Как вы справляетесь с большими изменениями?

По мере необходимости.

Иногда рефакторинги имеют большую поверхность, но тривиальны в деталях. Это можно сделать довольно большими шагами. Прилагать слишком много усилий, чтобы сломать их, будет напрасно.

Я бы сказал, что изменение библиотеки XML относится к этой категории. Вы помещаете XML и получаете некоторое представление. Пока ваше представление не меняется (от графика, представляющего состояние к потоку событий), переключать библиотеку легко.

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

Если вы измените синтаксический анализатор XML и представление в вашем коде, это, безусловно, должно быть как минимум два отдельных рефакторинга.

Пробное испытание

Вы тестируете протокол связи между объектами / слоями с фиктивными объектами.

Подход, основанный на моделировании, может служить моделью связи, подобной модели OSI . Когда уровень X получает вызов с параметром x, он вызывает уровень Z с параметрами a и b. Ваш тест определяет этот протокол связи.

Как бы полезна ни была фиктивная проверка, протестируйте с ними как можно меньше функциональных возможностей. Наилучшим вариантом являются тесты на основе состояния: установочное устройство, тестируемая система вызова, проверка состояния тестируемой системы и чисто функциональные (как в функциональном программировании) тесты: вызов с x возвращает a.

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

Интеграционные тесты не являются опцией для тестирования вашей системы. Они должны использоваться только для тестирования аспектов системы, которые могут сломаться при интеграции нескольких устройств. Если вы попытаетесь протестировать свою систему с помощью интеграционных тестов, вы попадете в казино с перестановками.

Так что ваша стратегия тестирования GUI должна быть ясной. Части кода GUI, которые нельзя протестировать изолированно, следует тестировать с помощью фиктивных тестов (при нажатии этой кнопки сервис X вызывается с параметром y).

Базы данных немного мутят воду. Вы не можете смоделировать базу данных, если не собираетесь переопределить поведение каждой базы данных, которую хотели бы поддерживать. Но это не модульный тест, поскольку вы интегрируете внешнюю систему. Я примирился с этой концептуальной проблемой и думаю о DAO и базе данных как об одном неразделимом модуле, который можно протестировать с помощью подхода, основанного на состоянии. (К сожалению, этот аппарат ведет себя по-разному, когда у него есть свой оракульный день по сравнению с его днем ​​MySQL. И он может сломаться посередине и сказать вам, что он не может разговаривать сам с собой.)

3 голосов
/ 19 сентября 2009

При тестировании GUI или кода базы данных с издевается, что ты на самом деле тестируешь? Мотыки созданы, чтобы вернуть именно ответь хочешь, так откуда ты знаешь что ваш код будет работать с база данных реального мира? Что Преимущество автоматизированных тестов для этого Такие вещи? Это повышает уверенность несколько, но а) это не дает вам тот же уровень уверенности, что завершить модульное тестирование должно, и б) чтобы в определенной степени, не так ли проверка правильности ваших предположений с вашим кодом, а не что ваш код работает с БД или GUI?

Это мой подход: для уровня доступа к базе данных (DAL) я не использую mock для моего модульного теста. Вместо этого я запускаю тесты на реальной базе данных, хотя она отличается от рабочей базы данных. Таким образом, в этом смысле вы можете сказать, что я не запускаю модульный тест для базы данных. Для приложений NHibernate у меня есть две базы данных с одинаковой схемой, но с разными типами баз данных ( ORM делает это легко). Я использую sqlite для своего автоматического тестирования и реальную базу данных MySQL или SQL-сервера для специального тестирования.

Только один раз я использовал mock для модульного тестирования DAL; и именно тогда я использовал строго типизированный набор данных в качестве ORM (большая ошибка!). Я сделал так, чтобы Typemock вернул мне макетную копию полной таблицы, чтобы я мог выполнить select * на ней. Позже, оглядываясь назад, я пожалел, что никогда не буду этого делать, но это было давно, и мне было жаль, что я не использовал надлежащую ORM.

Что касается GUI, возможно для модульного тестирования взаимодействия с GUI. Для этого я использовал шаблон MVP для разделения модели, вида и презентатора. На самом деле для этого типа приложений я тестирую только на Presenter и Model, в которых я использую Typemock (или внедрение зависимостей ), чтобы изолировать разные слои, чтобы в одно время я мог сосредоточиться только на одном слое. Я не проверяю представление, но я делаю тестирующего Presenter (где происходит большинство взаимодействий и ошибок).

2 голосов
/ 20 сентября 2009

Мои 2 цента ...

  1. если ваши тесты не работают, потому что вы переключили тип синтаксического анализатора XML - это означает, что тесты хрупкие . Тесты должны указывать что, а не как . Это означает, что в этом случае тесты как-то узнают, что вы используете SAX-анализатор (подробности реализации); что они не должны. Исправьте эту проблему, и вы будете лучше с большими изменениями.
  2. Когда вы абстрагируете GUI или Mocks от тестов через интерфейс, вы гарантируете, что ваш испытуемый, который использует mocks (как удваивается для реальных соавторов), работает как задумано. Вы можете изолировать ошибки в вашем коде от ошибок ваших соавторов. Насмешки помогают вам быстро поддерживать ваш набор тестов. Вам также нужны тесты, которые подтверждают, что ваш реальный соавтор также соответствует интерфейсу И проверяет, что ваши настоящие соавторы «подключены» правильно.
1 голос
/ 19 сентября 2009
  1. Как вы справляетесь с большими изменениями?

    • Шаг за шагом. Я работал над несколькими нетривиальными программами и всегда мог разбить вещи на небольшие изменения (требующие часов и, возможно, дней). Например, переписывание 30-мегапиксельного веб-сайта вылилось в создание одной страницы за раз - это переходило с одного языка на другой, написание (небольших) тестов по мере продвижения, поддержание сайта в частом развертывании. В другом проекте мы превратили веб-приложение с графическим интерфейсом пользователя в необработанный внутренний сервер. Это включало много маленьких шагов через месяц или два работы, а затем в конечном итоге отбрасывать большую часть веб-кода. Но мы смогли сохранить все тесты на ходу. Мы сделали это не потому, что пытались что-то доказать, а потому, что это был лучший способ повторно использовать код и тесты.

    • Большим шагам могут помочь тесты с более широкой областью применения. Например, ваш пример SAX-> DOM будет иметь высокоуровневый интеграционный тест, который будет проверять окончательное поведение. Однако когда я сделал нечто подобное, я написал гораздо меньшие поведенческие тесты для различных типов обработки узлов, и их преобразование можно было выполнять по одному.

  2. Что вы действительно тестируете при тестировании GUI или кода базы данных с помощью mocks?

    • Вы всегда должны убедиться, что пишете ценные тесты. Это может быть сложно. Легко, даже если вы думаете, написать несколько довольно избыточных тестов.
    • Мок не имеет смысла, когда вы пытаетесь протестировать запросы к базе данных. Они полезны, когда вы пытаетесь «макетировать» слой ниже того, что вы тестируете ... поэтому они полезны в тесте контроллера, где вы будете имитировать поведение сервисного уровня - который вы будете тестировать независимо. Для тестирования запросов к базе данных вам необходимо загрузить базу данных соответствующими приборами, которые будут проверять то, что вы пытаетесь сделать. Это может быть сделано с помощью приборов или тщательного тестирования кода настройки. Требуется некоторое время, чтобы понять, как правильно, поэтому хорошо иметь хорошо спроектированный набор данных фикстур, который позволит вам писать хорошие тесты запросов к базе данных, охватывающие как можно больше важных случаев.

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

1 голос
/ 19 сентября 2009

При тестировании GUI или кода базы данных с издевается, что ты на самом деле тестируешь?

Обычно я пытаюсь разделить свою бизнес-логику, логику отображения и доступ к базе данных. Большинство моих тестов GUI имеют дело с бизнес-логикой. Вот пример псевдокода:

// Production code in class UserFormController:

void changeUserNameButtonClicked() {
  String newName = nameTextBox.getText();
  if (StringUtils.isEmpty(newName)) {
    errorBox.showError("User name may not be empty !");
  } else {
    User user = engine.getCurrentUser();
    user.name = newName;
    engine.saveUser(user);
  }
}

// Test code in UserFormControllerTest:

void testValidUserNameChange() {
  nameTextBox = createMock(TextBox.class);
  expect(nameTextBox.getText()).andReturn("fred");
  engine = createMock(Engine.class);
  User user = createMock(user);
  user.setName("fred");
  expectLastCall();
  expect(engine.getCurrentUser()).andReturn(user);
  engine.saveUser(user);
  expectLastCall();
  replay(user, engine, nameTextBox);

  UserFormController controller = new UserFormController();
  controller.setNameTextBox(nameTextBox);
  controller.setEngine(engine);
  controller.changeUserNameButtonClicked();  

  verify(user, engine, nameTextBox);
}

void testEmptyUserNameChange() {
  nameTextBox = createMock(TextBox.class);
  errorBox = createMock(ErrorBox.class);
  expect(nameTextBox.getText()).andReturn("");
  errorBox.showError("User name may not be empty !");
  expectLastCall();
  replay(nameTextBox, errorBox);

  UserFormController controller = new UserFormController();
  controller.setNameTextBox(nameTextBox);
  controller.setErrorBox(errorBox);
  controller.changeUserNameButtonClicked();  

  verify(nameTextBox, errorBox);
}

Это гарантирует, что независимо от того, насколько повреждена моя база данных и код GUI, по крайней мере, логика, управляющая изменением имени пользователя, работает правильно. Если вы организуете свой код GUI в набор отдельных элементов управления (или виджетов, элементов формы или как они называются в вашей структуре GUI), вы можете протестировать их аналогичным образом.

Но в конечном итоге, как вы сказали, эти модульные тесты не дадут вам полной картины. Чтобы получить это, вам нужно сделать то, что предложили другие: создать реальную базу данных с «золотым набором» данных и провести интеграционные / функциональные тесты против нее. Но, IMO, такие тесты выходят за рамки TDD, потому что их установка обычно довольно трудоемка.

1 голос
/ 19 сентября 2009

С точки зрения обработки больших изменений ... целью TDD является тестирование поведения вашего кода и его взаимодействия со службами, от которых он зависит. Если вы хотите использовать TDD и переходите от парсера DOM к парсеру SAX, и вы сами пишете парсер саксофона, то вы будете писать тесты, которые проверяют поведение парсера SAX на основе известного ввода, то есть документа XML. SAX-анализатор может зависеть от набора вспомогательных объектов, которые на самом деле могут быть изначально смоделированы для целей тестирования поведения SAX-анализатора. Когда вы будете готовы написать код реализации для вспомогательных объектов, вы сможете написать тесты для определения их ожидаемого поведения на основе известного ввода. В примере парсера SAX вы бы написали отдельные классы для реализации этого поведения, чтобы не мешать существующему коду, который зависит от парсера DOM. Фактически, вы могли бы создать интерфейс IXMLParser, который реализуют анализатор DOM и анализатор SAX, чтобы вы могли переключать их по своему желанию.

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

Я не знаком с какими-либо практическими примерами, но уверен, что они есть.

0 голосов
/ 19 сентября 2009

Обработка больших изменений

По моему опыту, это относительно редко. Когда они происходят, обновление тестов - это небольшая проблема. Хитрость заключается в том, чтобы выбрать правильную гранулярность для тестов. Если вы протестируете открытый интерфейс, обновления будут выполняться быстро. Если вы протестируете частный код реализации, переход с SAX на DOM-парсер сработает долго, и вы будете чувствовать себя как дома. ; -)

Код GUI тестирования

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

Проверка кода базы данных

Когда это возможно, я предпочитаю размещать код доступа к данным за интерфейсами и макетировать его при тестировании бизнес-логики. Как уже упоминали другие, в какой-то момент вы можете запустить интеграционные тесты с DAL, чтобы убедиться, что он работает с тестовой базой данных в известном состоянии. Вам могут потребоваться другие интеграционные тесты всей системы; хорошо иметь несколько слоев тестов. TDD в первую очередь касается дизайна и не исключает необходимости проведения интеграционных или приемочных испытаний.

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

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

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

РЕДАКТИРОВАТЬ: Честно говоря, у меня проблемы с разделением кода доступа к данным во многих случаях, и в итоге я использую базы данных тестовой колоды. Даже такие интеграционные тесты оказались ценными, хотя они медленнее и более хрупки. Как я уже сказал, я все еще учусь.

0 голосов
/ 19 сентября 2009

Что касается угла базы данных, как упомянул Нгу Сун Хуэй, вы должны (IMHO) использовать что-то вроде DBUnit , что позволит настроить базу данных в известной конфигурации (чтобы вы могли проверить ожидаемые результаты) но он использует реальную базу данных, которую будет использовать реальное приложение.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...