Должно ли 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).
- Заглушка может или не может дать сбой, в зависимости от того, добавляете ли вы только тестируемые методы или весь интерфейс с некоторыми фиктивными методами.
- Подделка не должна потерпеть неудачу, так как она должна реализовывать метод (согласно интерфейсу)
В моих модульных тестах я стараюсь быть как можно более общим: если реализация меняется, но видимое поведение (с точки зрения вызывающего) остается тем же, мои тесты должны пройти. В идеале, единственным случаем, когда мне нужно изменить существующий модульный тест, должно быть исправление ошибки (теста, а не тестируемой функции).