Де знаходиться межа між логікою тестування одиниці тестування та недовірливими мовними конструкціями?


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. Якщо його викликає веб-інтерфейс, він може перевести помилку у відповідне повідомлення, яке відображатиметься користувачеві тощо. Справа в тому, що ця функція не знає, як поводитися з помилками, які можуть бути викинуті.

Суть моєї плутанини полягає в тому, що для тестування такої функції видається необхідним повторити точну реалізацію в самому тесті (вказавши, що методи викликаються макетами в певному порядку), і це здається неправильним.


44
Після його запуску. У вас є печиво
Еван

6
щодо вашого оновлення: чому ви хочете знущатися над сковородою? чи тісто? вони звучать як прості об’єкти в пам'яті, які слід тривіально створювати, і тому немає жодної причини, чому ви не повинні перевіряти їх як одну одиницю. пам'ятайте, що "одиниця" в "тестуванні одиниць" не означає "єдиний клас". це означає "найменшу можливу одиницю коду, яка використовується для того, щоб щось зробити". сковорода - це, мабуть, не що інше, як контейнер для предметів з тіста, тому було б надумано перевірити його ізольовано, а не просто тестувати за допомогою методу bakeCookies ззовні.
Сара,

11
Зрештою, основоположний принцип роботи тут полягає в тому, що ви пишете достатньо тестів, щоб переконатися, що код працює, і що це адекватна "канарка у вугільній шахті", коли хтось щось змінює. Це воно. Немає ніяких магічних закликів, формулярних припущень чи догматичних тверджень, тому 85-90% покриття коду (не 100%) вважається відмінним.
Роберт Харві

5
@RobertHarvey, на жаль, формальні банальності та звукові укуси TDD, але впевнений, що ви заробите з ентузіазмом кивок на згоду, не допоможете вирішити проблеми в реальному світі. для цього вам потрібно забруднити руки і ризикнути відповісти на актуальне запитання
Jonah

4
Одиничне випробування в порядку зменшення цикломатичної складності. Повірте, у вас не вистачить часу, перш ніж перейти на цю функцію
Ніл МакГуйган

Відповіді:


118

Чи повинен savePeople()бути перевірений блок? Так. Ви не тестуєте, що dataStore.savePersonпрацює, або що db-з'єднання працює, або навіть що foreachпрацює. Ви випробовуєте, що savePeopleвиконує обіцянку, яку вона дає через свій контракт.

Уявіть собі цей сценарій: хтось робить великий рефактор бази коду і випадково видаляє forEachчастину реалізації, щоб вона завжди зберігала лише перший елемент. Ви не хочете, щоб одиничний тест був для цього?


20
@RobertHarvey: Там багато сірої області, і зробити відмінність, IMO, не важливо. Ти маєш рацію - проте не дуже важливо перевіряти те, що "він викликає правильні функції", а, скоріше, "це робить правильно", незалежно від того, як це робиться. Важливо - перевірити, що, враховуючи певний набір входів у функцію, ви отримуєте певний набір результатів. Однак я бачу, як це останнє речення може бути заплутаним, тому я його зняв.
Брайан Оуклі

64
"Ви випробовуєте, що savePeople виконує обіцянку, яку вона дає через свій контракт." Це. Стільки цього.
Ловіс

2
Якщо у вас є тест системи, що охоплює його.
Ян

6
@Ian End to end тести не замінюють одиничні тести, вони безкоштовні. Тільки тому, що у вас може бути тест в кінці, який гарантує вам збереження списку людей, не означає, що ви не повинні мати тест на одиницю, щоб також його охопити.
Вінсент Савард

4
@VincentSavard, але вартість / вигода від одиничного тесту зменшується, якщо ризик контролюється в певний спосіб.
Ян

36

Зазвичай подібне питання виникає, коли люди роблять "тестові" розробки. Підійдіть до цієї проблеми з точки зору TDD, де перед впровадженням виходять тести, і поставте собі це питання ще раз як вправу.

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

Зрештою, тести не лише перевірять, чи можна правильно зберегти Personв Store, але й багатьох людей. Це також призвело б до мене питання про те, чи потрібні інші перевірки, наприклад: "Чи потрібно мені переконатися, що savePeopleфункція є атомною, зберігаючи всі або ні одну?", "Чи може це якимось чином повернути помилки людям, які не могли" t не може бути збережено? Як виглядатимуть ці помилки? "тощо. Все це означає набагато більше, ніж просто перевірка на використання тієї forEachчи іншої форми ітерації.

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


4
В основному тестуйте інтерфейс, а не реалізацію.
Снуп

8
Справедливі та проникливі моменти. Але я відчуваю, що моє справжнє питання ухиляється :) Ваша відповідь говорить: "У реальному світі, у добре розробленій системі, я не думаю, що ця спрощена версія вашої проблеми не існувала б". Знову справедливо, але я спеціально створив цю спрощену версію, щоб висвітлити суть більш загальної проблеми. Якщо ви не можете оминути штучну природу прикладу, ви, можливо, можете уявити інший приклад, у якому у вас є вагомі причини для подібної функції, яка робила лише ітерацію та делегування. А може, ви вважаєте, що це просто неможливо?
Йона

@Jonah оновлено. Я сподіваюся, що він відповість на ваше запитання трохи краще. Це все дуже грунтується на думці і може бути проти цілі цього веб-сайту, але це, безумовно, дуже цікаве обговорення. До речі, я спробував відповісти з точки зору професійної роботи, де ми повинні прагнути залишити одиничні тести на всю поведінку додатків, незалежно від того, наскільки тривіальною може бути реалізація, тому що ми маємо обов’язок створити добре перевірену та задокументована система для нових обслуговуючого персоналу, якщо ми підемо. Що стосується особистих чи, скажімо, некритичних (гроші також є критичними) проектів, я маю зовсім іншу думку.
MichelHenrich

Дякуємо за оновлення. Як саме ви б протестували savePeople? Як я описав в останньому абзаці ОП чи іншим способом?
Йона

1
Вибачте, я не прояснив себе частиною "без глуздів". Я мав на увазі, що не буду використовувати макет для savePersonфункції, як ви запропонували, замість цього я перевірив би її більш загальну savePeople. Одиничні тести для Storeбуде змінено для запуску, savePeopleа не безпосередньо виклику savePerson, тому для цього не використовуються макети. Але, звичайно, база даних не повинна бути присутнім, оскільки ми хотіли б виділити проблеми кодування від різних проблем інтеграції, що виникають із фактичними базами даних, тому тут ми все ще маємо насмішку.
MichelHenrich

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. Якщо його викликає веб-інтерфейс, він може перевести помилку у відповідне повідомлення, яке відображатиметься користувачеві тощо. Справа в тому, що ця функція не знає, як поводитися з помилками, які можуть бути викинуті.

Очікувані винятки / помилки є дійсними тестовими випадками: Ви підтверджуєте, що у випадку, якщо така подія трапиться, функція поводиться так, як ви очікували б. Цього можна досягти, дозволяючи відповідний макет / підроблений / заглушений предмет кидати за бажанням.

Суть моєї плутанини полягає в тому, що для тестування такої функції видається необхідним повторити точну реалізацію в самому тесті (вказавши, що методи викликаються макетами в певному порядку), і це здається неправильним.

Іноді це доводиться робити (хоча ви здебільшого дбаєте про це в інтеграційних тестах). Частіше існують інші способи перевірити очікувані побічні ефекти / зміни стану.

Перевірка точних викликів функцій спричиняє досить крихкі тестові одиниці: Лише невеликі зміни оригінальної функції призводять до їх відмови. Це можна бажати чи ні, але це вимагає змін у відповідних тестах одиниць під час зміни функції (будь то рефакторинг, оптимізація, виправлення помилок, ...).

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

Для прикладу розглянемо, хто додає виклик oven.preheat()(оптимізація!) У вашому прикладі випічки файлів cookie:

  • Якщо ви знущалися над духовним об’єктом, він не очікує цього виклику і не завершить тест, хоча спостережлива поведінка методу не змінилася (сподіваємось, у вас все ще є сковорода куки).
  • Заглушка може або не може вийти з ладу, залежно від того, додали ви лише тестовані методи або весь інтерфейс деякими фіктивними методами.
  • Підробка не повинна провалюватися, оскільки вона має реалізувати метод (відповідно до інтерфейсу)

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


1
Проблема полягає в тому, що як тільки ви пишете, myDataStore.containsPerson('Joe')ви припускаєте наявність функціональної бази тестів. Після цього ви пишете інтеграційний тест, а не одиничний тест.
Йона

Я припускаю, що я можу покластися на тестовий сховище даних (мені байдуже, чи це справжній чи знущальний), і що все працює як налаштовано (оскільки я повинен мати одиничні тести для цих випадків). Єдине, що тест хоче перевірити, це те, що savePeople()насправді додає цих людей до будь-якого сховища даних, який ви надаєте, доки цей сховище даних реалізує очікуваний інтерфейс. Тест на інтеграцію буде, наприклад, перевірити, чи справді моя обгортка бази даних виконує правильні виклики бази даних для виклику методу.
hoffmale

Для уточнення, якщо ви використовуєте макет, все, що ви можете зробити, це стверджувати, що метод з цього макету викликався , можливо, з певним параметром. Ви не можете стверджувати про стан макети після цього. Отже, якщо ви хочете зробити твердження про стан бази даних після виклику методу, який перевіряється, як у myDataStore.containsPerson('Joe'), вам доведеться використовувати функціональний db певного типу. Коли ви зробите цей крок, це вже не одиничний тест.
Йона

1
це не повинно бути реальною базою даних - просто об’єктом, який реалізує той самий інтерфейс, що і реальний сховище даних (читайте: він проходить відповідні тестові одиниці для інтерфейсу сховища даних). Я б все-таки вважав це глузуванням. Нехай макет зберігає все, що додається будь-яким методом для цього в масив, і перевіряє, чи myPeopleє в масиві тестові дані (елементи ). IMHO макет все одно повинен мати таку саму поведінку, що спостерігається, що очікується від реального об'єкта, інакше ви перевіряєте відповідність інтерфейсу макету, а не реального інтерфейсу.
гофмале

"Нехай макет зберігає все, що додається будь-яким методом для цього в масиві, і перевіряє, чи є дані тесту (елементи myPeople) у масиві" - це все ще "справжня" база даних, лише спеціальна, пам'ять, яку ви створили. "IMHO макет все-таки повинен мати таку саму поведінку, що спостерігається, що очікується від реального об'єкта" - я вважаю, що ви можете виступати за це, але це не те, що "знущатися" означає в тестувальній літературі або в будь-якій з популярних я бібліотек " я бачив. Макет просто підтверджує, що очікувані методи викликаються із очікуваними параметрами.
Йона

13

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

У своїй кар’єрі я багато робив оптимізацію продуктивності, і часто знаходив проблеми з точною схемою, яку ви продемонстрували: щоб зберегти N баз даних у базі даних, виконайте N вставок. Зазвичай ефективніше робити об'ємну вставку, використовуючи один вислів.

З іншого боку, ми також не хочемо передчасно оптимізувати. Якщо ви зазвичай заощаджуєте лише 1 - 3 людини одночасно, то написання оптимізованої партії може бути зайвим.

Завдяки правильному тестуванню одиниці ви можете записати його так, як ви його здійснили вище, і якщо вам здається, що вам потрібно його оптимізувати, ви можете зробити це за допомогою безпечної мережі автоматизованого тесту для виявлення будь-яких помилок. Звичайно, це змінюється залежно від якості тестів, тому випробовуйте вільно і тестуйте добре.

Вторинна перевага одиничного тестування такої поведінки полягає в тому, щоб служити документацією для того, яка її мета. Цей тривіальний приклад може бути очевидним, але з огляду на наступний пункт нижче, це може бути дуже важливим.

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

Я додам думку, яку я отримав від інструктора TDD. Не тестуйте метод. Перевірте поведінку. Іншими словами, ви не savePeopleтестуєте, що працює, ви перевіряєте, що за один дзвінок можна зберегти декілька користувачів.

Я виявив свою здатність зробити тестування одиниць якості та покращення TDD, коли я перестав думати про тести одиниць як про перевірку того, що програма працює, але, скоріше, вони перевіряють, що одиниця коду робить те, що я очікую . Вони різні. Вони не підтверджують, що це працює, але вони перевіряють, що він робить те, що я думаю, що це робить. Коли я почав думати таким чином, моя точка зору змінилася.


Приклад рефакторингу об'ємних вставок є хорошим. Можливий тест одиниць, який я запропонував в ОП - що макет данихStore savePersonзакликав його для кожної людини у списку, однак порушиться з рефакторингом об'ємних вставок. Що для мене вказує, що це поганий одиничний тест. Однак я не бачу альтернативи, яка б пропускала як масові, так і одні-рятувальні на людину реалізації, не використовуючи фактичну базу даних тестів і не стверджуючи проти неї, що здається неправильним. Чи можете ви надати тест, який працює для обох реалізацій?
Йона

1
@ jpmc26 А як щодо тесту, який перевіряє, що люди були врятовані ...?
іммібіс

1
@immibis Я не розумію, що це означає. Імовірно, справжній магазин підтримується базою даних, тому вам доведеться знущатися або заглушувати його для одиничного тесту. Тож у цей момент ви перевірите, чи ваш макет чи заглушка можуть зберігати предмети. Це абсолютно марно. Найкраще, що ви могли зробити, це стверджувати, що savePersonметод викликався для кожного вводу, і якщо ви замінили цикл об'ємною вставкою, ви більше не будете викликати цей метод. Так ваш тест зламається. Якщо у вас є щось інше на увазі, я відкритий для цього, але цього ще не бачу. (І не бачачи, що це було моєю точкою.)
jpmc26

1
@immibis Я не вважаю це корисним тестом. Використання підробленого сховища даних не дає мені впевненості, що він буде працювати з реальною справою. Як я можу знати, що мої підробки працюють як реальна річ? Я хотів би просто дозволити пакет інтеграційних тестів висвітлювати його. (Я, мабуть, повинен уточнити, що я мав на увазі "будь-яке тестування одиниці " в своєму першому коментарі тут.)
jpmc26,

1
@immibis Я фактично переглядаю свою позицію. Я скептично ставився до вартості одиничних тестів через подібні проблеми, але, можливо, я недооцінюю цінність, навіть якщо ви підробляєте дані. Я дійсно знаю , що вхід / вихід тестування має тенденцію бути набагато більш корисними , ніж фіктивні важкі випробування, але , можливо, відмова замінити вхід з підробкою фактично є частиною проблеми тут.
jpmc26

6

Чи bakeCookies()слід тестувати? Так.

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

Не зовсім. Придивіться уважно до того, що функція повинна виконувати - вона повинна встановити ovenоб'єкт на певний стан. Якщо подивитися на код, то виявляється, що стан panта doughоб'єкти насправді не мають великого значення. Тож вам слід передати ovenоб’єкт (або знущатися над ним) і стверджувати, що він знаходиться в певному стані в кінці виклику функції.

Іншими словами, ви повинні стверджувати, що bakeCookies()печене печиво .

Для дуже коротких функцій одиничні тести можуть здатися трохи більше, ніж тавтологія. Але не забувайте, що ваша програма триватиме набагато довше, ніж час, коли ви її працюєте. Ця функція може або не може змінитися в майбутньому.

Блок тестів виконує дві функції:

  1. Це перевіряє, що все працює. Це найменш корисна тестова функціональна одиниця, і, здається, ви розглядаєте цю функціональність лише тоді, коли задаєте питання.

  2. Він перевіряє, чи майбутні модифікації програми не порушують функціонал, який був реалізований раніше. Це найкорисніша функція одиничних тестів, яка перешкоджає введенню помилок у великі програми. Це корисно в звичайному кодуванні при додаванні функцій до програми, але воно більш корисне при рефакторингу та оптимізаціях, коли основні алгоритми, що реалізують програму, різко змінюються, не змінюючи жодної спостережуваної поведінки програми.

Не перевіряйте код всередині функції. Замість того, щоб перевірити, що функція робить те, що вона каже, що робить. Якщо ви дивитесь на одиничні тести таким чином (функції тестування, а не код), то зрозумієте, що ви ніколи не тестуєте мовні конструкції або навіть логіку програми. Ви тестуєте API.


Привіт, дякую за вашу відповідь. Чи проти заглянути в моєму другому оновлення та висловити свої думки щодо тестування функції тесту в цьому прикладі?
Йона

Я вважаю, що це може бути ефективним, коли ви можете використовувати справжню духовку або фальшиву духовку, але значно меншою ефективністю для макетної духовки. Знущання (за визначенням Мессароса) не мають жодного стану, окрім запису методів, що їх викликали. Мій досвід полягає в тому, що коли такі функції bakeCookiesперевірені таким чином, вони мають тенденцію до ламання під час рефакторів, що не вплине на поведінку програми, що спостерігається.
James_pic

@James_pic, точно. І так, саме таке макетне визначення я використовую. Отже, з огляду на ваш коментар, що ви робите у такому випадку? Відмовилися від тесту? Напишіть тендітний, повторюваний впровадження тест все-таки? Щось ще?
Йона

@Jonah Я взагалі або дивлюсь на тестування цього компонента за допомогою інтеграційних тестів (я виявив, що попередження про те, що важче відладку перекрити, можливо, завдяки якості сучасних інструментів), або піти на проблему зі створення напівпереконлива підробка.
James_pic

3

Чи повинен тест savePeople () бути тестовим, чи такі тести означатимуть тестування вбудованої конструкції мови forEach?

Так. Але ти міг би це зробити так, що просто перевірив би конструкцію.

Тут слід зауважити, як поводиться ця функція, коли savePersonпровалюється на півдорозі? Як це має працювати?

Це така тонка поведінка, яку функція передбачає, що ви повинні застосовувати за допомогою одиничних тестів.


Так, я погоджуюся, що умови перевірки помилок повинні бути перевірені, але imo це не цікаве питання - відповідь зрозуміла. Отже, я спеціально зазначив, що для мого запитання savePeopleне слід нести відповідальність за обробку помилок. Щоб ще раз уточнити, припускаючи, що savePeopleце відповідає лише за повторення списку та делегування збереження кожного елемента іншому методу, чи все-таки його слід перевірити?
Йона

@Jonah: Якщо ви будете наполягати на обмеженні свого тесту на одиницю виключно foreachконструкцією, а не будь-якими умовами, побічними ефектами чи поведінкою поза нею, то ви маєте рацію; новий тест блоку дійсно не все так цікаво.
Роберт Харві

1
@jonah - чи варто повторити і зберегти якомога більше або зупинитись на помилках? Одноразове збереження не може цього вирішити, оскільки не може знати, як воно використовується.
Теластин

1
@jonah - ласкаво просимо на сайт. Одним із ключових компонентів нашого формату запитань є те, що ми не тут, щоб допомогти вам . Ваше питання, звичайно, допомагає вам, але також допомагає багатьом іншим, хто заходить на сайт, шукаючи відповіді на свої запитання. Я відповів на поставлене вами питання. Я не винен, якщо відповідь вам не подобається або ви бажаєте зрушити ворота. І, чесно кажучи, схоже, що інші відповіді говорять про те саме основне, хоча і красномовніше.
Теластин

1
@Telastyn, я намагаюся отримати уявлення про тестування одиниць. Моє початкове запитання було недостатньо зрозумілим, тому я додаю роз'яснення, щоб спрямувати розмову до мого реального питання. Ви вирішили інтерпретувати, що як я якось обманюю вас у грі «бути правим». Я витратив сотні годин, відповідаючи на запитання щодо перегляду коду та ТАК. Моя мета - завжди допомагати людям, яким я відповідаю. Якщо вашого немає - це ваш вибір. Не потрібно відповідати на мої запитання.
Йона

3

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

Те, що писати тест так легко, має бути ознакою того, що ваш дизайн не такий уже й поганий. Ви б віддали перевагу дизайну, який непросто перевірити?

"Це ніколи не зміниться". так починається більшість невдалих проектів. Тест одиниці визначає лише, чи працює прилад за певних обставин. Запропонуйте йому пройти, і тоді ви можете забути про деталі його реалізації та просто скористатися ними. Використовуйте цей мозковий простір для наступного завдання.

Знання того, як справи працюють, як очікувалося, дуже важливо і не є тривіальним у великих проектах і особливо великих командах. Якщо є одне спільне між програмістами, це той факт, що ми всі мали справу з чужим жахливим кодом. Найменше ми можемо зробити кілька тестів. Якщо ви сумніваєтесь, напишіть тест і рухайтеся далі.


Дякуємо за відгуки Хороші бали. Питання, на яке я дійсно хочу відповісти (я тільки що додав ще одне оновлення для уточнення) - це відповідний спосіб перевірити функції, які не мають нічого іншого, як викликати послідовність інших служб шляхом делегування. У таких випадках здається, що одиничні тести, підходящі для "документування договору", є лише переглядом реалізації функції, стверджуючи, що методи викликаються різними макетами. І все-таки тест, ідентичний виконанню, у цих випадках відчуває себе неправильно ....
Йона

1

Чи повинен тест savePeople () бути тестовим, чи такі тести означатимуть тестування вбудованої конструкції мови forEach?

На це вже відповів @BryanOakley, але у мене є додаткові аргументи (я думаю):

По-перше, тест на одиницю - це перевірка виконання договору, а не реалізація API; тест повинен встановити передумови, потім викликати, а потім перевірити на ефекти, побічні ефекти, будь-які інваріанти та умови після. Коли ви вирішите, що перевірити, реалізація API не має значення (і не повинна) .

По-друге, ваш тест буде там, щоб перевірити інваріанти, коли функція зміниться . Справа в тому , що не змінюється в даний час не означає , що ви не повинні мати випробування.

По-третє, є цінність у здійсненні тривіального тесту як у TDD-підході (який мандатує), так і поза ним.

Коли я пишу на C ++, для своїх класів я схильний писати тривіальний тест, який створює об'єкт і перевіряє інваріанти (призначені, регулярні тощо). Мені було дивно, скільки разів цей тест ламається під час розробки (наприклад, - додавши непорушного члена до класу, помилково).


1

Я думаю, що ваше питання зводиться до:

Як я можу перевірити функцію void, не будучи тестом інтеграції?

Якщо ми змінимо функцію випічки файлів cookie, щоб повернути файли cookie, наприклад, стає очевидно, яким повинен бути тест.

Якщо нам доведеться викликати pan.GetCookies після виклику функції, хоча ми можемо поставити під сумнів, чи є "справді тест на інтеграцію" чи ", але хіба ми просто тестуємо об'єкт" пан "?"

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

Але! Я б заперечував, що в такому випадку вам слід відновлювати свої недійсні функції, щоб повернути перевірений результат АБО використовувати реальні об'єкти та зробити інтеграційний тест

--- Оновлення для прикладу 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);
}

Все одно пройдуть тест на одиницю. Крім того, ви маєте перевагу в тому, що зможете повторно використовувати макетні об’єкти через тести, а також ввести їх у свою заявку на тести інтерфейсу чи інтеграції.


це не тест на інтеграцію просто тому, що ви згадуєте назви двох конкретних класів ... інтеграційні тести - це тестування інтеграцій із зовнішніми системами, такими як дисковий IO, БД, зовнішні веб-сервіси і т. д. виклик pan.getCookies () знаходиться в -поміть, швидко, перевіряє предмет, який нас цікавить і т. д. Я погоджуюся, що використання методу повернення файлів cookie безпосередньо відчуває себе кращим дизайном.
сара

3
Зачекайте. З усього, що ми знаємо, pan.getcookies надсилає електронне повідомлення кухару з проханням вийняти печиво з духовки, коли вони отримають шанс
Еван,

Я думаю, це теоретично можливо, але це було б досить оманливим ім'ям. хто коли-небудь чув про обладнання духовки, яке надсилало електронні листи? але я бачу, ви вказуєте, це залежить. Я припускаю, що ці спільні заняття є предметами з листя або просто простими речами в пам’яті, але якщо вони роблять підступні речі, тоді потрібна обережність. Я думаю, що надіслати електронною поштою, безумовно, слід робити на більш високому рівні, ніж це. Це здається, що неприємна та брудна бізнес-логіка в організаціях.
сара

2
Це було риторичне питання, але: "хто коли-небудь чув про обладнання духовки, яке надсилало електронні листи?" venturebeat.com/2016/03/08/…
clacke

Вітаю, Еване, я думаю, що ця відповідь наближається до того, що я насправді прошу. Я думаю, що ваша думка щодо bakeCookiesповернення запеченого печива є місцем, і я подумав після публікації. Тому я думаю, що це ще раз не чудовий приклад. Я додав ще одне оновлення, яке, сподіваюся, дає більш реалістичний приклад того, що мотивує моє запитання. Буду вдячний за ваш внесок.
Йона

0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.