Каква е ролята на assert в C++ програми, които имат модулни тестове?

Добавям модулни тестове към някои наследени C++ кодове и се натъквах на много сценарии, при които assert във функция ще се прекъсне по време на изпълнение на модулен тест. Често срещан идиом, на който съм се натъквал, са функции, които приемат аргументи на указателя и незабавно потвърждават, ако аргументът е NULL.

Бих могъл лесно да заобиколя това, като деактивирам твърдения, когато тествам модул. Но започвам да се чудя дали модулните тестове трябва да облекчат нуждата от твърдения по време на изпълнение. Това правилна оценка ли е? Трябва ли модулните тестове да заменят твърденията по време на изпълнение, като се случват по-рано в конвейера (т.е. грешката се улавя в неуспешен тест, вместо когато програмата работи).

От друга страна, не ми харесва добавянето на меки грешки в кода (напр. if (param == NULL) return false;). Утвърждението по време на изпълнение поне улеснява отстраняването на грешки при проблем, в случай че единичен тест е пропуснал грешка.


person lhumongous    schedule 07.04.2010    source източник
comment
Обърнете внимание, че в Windows Boost.Test позволява да се уловят твърдения като изключения, така че изстреляно твърдение обикновено просто се проваля на теста и алтернативно може да бъде уловено, например, да не се провали на теста.   -  person Martin Ba    schedule 22.11.2011


Отговори (10)


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

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

Второ, да кажем, че имате функция Foo, която твърди, че нейните параметри са валидни.
Във вашия модулен тест за Foo можете да се уверите, че предоставяте само валидни параметри, така че смятате, че сте добре.
6 месеца надолу следата, която някой друг разработчик ще извика Foo от някаква нова част от код (която може или не може да има единични тестове) и в този момент ще бъдете много благодарни, че сте поставили тези твърдения там.

person Orion Edwards    schedule 07.04.2010

Ако вашият модулен тестов код е правилен, тогава твърдението е грешка, която единичният тест е разкрил.

Но е много по-вероятно вашият модулен тестов код да нарушава ограниченията на единиците, които тества - вашият модулен тестов код е бъгов!

Коментаторите повдигнаха въпроса, че:

Помислете за единичен тест, който потвърждава, че функцията обработва правилно невалидния вход.

Утвърждението е начинът на програмиста да се справи с невалиден вход чрез прекъсване на програмата. Чрез прекъсване програмата функционира правилно.

Утвържденията са само в компилациите за отстраняване на грешки (те не се компилират, ако макросът NDEBUG е дефиниран) и е важно да се тества дали програмата прави нещо разумно в компилациите на версията. Помислете дали да не стартирате единичния си тест на невалидните параметри при компилации на версията.

Ако искате и двата свята – проверката на твърденията да се задействат в компилациите за отстраняване на грешки – тогава искате вашият сноп за модулни тестове в нишка да улавя тези твърдения. Можете да направите това, като предоставите свой собствен assert.h, вместо да използвате системния; макрос (бихте искали __LINE__ и __FILE__ и ##expr) ще извика функция, която сте написали, която може да хвърли персонализирано AssertionFailed, когато се изпълнява в модулен тестов пакет. Това очевидно не улавя твърдения, компилирани в други двоични файлове, срещу които се свързвате, но не са компилирани от източника с вашия персонализиран asserter. (Бих препоръчал това вместо предоставянето на ваш собствен abort(), но това е друг подход, който може да обмислите, за да постигнете същата цел.)

person Will    schedule 07.04.2010
comment
Не съм гласувал против вас, но мисля, че проблемът е, че валиден модулен тест може да задейства твърдение. Помислете за единичен тест, който потвърждава, че функцията обработва правилно невалидния вход. Утвърждението ще се задейства, защото входът е невалиден, но тестът на модула е много подходящ, тъй като тества този случай. - person chollida; 08.04.2010
comment
Много по-вероятно е твърдението да е разкрило грешка в единичния тест. Не съм гласувал против теб, BTW. - person ; 08.04.2010
comment
Бихте ли разяснили защо смятате, че е по-вероятно модулният тест да нарушава ограничението на кода, който тества? Ако единичен тест просто извиква функция на клас и задейства assert, това не предполага ли, че кодът, който се тества, може да има проблем? - person lhumongous; 08.04.2010
comment
@lhumongous Ако тествате функция за това как се справя с „невалидни параметри“, не можете наистина да се оплачете, че програмистът е поставил assert, за да улови това. Утвържденията се използват за привличане на ранно внимание към компрометирани ограничения. С леко отклонение, моето мнение е, че модулните тестове за тестване на незаконни параметри не са толкова полезни, колкото fuzzers и други системни тестове. Компилирайте с -Wall, сканирайте с lint/coverity, стартирайте програмата си във valgrind/purify, засипете я с asserts и направете безкраен маймунски тест. Така се изпушват буболечките. - person Will; 09.04.2010

Твърденията се улавят, когато вашият код се използва неправилно (нарушаване на неговите ограничения или предварителни условия - използване на библиотека без инициализация, предаване на NULL на функция, която няма да го приеме и т.н.).

Модулните тестове потвърждават, че вашият код прави правилното нещо, стига да се използва правилно.

Твърденията също улавят, когато вашият код е влязъл в състояние, което сте смятали за невъзможно (което, тъй като се смята, че е невъзможно, не може да бъде тествано модулно).

Интересното е, че поне една C++ рамка за тестване на единици (Google Test) поддържа тестове за смърт, които са единични тестове, които проверяват дали вашите твърдения работят правилно (за да можете да знаете, че вашите твърдения вършат работата си за улавяне на невалидни състояния на програмата).

person Josh Kelley    schedule 07.04.2010
comment
Ако дадена функция не приема NULL, не трябва ли да се извиква с препратка вместо с указател? Освен ако не говорим за C. - person Chris Bednarski; 08.04.2010
comment
@KNoodles: char* или const char*; придържане към съществуващ стил на кодиране или стандарти за кодиране; typedef void * HANDLE... - person Josh Kelley; 08.04.2010
comment
Твърденията са за улавяне на вътрешна несъответствие, а не за невалиден вход. Изключенията са за изключителни случаи. Единичните тестове имат за цел да проверят както правилната функционалност на дадена функция, така и правилното обработване на грешки. Единичните тестове, които не предизвикват потенциално лош вход, не осигуряват добро покритие - person Stefan Sullivan; 15.12.2016

Утвържденията на IMO и модулните тестове са доста независими концепции. Никой от тях не може да замени другия.

Утвържденията имат за цел да гарантират, че определени условия/инварианти са винаги валидни по време на жизнения цикъл на програмата. Или по-точно, за да гарантираме, че ако такова условие бъде нарушено, ние ще разберем за него възможно най-скоро, възможно най-близо до основната причина за проблема.

Единичните тестове имат за цел да гарантират, че определени части от кода работят правилно изолирано от останалата част от програмата.

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

person Péter Török    schedule 07.04.2010

Обикновено не би трябвало да можете да спъвате твърденията, тъй като те трябва да улавят „невъзможни ситуации“. Ако дадено твърдение се активира, това трябва да показва грешка.

Едно изключение от това правило е, че много разработчици използват твърдения, за да проверят дали аргументите са валидни. Това е добре, при условие че има и резервно копие за компилации без твърдения:

assert(arg != 0);
if (arg != 0)
    throw std::runtime_error();

По този начин, ако лош спор се случи само при определени условия (т.е. на полето), той пак ще бъде заловен.

Ако кодирате по този начин, можете да изключите твърденията и да напишете отрицателни тестове, за да сте сигурни, че лошите аргументи са уловени.

person R Samuel Klatchko    schedule 07.04.2010
comment
Ако го искате по този начин (аз не го правя), защо просто не дефинирате вашето твърдение да хвърля std::runtime_error(), ако DEBUG не е дефиниран? - person Viktor Sehr; 08.04.2010
comment
@ViktorSehr - защото поведението на assert е стандартизирано и мисля, че би било объркващо да се промени поведението. - person R Samuel Klatchko; 08.04.2010
comment
В този случай не би ли било възможно да се използва обвивка? Или просто извежда грешка по време на изпълнение, независимо от настройките на компилацията? - person UncleBens; 08.04.2010

Тук има две възможности:

1) Поведението на функцията се определя (изрично от нейния собствен интерфейс или от общи правила за проекта), когато входът е нулев. Следователно единичният тест трябва да тества това поведение. Така че имате нужда от манипулатор, за да стартирате процес, който изпълнява тестовия случай, и манипулаторът потвърждава, че кодът е задействал твърдението и е прекъснал, или трябва да се подиграете на assert по някакъв начин.

2) Поведението на функцията не е дефинирано, когато входът е нула. Следователно единичният тест не трябва да преминава в null - тестът също е клиент на кода. Не можете да тествате нещо, ако няма нищо конкретно, което то трябва да прави.

Няма трета опция, "функцията има недефинирано поведение, когато се подава нулев вход, но тестовете преминават нулеви така или иначе, само в случай, че се случи нещо интересно". Така че не виждам как "Мога лесно да заобиколя това, като деактивирам твърденията, когато тествам модул" изобщо помага. Със сигурност единичните тестове ще накарат тестваната функция да дереферира нулев указател, което не е по-добро от прекъсване на assert. Цялата причина, поради която се твърди, е да се спре нещо още по-лошо да се случи.

Във вашия случай може би (1) се прилага в компилациите DEBUG и (2) се прилага в компилациите NDEBUG. Така че може би бихте могли да стартирате тестовете за нулев вход само при компилации за отстраняване на грешки и да ги пропуснете, когато тествате компилацията на версията.

person Steve Jessop    schedule 07.04.2010

Използвам само твърдения, за да проверявам за неща, които "никога не могат да се случат". Ако assert се активира, значи някъде е допусната програмна грешка.

Да кажем, че даден метод приема името на входен файл и единичен тест го захранва с името на несъществуващ файл, за да види дали е хвърлено изключение „файлът не е намерен“. Това не е нещо, което "никога не може да се случи". Възможно е да не намерите файл по време на изпълнение. Не бих използвал assert в метода, за да проверя дали файлът е намерен.

Дължината на низа на аргумента на името на файла обаче никога не трябва да бъде отрицателна. Ако е така, значи някъде има грешка. Така че мога да използвам твърдение, за да кажа, че "тази дължина никога не може да бъде отрицателна". (Това е просто изкуствен пример.)

В случая на вашия въпрос, ако функцията твърди != NULL, или единичният тест е грешен и не трябва да изпраща NULL, защото това никога няма да се случи, или единичният тест е валиден и NULL може евентуално да бъде изпратен, и функцията е грешна и не трябва да твърди != NULL и вместо това трябва да обработи това условие.

person Jim Flood    schedule 07.04.2010

Лично аз не съм склонен да използвам твърдения, тъй като, както сте открили, те често не се справят добре с единичните тестове. Склонен съм да предпочитам да хвърлям изключения в ситуации, в които други често биха използвали твърдения. Тези проверки и изключенията, които се хвърлят при неуспех, присъстват както в компилациите за отстраняване на грешки, така и в компилациите за освобождаване и откривам, че те често улавят неща, които „невъзможно е да се случат“ дори в компилациите за версии (които твърдят, че често не го правят, тъй като те) често се компилира). Намирам, че работи по-добре за мен и означава, че мога да пиша модулни тестове, които очакват изключението да бъде хвърлено при невалиден вход, вместо да очаквам да се задейства твърдение.

Много хора не са съгласни, вижте 1, 2, etc, но не ме интересува. Избягването на твърдения и използването на изключения вместо това работи добре за мен и ми помага да създавам надежден код за клиенти...

person Len Holgate    schedule 07.04.2010

Първо, за да може единичен тест да достигне до assert (или ASSERT или _ASSERT или _ASSERTE при компилации на Windows), единичният тест ще трябва да изпълни тествания код с компилацията за отстраняване на грешки.

Предполагам, че това може лесно да се случи на машината на програмиста. За нашите нощни компилации, ние изпълняваме само модулните тестове в конфигурацията на изданието, така че няма притеснения относно твърденията там.

Второ, може да се приеме нормативният подход с твърдения --

Утвържденията имат за цел да гарантират, че определени условия/инварианти са винаги валидни по време на жизнения цикъл на програмата. Или по-точно, за да гарантираме, че ако такова условие бъде нарушено, ние ще разберем за него възможно най-скоро, възможно най-близо до основната причина за проблема.

В този случай нито един модулен тест не трябва да повдига твърдение, тъй като извикването на кода по начин, по който се повдига твърдение, не трябва да е възможно.

или човек може да приеме „прагматичния“ подход с твърдения:

Нека разработчиците разпръснат ASSERT навсякъде за сценарии „не правете това“ и „неприложени“. (И можем да спорим цял ден дали това е грешно™ или правилно™, но това няма да доведе до предоставяне на функциите.)

Ако възприемете прагматичния подход, тогава единичен тест, който удря твърдение, означава, че единичният тест е извикал кода по начин, който не се напълно поддържа от кода. Това просто може да означава, че кодът "не прави нищо" в компилация на версия или може да означава, че кодът се срива в компилация на версия или може да означава, че кодът прави "нещо интересно".

Ето опциите, за които е известно, че използвам:

  • Ако твърдението е придружено от допълнителна проверка, за да направи извикването „безобидно“, направете теста на модула тест за твърдението (в отстраняване на грешки) и за „безвредно“ условие при освобождаване.
  • За сривове или „нещо интересно“ или няма единичен тест, който да има смисъл, или можете да направите единичен тест „само за отстраняване на грешки“, който тества дали наистина получавате твърдение (макар че не съм толкова сигурен, че е полезно).
person Martin Ba    schedule 22.11.2011

1- Assert е добър начин за изясняване на инварианти (инварианти на цикъл) по време на разработването на алгоритми. Може да бъде полезно за "четимост", както и за отстраняване на грешки. Езикът Айфел от Бертран Майер има ключова дума invariant. На други езици assert може да се използва за тази цел.

2- Assert може да се използва и при други обстоятелства като междинно решение по време на разработката и да се премахва постепенно, докато кодът се завършва. Тоест като TODO елементи. Някои от asserts (тези, които не са в елемент #1 по-горе) трябва да бъдат заменени с обработка на изключения и т.н. Много по-лесно е да ги забележите, ако всички такива проверки се показват като твърдения.

3- Понякога го използвам, за да изясня типа входове в езици, които нямат система за проверка на типа (Python и JavaScript), в определени контексти (напр. при разработване на нови алгоритми). Не съм сигурен дали е препоръчителна практика. Подобно на елемент #1, става въпрос за увеличаване на четливостта на програмата.

person Sohail Si    schedule 07.06.2016