TDD - функция верхнего уровня имеет слишком много макетов. Должен ли я вообще испытывать это? - PullRequest
7 голосов
/ 14 января 2010

У меня есть приложение .NET с веб-интерфейсом, интерфейсом WCF-службы Windows. Приложение довольно простое - оно требует некоторого пользовательского ввода, отправляя его в сервис. Служба делает это - принимает данные (электронная таблица Excel), извлекает элементы данных, проверяет базу данных SQL, чтобы убедиться, что элементы еще не существуют - если они не существуют, мы делаем запрос в реальном времени стороннему поставщику данных и получить результаты, вставив их в базу данных. По пути он делает некоторые записи.

У меня есть класс Job с одним общедоступным ctor и публичным методом Run (). Ctor принимает все параметры, а метод Run () выполняет всю вышеуказанную логику. Каждый логический фрагмент функциональности разделен на отдельный класс - IParser выполняет синтаксический анализ файла, IConnection взаимодействует с поставщиком данных, IDataAccess осуществляет доступ к данным и т. Д. Класс Job имеет частные экземпляры этих интерфейсов и использует DI для создания фактические реализации по умолчанию, но позволяют пользователю класса вводить любой интерфейс.

В реальном коде я использую ctor по умолчанию. В моих модульных тестах для метода Run () я использую все фиктивные объекты, созданные с помощью NMock 2.0. Этот метод Run () по сути является функцией «верхнего уровня» этого приложения.

Теперь вот моя проблема / вопрос: модульные тесты для этого метода Run () сумасшедшие. У меня есть три фиктивных объекта, которые я отправляю в ctor, и каждый фиктивный объект устанавливает ожидания для себя. В конце я проверяю. У меня есть несколько различных потоков, которые может принять метод Run, каждый из которых имеет свой собственный тест - он может найти все, что уже есть в базе данных, и не сделать запрос поставщику ... или может быть сгенерировано исключение, и статус задания может быть установленным на «fail» ... ИЛИ у нас может быть случай, когда у нас не было данных, и нам нужно было сделать запрос поставщика (поэтому все эти вызовы функций должны быть выполнены).

Теперь - прежде чем вы кричите на меня и говорите: «Ваш метод Run () слишком сложен!» - этот метод Run - всего лишь 50 строк кода! (Он вызывает некоторые частные функции; но весь класс занимает всего 160 строк). Поскольку вся «настоящая» логика выполняется в интерфейсах, объявленных в этом классе. однако самый большой модульный тест для этой функции - это 80 строк кода с 13 вызовами Expect.BLAH () .. _

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

Итак, стоит ли мне вообще проверять эту функцию высокого уровня? И что я получаю, делая это? Или я совершенно неправильно использую здесь макетные объекты? Возможно, мне следует отказаться от модульных тестов в этом классе, а вместо этого просто сделать автоматический интеграционный тест, который использует реальные реализации объектов и Asserts () для запросов SQL, чтобы убедиться, что существуют правильные данные конечного состояния? Что мне здесь не хватает?

РЕДАКТИРОВАТЬ: Вот код - первая функция - это метод Run () - затем мои пять тестов, которые проверяют все пять возможных путей кода. Я изменил его по причинам NDA, но общая концепция все еще там. Что-то не так с тем, как я тестирую эту функцию, какие-либо предложения по изменению, чтобы сделать ее лучше? Спасибо.

Ответы [ 8 ]

4 голосов
/ 21 января 2010

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

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

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

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

EDIT

Только что увидел, что вы опубликовали код и у вас есть некоторые мысли (без определенного порядка).

  1. Слишком много кода (IMO) внутри вашего блока SyncLock. Общее правило заключается в том, чтобы сохранить минимальный код внутри SyncLock. ВСЕ должно быть заблокировано?
  2. Начните разбивать код на функции, которые можно протестировать независимо. Пример: ForLoop, который удаляет идентификаторы из списка (String), если они существуют в БД. Некоторые могут утверждать, что вызов m_dao.BeginJob должен быть в какой-то функции GetID, которую можно протестировать.
  3. Можно ли превратить любую из процедур m_dao в функции, которые можно протестировать самостоятельно? Я бы предположил, что класс m_dao где-то имеет свои собственные тесты, но, глядя на код, выясняется, что это может быть не так. Они должны вместе с функциональностью в классе m_Parser. Это облегчит бремя тестов Run.

Если бы это был мой код, моя цель состояла бы в том, чтобы доставить код туда, где все отдельные вызовы процедур внутри Run тестируются самостоятельно, и чтобы тесты Run просто тестировали окончательный результат. Для заданных входных данных A, B, C: ожидаемый результат X. Для входных данных E, F, G: ожидаемый Y. Детали того, как Run достигает X или Y, уже проверены в тестах других процедур.

Это были только мои первые мысли. Я уверен, что есть несколько подходов, которые можно использовать.

2 голосов
/ 20 января 2010

Две мысли: во-первых, вам все равно нужно пройти интеграционный тест, чтобы убедиться, что все сваливается вместе. Во-вторых, мне кажется, что вы пропускаете промежуточные объекты. В моем мире 50 строк - это длинный метод. Трудно сказать что-то более точное, не видя код.

1 голос
/ 22 января 2010

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

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

Таким образом, вы можете быть уверены, что и вход, и выход соответствуют желаемому.

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

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

1 голос
/ 21 января 2010

для проверки такой функции: Вы сказали в комментарии

"тесты читаются так же, как фактические функция, и так как я использую насмешки, его единственное утверждение функций звонил и отправлял параметры (могу проверить это на глаз 50 линии функция) "

imho, взглянуть на эту функцию недостаточно, разве вы не слышали: "Не могу поверить, что пропустил это!" ... у вас есть достаточное количество сценариев, которые могут пойти не так в этом методе Run, охват этой логики - хорошая идея.

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

если тесты слишком длинные / трудно понять, что там происходит: не тестируйте отдельные сценарии с каждым утверждением, что оно связано с ним. Разбейте его, протестируйте вещи, как будто они должны регистрировать x сообщений, когда происходит y (1 тест), следует сохранять в db, когда происходит y (другой отдельный тест), он должен отправлять запрос третьей стороне, когда происходит z (еще один тест) и т. д.

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

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

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

1 голос
/ 20 января 2010

Итак - я должен даже потрудиться проверить это функция высокого уровня?

Да. Если есть разные пути кода, вы должны быть.

И что я собираюсь этим сделать? Или я совершенно неправильно использую макет / заглушку объекты здесь?

Как отметил Дж.Б. (Приятно видеть вас на AgileIndia2010!), Статью Фаулера рекомендуется прочитать. Как простое упрощение: используйте заглушки, если вам не нужны значения, возвращаемые соавторами. Если вы возвращаете значения изlaborator.call_method (), изменяете поведение (или вам нужны нетривиальные проверки аргументов, вычисление для возвращаемых значений), вам нужны макеты.

Предлагаемые рефакторинги:

  1. Попробуйте перенести создание и внедрение макетов в общий метод установки. Большинство структур модульного тестирования поддерживают это; будет вызываться перед каждым тестом
  2. Ваши вызовы LogMessage являются маяками - еще раз вызывая методы выявления намерений. например SubmitBARRequest (). Это сократит ваш производственный код.
  3. Попробуйте переместить каждый Expect.Blah1 (..) в методы выявления намерений. Это сократит ваш тестовый код и сделает его чрезвычайно читабельным и более легким для изменения. например Заменить все экземпляры .

    Expect.Once.On (mockDao) _ .Method ("BeginJob") _ .With (New Object () {submitBy, clientID, runDate, "Sent For Baring"}) _ .Will ([Return] .Value (0));

с

ExpectBeginJobOnDAO_AndReturnZero(); // you can name it better

1 голос
/ 14 января 2010

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

0 голосов
/ 20 января 2010

Я собираюсь догадаться, что каждый тест для Run() устанавливает ожидания для каждого метода, который они вызывают в макетах, даже если этот тест не фокусируется на проверке каждого такого вызова метода. Я настоятельно рекомендую вам Google "издевательства не окурки" и прочитайте статью Фаулера.

Кроме того, 50 строк кода довольно сложны. Сколько кодовых путей через метод? 20 +? Вы могли бы извлечь выгоду из более высокого уровня абстракции. Мне нужно увидеть код, чтобы судить точнее.

0 голосов
/ 14 января 2010

Ваши тесты слишком сложны.

Вы должны проверить аспекты вашего класса, а не писать юнит-тест для каждого члена вашего класса. Юнит-тест не должен охватывать всю функциональность члена.

...