Где грань между логикой приложения модульного тестирования и ненадежными языковыми конструкциями?

87

Рассмотрим функцию, подобную этой:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Это может быть использовано так:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Давайте предположим , что Storeимеют свои собственные модульные тесты, или поставщик при условии. В любом случае мы верим Store. И давайте далее предположим, что обработка ошибок - например, ошибок отключения базы данных - не является обязанностью savePeople. В самом деле, давайте предположим, что само хранилище - это волшебная база данных, которая никак не может ошибаться. Учитывая эти предположения, вопрос заключается в следующем:

Должно ли savePeople()быть модульное тестирование, или такие тесты будут равносильны тестированию встроенной forEachязыковой конструкции?

Мы могли бы, конечно, передать насмешку dataStoreи утверждать, что dataStore.savePerson()это называется один раз для каждого человека. Вы, конечно, можете привести аргумент, что такой тест обеспечивает защиту от изменений реализации: например, если мы решили заменить forEachтрадиционный forцикл или какой-то другой метод итерации. Так что тест не совсем тривиален. И все же это кажется ужасно близко ...


Вот еще один пример, который может быть более плодотворным. Рассмотрим функцию, которая ничего не делает, кроме как координирует другие объекты или функции. Например:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

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

Означает ли эта неспособность проверить функцию в значимом «черном ящике» недостаток конструкции самой функции? Если так, как это могло быть улучшено?


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

Когда пользователь создает новую учетную запись, должен произойти ряд вещей: 1) новая запись пользователя должна быть создана в базе данных 2) приветственное электронное письмо должно быть отправлено 3) IP-адрес пользователя должен быть записан для мошенничества цели.

Итак, мы хотим создать метод, который связывает все шаги «нового пользователя»:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.insertUserRecord(validateduserData);
  emailService.sendWelcomeEmail(validatedUserData);
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

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

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

Ион
источник
44
После того, как он работает. У вас есть печенье
Ewan
6
по поводу вашего обновления: зачем вам когда-нибудь издеваться над кастрюлей? или тесто? они звучат как простые объекты в памяти, которые нужно создавать тривиально, и поэтому нет никаких причин, по которым вы не должны тестировать их как одно целое. помните, что «юнит» в «модульном тестировании» не означает «отдельный класс». это означает «наименьшую возможную единицу кода, которая используется для выполнения чего-либо». Сковорода, вероятно, не более, чем контейнер для тестовых объектов, поэтому было бы целесообразно протестировать его изолированно, а не просто тестировать метод bakeCookies снаружи.
Сара
11
В конце концов, основной принцип работы здесь заключается в том, что вы пишете достаточно тестов, чтобы убедиться, что код работает, и что это адекватная «канарейка в угольной шахте», когда кто-то что-то меняет. Вот и все. Здесь нет магических заклинаний, формульных предположений или догматических утверждений, поэтому 85-90% покрытия кода (а не 100%) широко считается отличным.
Роберт Харви
5
@RobertHarvey, к сожалению, формальные банальности и звуковые фрагменты TDD, хотя и обязательно принесут вам восторженные поклоны, не помогут решить реальные проблемы. для этого вам нужно испачкать руки и рискнуть ответить на реальный вопрос
Иона
4
Юнит тест в порядке убывания цикломатической сложности. Поверьте мне, у вас не хватит времени, прежде чем вы приступите к этой функции
Нил Макгиган

Ответы:

118

Должен ли savePeople()быть юнит-тест? Да. Вы не тестируете, что dataStore.savePersonработает, или работает соединение с БД, или даже, что foreachработает. Вы проводите тестирование, которое savePeopleвыполняет обещание, которое оно дает по контракту.

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

Брайан Оукли
источник
20
@RobertHarvey: Там много серой зоны, и различие, IMO, не важно. Вы правы, хотя - на самом деле не важно проверять, что «он вызывает правильные функции», а скорее «он делает правильные вещи» независимо от того, как он это делает. Важно проверить, что, учитывая определенный набор входов для функции, вы получаете определенный набор выходов. Однако я вижу, как это последнее предложение может сбить с толку, поэтому я удалил его.
Брайан Оукли
64
«Вы проверяете, что savePeople выполняет обещание, которое он дает по контракту». Этот. Вот так много.
Ловис
2
Если у вас нет «сквозного» системного теста, который покрывает его.
Ян
6
@Ian Сквозные тесты не заменяют модульные тесты, они являются бесплатными. Тот факт, что у вас может быть сквозной тест, гарантирующий сохранение списка людей, не означает, что вам не нужно проходить модульный тест, чтобы его охватить.
Винсент Савард
4
@ VincentSavard, но стоимость / выгода юнит-теста снижается, если риск контролируется другим способом.
Ян
36

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

По крайней мере, в моем приложении TDD, которое обычно находится снаружи, я бы не реализовывал функцию, подобную savePeopleреализованной savePerson. Функции savePeopleи savePersonначинаются как единое целое и управляются тестом из одних и тех же модульных тестов; разделение между ними возникнет после нескольких испытаний на этапе рефакторинга. Этот способ работы также ставит вопрос о том, где savePeopleдолжна быть функция - является ли она свободной функцией или ее частью dataStore.

В конце концов, тесты не только проверить , если вы можете правильно сэкономить Personв Store, но и многие люди. Это также привело бы меня к вопросу о необходимости других проверок, например: «Нужно ли мне убедиться, что savePeopleфункция атомарна, сохранять все или ничего?», «Может ли она просто как-то возвращать ошибки людям, которые не могли» не будут сохранены? Как будут выглядеть эти ошибки? »и так далее. Все это составляет гораздо больше, чем просто проверка на использование той forEachили иной формы итерации.

Хотя, если требование сохранения более чем одного человека пришло только после того, savePersonкак оно уже было доставлено, я бы обновил существующие тесты, savePersonчтобы выполнить новую функцию savePeople, убедившись, что она все еще может спасти одного человека, просто сначала делегировав, затем протестируйте поведение более чем одного человека с помощью новых тестов, думая, необходимо ли сделать поведение атомарным или нет.

MichelHenrich
источник
4
В основном тестируйте интерфейс, а не реализацию.
Снуп
8
Справедливые и проницательные моменты. И все же я чувствую, что мой настоящий вопрос уклоняется :) Ваш ответ говорит: «В реальном мире, в хорошо спроектированной системе, я не думаю, что эта упрощенная версия вашей проблемы существовала бы». Опять же, честно, но я специально создал эту упрощенную версию, чтобы подчеркнуть суть более общей проблемы. Если вы не можете преодолеть искусственную природу примера, вы можете представить себе другой пример, в котором у вас была веская причина для подобной функции, которая выполняла только итерацию и делегирование. Или, может быть, вы думаете, что это просто невозможно?
Иона
@ Иона обновлен. Я надеюсь, что это отвечает на ваш вопрос немного лучше. Это все основано на мнении и может противоречить цели этого сайта, но это, безусловно, очень интересное обсуждение. Кстати, я постарался ответить с точки зрения профессиональной работы, где мы должны стремиться оставить модульные тесты для всего поведения приложения, независимо от того, насколько тривиальной может быть реализация, потому что мы обязаны создавать хорошо протестированные и документированная система для новых сопровождающих, если мы уйдем. Для личных или, скажем, некритических (деньги тоже важны) проектов у меня совсем другое мнение.
Мишель Генрих
Спасибо за обновления. Как именно вы будете тестировать savePeople? Как я описал в последнем абзаце ОП или каким-то другим способом?
Иона
1
Извините, я не прояснил себя с частью "без насмешек". Я имел в виду, что не буду использовать макет для savePersonфункции, как вы предложили, вместо этого я протестирую ее через более общие savePeople. Модульные тесты для Storeбудут изменены, чтобы проходить через, savePeopleа не напрямую вызывать savePerson, поэтому для этого никакие макеты не используются. Но база данных, конечно, не должна присутствовать, так как мы хотели бы изолировать проблемы кодирования от различных проблем интеграции, которые возникают с реальными базами данных, поэтому здесь у нас все еще есть пример.
Мишель Генрих
21

Должно ли savePeople () быть проверено модулем

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

function testSavePeople() {
    myDataStore = new Store('some connection string', 'password');
    myPeople = ['Joe', 'Maggie', 'John'];
    savePeople(myDataStore, myPeople);
    assert(myDataStore.containsPerson('Joe'));
    assert(myDataStore.containsPerson('Maggie'));
    assert(myDataStore.containsPerson('John'));
}

Этот тест делает несколько вещей:

  • он проверяет контракт функции savePeople()
  • это не заботится о реализации savePeople()
  • он документирует пример использования savePeople()

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

Например, ваша реализация хранилища данных может предоставить saveBulkPerson()метод в будущем - теперь изменение в реализации savePeople()для использования saveBulkPerson()не будет нарушать модульный тест, пока он saveBulkPerson()работает, как ожидалось. И если saveBulkPerson()как - то не работает , как ожидалось, устройство тест будет расслышал.

или такие тесты будут равносильны тестированию встроенной языковой конструкции forEach?

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

По поводу вашего обновления вопроса:

Тест на изменение состояния! Например, часть теста будет использоваться. В соответствии с вашей реализацией утверждают, что количество использованных средств doughсоответствует, panили утверждают, что doughиспользованное количество использовалось. Утверждают, что panпосле вызова функции в нем содержатся файлы cookie. Утверждают, что ovenпусто / в том же состоянии, что и раньше.

Для дополнительных тестов проверьте крайние случаи: что произойдет, если ovenперед вызовом не пусто? Что будет, если не хватит dough? Если panуже заполнен?

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

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


Для вашего последнего дополнения:

Когда пользователь создает новую учетную запись, должен произойти ряд вещей: 1) новая запись пользователя должна быть создана в базе данных 2) приветственное электронное письмо должно быть отправлено 3) IP-адрес пользователя должен быть записан для мошенничества цели.

Итак, мы хотим создать метод, который связывает все шаги «нового пользователя»:

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Для функции , как это я бы глумиться / стаб / подделка (что кажется более общим) в dataStoreи emailServiceпараметров. Эта функция сама по себе не выполняет никаких переходов состояний ни по одному параметру, она делегирует их методам некоторых из них. Я хотел бы проверить, что вызов функции сделал 4 вещи:

  • он вставил пользователя в хранилище данных
  • он отправил (или, по крайней мере, вызвал соответствующий метод) приветственное письмо
  • он записал IP пользователей в хранилище данных
  • он делегировал любое исключение / ошибку, с которой столкнулся (если есть)

Первые 3 проверки могут быть сделаны с помощью макетов, заглушек или подделок dataStoreи emailService(вы действительно не хотите отправлять электронные письма при тестировании). Так как мне пришлось искать это для некоторых комментариев, вот различия:

  • Подделка - это объект, который ведет себя так же, как и оригинал, и в некоторой степени неразличим. Его код обычно может быть повторно использован в тестах. Это может быть, например, простая база данных в памяти для оболочки базы данных.
  • Заглушка просто реализует столько, сколько необходимо для выполнения необходимых операций этого теста. В большинстве случаев заглушка является специфической для теста или группы тестов, для которых требуется лишь небольшой набор методов оригинала. В этом примере это может быть dataStoreпросто реализующая подходящую версию insertUserRecord()и recordIpAddress().
  • Макет - это объект, который позволяет вам проверить, как он используется (чаще всего, позволяя вам оценивать вызовы его методов). Я бы постарался использовать их экономно в модульных тестах, поскольку, используя их, вы на самом деле пытаетесь протестировать реализацию функции, а не соответствие ее интерфейсу, но они все еще используются. Существует множество фальшивых фреймворков, которые помогут вам создать именно тот макет, который вам нужен.

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

Ожидаемые исключения / ошибки являются действительными тестовыми примерами: вы подтверждаете, что в случае такого события функция ведет себя так, как вы ожидаете. Это может быть достигнуто путем добавления соответствующего объекта mock / fake / stub при желании.

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

Иногда это нужно делать (хотя вы в основном заботитесь об этом в интеграционных тестах). Чаще всего существуют другие способы проверки ожидаемых побочных эффектов / изменений состояния.

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

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

Для примера рассмотрим добавление вызова oven.preheat()(оптимизация!) В ваш пример выпечки печенья:

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

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

hoffmale
источник
1
Проблема в том, что, как только вы пишете, myDataStore.containsPerson('Joe')вы предполагаете существование базы функциональных тестов. Как только вы это сделаете, вы пишете интеграционный тест, а не юнит-тест.
Иона
Я предполагаю, что могу положиться на наличие тестового хранилища данных (мне все равно, реальное это или фиктивное), и что все работает как настроено (так как у меня уже должны быть модульные тесты для этих случаев). Единственное, что хочет протестировать тест, - это savePeople()добавить этих людей в любое хранилище данных, которое вы предоставляете, если это хранилище данных реализует ожидаемый интерфейс. Интеграционный тест будет, например, проверять, что моя обертка базы данных действительно выполняет правильные вызовы базы данных для вызова метода.
hoffmale
Чтобы уточнить, если вы используете макет, все, что вы можете сделать, это утверждать, что метод в этом макете был вызван , возможно, с каким-то конкретным параметром. Вы не можете утверждать о состоянии издевательства впоследствии. Поэтому, если вы хотите сделать утверждения о состоянии базы данных после вызова тестируемого метода, как, например myDataStore.containsPerson('Joe'), вы должны использовать какой-то функциональный db. Как только вы сделаете этот шаг, он больше не будет модульным тестом.
Иона
1
это не обязательно должна быть реальная база данных - просто объект, который реализует тот же интерфейс, что и реальное хранилище данных (читай: он проходит соответствующие модульные тесты для интерфейса хранилища данных). Я все еще считаю это насмешкой. Пусть макет хранит все, что добавляется любым методом для этого в массиве, и проверяет, myPeopleнаходятся ли тестовые данные (элементы ) в массиве. ИМХО, макет должен по-прежнему иметь то же наблюдаемое поведение, что и реальный объект, иначе вы тестируете на соответствие макету, а не реальному интерфейсу.
hoffmale
«Пусть макет хранит все, что добавляется любым методом для этого в массиве, и проверяет, находятся ли тестовые данные (элементы myPeople) в массиве» - это все еще «настоящая» база данных, просто специальная, в памяти, которую вы создали. «ИМХО, у макета должно быть то же самое наблюдаемое поведение, которое ожидается от реального объекта» - я полагаю, вы можете отстаивать это, но это не то, что означает «глумиться» в тестовой литературе или в любой из популярных библиотек, которые я видел. Макет просто проверяет, что ожидаемые методы вызываются с ожидаемыми параметрами.
Иона
13

Основное значение такого теста заключается в том, что он делает вашу реализацию рефакторизированной.

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

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

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

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

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

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

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

Brandon
источник
Хороший пример рефакторинга с использованием массовой вставки. Тем не менее, возможный модульный тест, который я предложил в OP - что макет хранилища данных savePersonвызывал его для каждого человека в списке, - однако, не справился с рефакторингом массовой вставки. Что для меня означает, что это плохой юнит-тест. Однако я не вижу альтернативного варианта, который бы проходил как массовую реализацию, так и реализацию по одному на одного человека, без использования реальной тестовой базы данных и утверждения против этого, что кажется неправильным. Не могли бы вы предоставить тест, который работает для обеих реализаций?
Иона
1
@ jpmc26 Как насчет теста, который проверяет, что люди были спасены ...?
user253751
1
@immibis Я не понимаю, что это значит. Предположительно, реальный магазин поддерживается базой данных, так что вам придется смоделировать или заглушить его для модульного теста. Таким образом, в этот момент вы будете проверять, что ваш макет или заглушка могут хранить объекты. Это совершенно бесполезно. Лучшее, что вы можете сделать, - это утверждать, что savePersonметод вызывался для каждого ввода, и если вы замените цикл массовой вставкой, вы больше не будете вызывать этот метод. Так что твой тест сломается. Если у вас есть что-то еще, я открыт для этого, но пока не вижу. (И не видя, что это была моя точка зрения.)
jpmc26
1
@immibis Я не считаю это полезным тестом. Использование поддельного хранилища данных не дает мне никакой уверенности, что оно будет работать с реальными вещами. Откуда я знаю, что моя подделка работает как настоящая вещь? Я бы предпочел, чтобы набор интеграционных тестов охватил его. (Я, вероятно, должен уточнить, что я имел в виду «любой модульный тест» в своем первом комментарии здесь.)
jpmc26
1
@immibis Я на самом деле пересматриваю свою позицию. Я скептически относился к значению модульных тестов из-за подобных проблем, но, возможно, я недооцениваю значение, даже если вы фальсифицируете ввод. Я действительно знаю, что тестирование ввода / вывода имеет тенденцию быть гораздо более полезным, чем фиктивные тяжелые тесты, но, возможно, отказ заменить ввод на фальшивку на самом деле является частью проблемы.
jpmc26
6

Должен bakeCookies()быть проверен? Да.

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

На самом деле, нет. Посмотрите внимательно на то, ЧТО должна делать функция - она ​​должна установить ovenобъект в определенное состояние. Глядя на код , представляется , что государства этого panи doughобъекты на самом деле не имеет большого значения . Поэтому вы должны передать ovenобъект (или смоделировать его) и подтвердить, что он находится в определенном состоянии в конце вызова функции.

Другими словами, вы должны утверждать, что bakeCookies()испекли печенье .

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

Модульные тесты выполняют две функции:

  1. Он проверяет, что все работает. Это наименее полезная функция, которую выполняет модульный тест, и кажется, что вы, кажется, учитываете эту функцию только при задании вопроса.

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

Не проверяйте код внутри функции. Вместо этого проверьте, что функция делает то, что говорит. Если вы посмотрите на модульные тесты таким образом (тестирование функций, а не кода), то поймете, что вы никогда не тестируете языковые конструкции или даже логику приложения. Вы тестируете API.

slebetman
источник
Привет, спасибо за ответ. Не могли бы вы взглянуть на мое второе обновление и высказать свое мнение о том, как выполнить модульное тестирование функции в этом примере?
Иона
Я считаю, что это может быть эффективно, когда вы можете использовать настоящую духовку или поддельную духовку, но значительно менее эффективно с ложной духовкой. У насмешек (согласно определениям Месароша) нет никакого состояния, кроме записи методов, которые к ним были вызваны. Мой опыт показывает, что когда подобные функции bakeCookiesтестируются таким образом, они имеют тенденцию к поломке во время рефакторинга, что не повлияет на наблюдаемое поведение приложения.
James_pic
@James_pic, точно. И да, это ложное определение, которое я использую. Итак, учитывая ваш комментарий, что вы делаете в таком случае? Отказаться от теста? Писать хрупкий, повторяющий реализацию тест? Что-то другое?
Иона
@Jonah В общем, я либо посмотрю на тестирование этого компонента с помощью интеграционных тестов (я обнаружил, что предупреждения об его отладке сложнее, чем обдува, возможно, из-за качества современных инструментов), либо возьму на себя задачу создания полуубедительная подделка.
James_pic
3

Должен ли savePeople () быть модульным, или такие тесты будут равносильны тестированию встроенной языковой конструкции forEach?

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

Здесь следует обратить внимание на то, как эта функция ведет себя при savePersonсбое на полпути? Как это должно работать?

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

Telastyn
источник
Да, я согласен, что тонкие условия ошибки должны быть проверены, но, на мой взгляд, это не интересный вопрос - ответ ясен. Отсюда причина, по которой я специально заявил, что для целей моего вопроса savePeopleне должен отвечать за обработку ошибок. Чтобы пояснить еще раз, предполагая, что savePeopleон отвечает только за итерацию по списку и делегируя сохранение каждого элемента другому методу, должен ли он все еще проверяться?
Иона
@Jonah: Если вы будете настаивать на том, чтобы ограничивать свой юнит-тест исключительно foreachконструкцией, а не какими-либо условиями, побочными эффектами или поведением вне ее, то вы правы; новый модульный тест на самом деле не так уж интересен.
Роберт Харви
1
@jonah - нужно ли перебирать и сохранять как можно больше или останавливаться на ошибке? Единственное сохранение не может решить это, так как оно не может знать, как оно используется.
Теластин
1
@jonah - добро пожаловать на сайт. Одним из ключевых компонентов нашего формата вопросов и ответов является то, что мы здесь не для того, чтобы вам помочь . Ваш вопрос, конечно, помогает вам, но также помогает многим людям, которые приходят на сайт в поисках ответов на свои вопросы. Я ответил на вопрос, который вы задали. Я не виноват, если вам не нравится ответ или вы предпочитаете сдвигать ворота. И, честно говоря, похоже, что другие ответы говорят о том же самом, хотя и более красноречиво.
Теластин
1
@Telastyn, я пытаюсь получить представление о модульном тестировании. Мой первоначальный вопрос не был достаточно ясным, поэтому я добавляю разъяснения, чтобы направить разговор к моему настоящему вопросу. Вы выбираете, что я как-то обманываю вас в игре «быть правым». Я потратил сотни часов, отвечая на вопросы по проверке кода и ТАК. Моя цель - всегда помогать людям, которым я отвечаю. Если у вас нет, это ваш выбор. Вам не нужно отвечать на мои вопросы.
Иона
3

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

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

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

Знание того, что работает, как и ожидалось, очень важно и не тривиально в больших проектах и ​​особенно в больших командах. Если у программистов есть что-то общее, так это то, что нам всем приходилось иметь дело с чужим ужасным кодом. Самое меньшее, что мы можем сделать, - это провести несколько тестов. Если сомневаетесь, напишите тест и двигайтесь дальше.

JeffO
источник
Спасибо за ваш отзыв. Хорошие моменты. Вопрос, на который я действительно хочу получить ответ (я только добавил еще одно обновление для пояснения), является надлежащим способом тестирования функций, которые не делают ничего, кроме вызова последовательности других служб посредством делегирования. В таких случаях кажется, что модульные тесты, подходящие для «документирования контракта», являются лишь повторением реализации функции, утверждая, что методы вызываются в различных имитациях. И все же тест, идентичный реализации в этих случаях, кажется неправильным ...
Иона
1

Должен ли savePeople () быть модульным, или такие тесты будут равносильны тестированию встроенной языковой конструкции forEach?

На это уже ответил @BryanOakley, но у меня есть несколько дополнительных аргументов (наверное):

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

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

В-третьих, имеет смысл внедрить тривиальный тест, как в подходе TDD (который его обязывает), так и вне его.

При написании C ++ для своих классов я склонен писать тривиальный тест, который создает экземпляр объекта и проверяет инварианты (присваиваемые, обычные и т. Д.). Мне показалось удивительным, сколько раз этот тест нарушался во время разработки (например, путем добавления неподвижного члена в класс по ошибке).

utnapistim
источник
1

Я думаю, что ваш вопрос сводится к:

Как выполнить модульное тестирование пустой функции, не являясь интеграционным тестом?

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

Если после вызова функции нам нужно вызвать pan.GetCookies, мы можем спросить, действительно ли это «интеграционный тест» или «но разве мы просто не тестируем объект pan?»

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

Но! Я бы сказал, что в этом случае вам следует провести рефакторинг ваших void-функций, чтобы вернуть тестируемый результат ИЛИ использовать реальные объекты и сделать интеграционный тест

--- Обновление для примера createNewUser

  • новая запись пользователя должна быть создана в базе данных
  • Приветственное письмо должно быть отправлено
  • IP-адрес пользователя должен быть записан в целях мошенничества.

ОК, так что на этот раз результат функции не легко вернуть. Мы хотим изменить состояние параметров.

Это где я становлюсь немного спорным. Я создаю конкретные реализации макета для параметров с состоянием

Пожалуйста, дорогие читатели, постарайтесь контролировать свою ярость!

так...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Это отделяет детали реализации тестируемого метода от желаемого поведения. Альтернативная реализация:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

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

Ewan
источник
это не интеграционный тест просто потому, что вы упомянули имена двух конкретных классов ... интеграционные тесты - это тестирование интеграции с внешними системами, такими как дисковый ввод-вывод, БД, внешние веб-сервисы и т. д. Вызов pan.getCookies () -память, быстро, проверяет, что нас интересует и т. д. Я согласен с тем, что метод, возвращающий куки-файлы, выглядит как лучший дизайн.
Сара
3
Подождите. Насколько мы знаем, pan.getcookies отправляет электронное письмо повару с просьбой вынуть печенье из духовки, когда у них появляется такая возможность
Ewan
Я предполагаю, что это теоретически возможно, но это было бы довольно обманчивое имя. кто нибудь слышал о печном оборудовании, которое отправляло электронные письма? но я вижу вашу точку зрения, это зависит Я предполагаю, что эти сотрудничающие классы являются листовыми объектами или просто обычными вещами в памяти, но если они делают хитрые вещи, тогда нужна осторожность. Я думаю, что отправка электронной почты определенно должна быть сделана на более высоком уровне, чем это, хотя. это похоже на грязную бизнес-логику в сущностях.
Сара
2
Это был риторический вопрос, но: «кто когда-либо слышал об оборудовании печи, которое отправляло электронные письма?» venturebeat.com/2016/03/08/...
clacke
Привет Иван, я думаю, что этот ответ приближается к тому, что я действительно спрашиваю. Я думаю, что ваша точка зрения о bakeCookiesвозврате испеченного печенья является правильной, и у меня была некоторая мысль после публикации. Так что я думаю, что это снова не очень хороший пример. Я добавил еще одно обновление, которое, я надеюсь, дает более реалистичный пример того, что мотивирует мой вопрос. Буду признателен за ваш вклад.
Иона
0

Вы должны также проверить bakeCookies- что даст / должен bakeCookies(egg, pan, oven)привести ... Жареное яйцо или исключение? Сами по себе ни те, panни другие ovenингредиенты не будут заботиться о них, так как ни один из них не должен, но bakeCookiesобычно должен давать печенье. В более общем плане это может зависеть от того, как doughполучается , и есть ли это какой -то шанс , что становится просто eggили , например , waterвместо этого.

Тобиас Кинцлер
источник