Чи гарантує покриття шляху пошук усіх помилок?


64

Якщо кожен шлях через програму перевіряється, чи гарантує це пошук усіх помилок?

Якщо ні, то чому б і ні? Як ви могли пройти всі можливі поєднання потоку програми і не знайти проблеми, якщо така існує?

Я вагаюся, припускаючи, що "всі помилки" можна знайти, але, можливо, це тому, що покриття контуру не практичне (як це комбінаторне), тому воно ніколи не виникає?

Примітка. Ця стаття дає короткий підсумок типів покриття, як я думаю про них.


33
Це еквівалентно проблемі зупинки .

31
Що робити, якщо код, який мав би бути там, чи не так?
RemcoGerlich

6
@Snowman: Ні, це не так. Вирішити проблему зупинки для всіх програм неможливо, але для багатьох конкретних програм вона вирішена. Для цих програм усі шляхи коду можуть бути перераховані за обмежений (хоча й можливо довгий) проміжок часу.
Jørgen Fogh

3
@ JørgenFogh Але, намагаючись знайти помилки в будь-якій програмі, чи не апріорі невідомо, зупиняється програма чи ні? Чи не це питання щодо загального методу "пошуку всіх помилок у будь-якій програмі через покриття шляху"? У такому випадку, чи не схоже це на "пошук, чи зупиняється якась програма"?
Андрес Ф.

1
@AndresF. невідомо лише, чи програма зупиняється, якщо підмножина мови, якою вона написана, здатна виражати програму, що не зупиняється. Якщо ваша програма написана на C, не використовуючи без обмежених циклів / рекурсії / setjmp тощо, або в Coq, або в ESSL, вона повинна зупинитися і простежити всі шляхи. (Тюрінг-повнота серйозно завищена)
Левшенко

Відповіді:


128

Якщо кожен шлях через програму перевіряється, чи гарантує це пошук усіх помилок?

Немає

Якщо ні, то чому б і ні? Як ви могли пройти всі можливі поєднання потоку програми і не знайти проблеми, якщо така існує?

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

def Add(x as Int32, y as Int32) as Int32:
   return x + y

Test.Assert(Add(2, 2) == 4) //100% test coverage
Add(MAXINT, 5) //Throws an exception, despite 100% test coverage

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

- EW Dijkstra (Додано наголос. Написано в 1988 р. Зараз це вже значно більше 2 десятиліть.)


7
@digitgopher: Я гадаю, але якщо програма не має ніякого вкладу, яку корисну справу вона робить?
Мейсон Уілер

34
Також є можливість пропустити інтеграційні тести, помилки в тестах, помилки в залежності, помилки в системі збирання / розгортання або помилки в оригінальній специфікації / вимогах. Ніколи не можна гарантувати пошук усіх помилок.
Іксрек

11
@Ixrec: Хоча SQLite робить зухвале зусилля! Але подивіться, які величезні зусилля! Це не може бути масштабним для великих кодових баз.
Мейсон Уілер

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

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

71

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

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


2
Це може переконатися, що при виклику перевіреного коду (з параметрами в тесті) немає винятку. Це трохи більше, ніж нічого.
Paŭlo Ebermann

7
@ PaŭloEbermann Погодився, трохи більше, ніж нічого. Однак це надзвичайно менше, ніж "пошук усіх помилок";)
Андрес Ф.

1
@ PaŭloEbermann: Винятки - це шлях коду. Якщо код може кинутись, але певні дані тесту не кидаються, тест не досягає 100% покриття шляху. Це не властиво виняткам як механізму обробки помилок. Visual Basic - х ON ERROR GOTOтакож шлях, як C - х if(errno).
MSalters

1
@MSalters Я говорю про код, який (за специфікацією) не повинен викидати жодного винятку, незалежно від вкладених даних. Якщо вона кине яку-небудь, це буде помилка. Звичайно, якщо у вас є код, який вказано, щоб кинути виняток, його слід перевірити. (І звичайно, як сказав Йорг, просто перевірити, що код не кидає виняток, зазвичай недостатньо, щоб переконатися, що він робить правильно, навіть для коду, що не кидає.) І деякі винятки можуть бути викинуті -видимий шлях коду, як для відміни нуля вказівника або ділення на нуль. Чи підхоплює це ваш інструмент покриття шляху?
Paŭlo Ebermann

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

34

Ось простіший приклад округлення речей. Розглянемо наступний алгоритм сортування (на Java):

int[] sort(int[] x) { return new int[] { x[0] }; }

Тепер перевіримо:

sort(new int[] { 0xCAFEBABE });

Тепер, врахуйте, що (A) цей конкретний виклик sortповертає правильний результат, (B) всі тести коду були охоплені цим тестом.

Але, очевидно, програма насправді не сортує.

Звідси випливає, що покриття всіх кодових шляхів недостатньо для гарантії того, що програма не має помилок.


12

Розглянемо absфункцію, яка повертає абсолютне значення числа. Ось тест (Python, уявіть собі деякі тестові рамки):

def test_abs_of_neg_number_returns_positive():
    assert abs(-3) == 3

Ця реалізація правильна, але вона отримує лише 60% покриття коду:

def abs(x):
    if x < 0:
        return -x
    else:
        return x

Ця реалізація неправильна, але вона отримує 100% охоплення коду:

def abs(x):
    return -x

2
Ось ще одна реалізація, яка проходить тест (будь ласка, пробачте нелінійний Python): def abs(x): if x == -3: return 3 else: return 0Ви, можливо, зможете відхилити else: return 0частину та отримати 100% покриття, але ця функція буде по суті марною, навіть якщо вона проходить тест на одиницю.
CVn

7

Ще одне доповнення до відповіді Мейсона , поведінка програми може залежати від середовища виконання.

Наступний код містить Use-After-Free:

int main(void)
{
    int* a = malloc(sizeof(a));
    int* b = a;
    *a = 0;
    free(a);
    *b = 12; /* UAF */
    return 0;
}

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

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

int main(int a, int b)
{
    if (a != b) {
        if (cryptohash(a) == cryptohash(b)) {
            return ERROR;
        }
    }
    return 0;
} 

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


Легко для досить малих цілих чисел :)
CodesInChaos

Не знаючи нічого про cryptohashце, трохи важко сказати, що таке "досить малий". Можливо, на суперкалькулятор потрібно два дні. Але так, intможе виявитися небагато short.
dureuill

З 32-бітовими цілими числами та типовими криптографічними хешами (SHA2, SHA3 тощо) обчислення цього повинно бути досить дешевим. Кілька секунд або близько того.
CodesInChaos

7

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

Інший спосіб відповісти на це питання - це практичний досвід:

У реальному світі, і справді на вашому власному комп’ютері, існує багато програмного забезпечення, розроблених за допомогою набору тестів, які дають 100% охоплення та які все ще мають помилки, включаючи помилки, які краще тестування визначили б.

Таким чином, пов'язане питання:

Який сенс інструментів покриття коду?

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

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

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

При покритті коду може бути заманливо, особливо якщо у вас майже ідеальні 98%, заповнювати справи так, щоб залишилися шляхи.

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

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


+1 Мені подобається ця відповідь, оскільки вона конструктивна і згадує деякі переваги покриття.
Андрес Ф.

4

Покриття на шляху не може визначити, чи були реалізовані всі необхідні функції. Покинути функцію - це помилка, але покриття шляху не виявить її.


1
Я думаю, що це залежить від визначення помилки. Я не думаю, що відсутні функції або функції не слід розглядати як помилки.
eis

@eis - ви не бачите проблем із продуктом, в документації якого написано, що він робить X, а насправді це не так? Це досить вузьке визначення поняття "помилка". Коли я вдавав QA для лінійки продуктів C ++ Borland, ми не були такими щедрими.
Піт Бекер

Я не розумію , чому б документації кажуть , що це робить X , якщо це не було реалізовано
EIS

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

На жаль, покриття шляху - це тестування білого поля , а не чорне поле . Тестування білого поля не може вловлювати відсутні функції.
Піт Бекер

4

Частина питання полягає в тому, що 100% покриття гарантує лише те, що код буде функціонувати правильно після одного виконання . Деякі помилки, як-от витоки пам'яті, можуть не виявлятися або спричиняти проблеми після одного виконання, але з часом це призведе до проблем для програми.

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


Погодились, що це частина питання, але справжнє питання є більш фундаментальним, ніж це. Навіть з теоретичним комп'ютером з нескінченною пам'яттю і без одночасності, 100% тестове покриття не означає відсутність помилок. Тривіальні приклади цього рясу у відповідях тут, але ось інше: якщо моя програма times_two(x) = x + 2, це буде повністю покрито тестовим набором assert(times_two(2) == 4), але це все одно очевидно баггі-код! Не потрібно витоків пам’яті :)
Андрес Ф.

2
Це чудовий момент, і я визнаю, що це більший / основоположний цвях у гробці можливості безпроблемних додатків, але, як ви кажете, це вже було додано тут, і я хотів додати щось, що не було досить охоплене існуючі відповіді. Я чув про програми, які вийшли з ладу, оскільки з'єднання з базами даних не були відпущені назад у пул зв’язків, коли вони більше не потрібні. Витік пам’яті є просто канонічним прикладом керування ресурсами. Моя думка полягала в тому, що правильне управління ресурсами взагалі не може бути повністю перевірено.
Дерек Ш

Влучне зауваження. Домовились.
Андрес Ф.

3

Якщо кожен шлях через програму перевіряється, чи гарантує це пошук усіх помилок?

Як уже було сказано, відповідь НІ.

Якщо ні, то чому б і ні?

Крім того, про що йдеться, є помилки, що з’являються на різних рівнях, які неможливо перевірити одиничними тестами. Зазначимо лише кілька:

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

2

Що означає кожен тестуваний шлях?

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

Розглянемо цей метод:

def add(num1, num2)
  foo = "bar"  # useless statement
  $global += 1 # side effect
  num1 + num2  # actual work
end

Якщо ви пишете тест, який стверджує add(1, 2) == 3, інструмент покриття коду скаже вам, що кожен рядок виконується. Але ви насправді нічого не стверджували про глобальний побічний ефект або марне призначення. Ці рядки виконані, але насправді не перевірені.

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

  • Одна мутація може змінити +=на -=. Ця мутація не спричинить збій тесту, тож доведеться, що ваш тест не підтверджує нічого значимого щодо глобального побічного ефекту.
  • Інша мутація може видалити перший рядок. Ця мутація не спричинила б тестовий збій, тому вона доведе, що ваш тест не підтверджує нічого важливого щодо завдання.
  • Ще одна мутація може видалити третій рядок. Це може спричинити збій тесту, що в цьому випадку показує, що ваш тест щось підтверджує про цю лінію.

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

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


0

Ну ... так, насправді, якщо кожен шлях "через" програма тестується. Але це означає, що кожен можливий шлях через увесь простір усіх можливих станів програми може мати, включаючи всі змінні. Навіть для дуже простої статично складеної програми - скажімо, старого кранчера номера Fortran - це неможливо, хоча це можна принаймні уявити: якщо у вас є лише дві цілі змінні, ви в основному маєте справу з усіма можливими способами підключення точок на двовимірна сітка; це насправді дуже схоже на мандрівного продавця. Для n таких змінних ви маєте справу з n -вимірним простором, тому для будь-якої реальної програми завдання є абсолютно нерозбірливим.

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


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

По-перше: вкрай доцільно перейти до декларативної мови. Імперативні мови чомусь завжди були найпопулярнішими, але спосіб поєднання алгоритмів із взаємодіями в реальному світі вкрай важко навіть сказати, що ви маєте на увазі під «правильним».

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

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

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


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

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