Искоришћавање неодређености у тестовима

Source: https://martinfowler.com/articles/nonDeterminism.html

Аутоматски регресијски пакет може играти кључну улогу на софтверском пројекту, вредан како за смањење недостатака у производњи, тако и за суштину еволуционог дизајна. У разговору са развојним тимовима често сам чуо о проблему недетерминистичких тестова – тестова који понекад пролазе и понекад не успевају. Леви неконтролисани, недетерминистички тестови могу у потпуности уништити вриједност аутоматизованог пакета регресије. У овом чланку сам описао како се бавити не-детерминистичким тестовима. У почетку карантин помаже да се смањи њихова оштећења на другим тестовима, али још увек их морате поправити ускоро. Стога говорим о третманима за уобичајене узроке не-детерминизма: недостатак изолације, асинхроно понашање, удаљени сервиси, време и цурење ресурса.


14. април 2011

Мартин Фовлер (Martin Fowler)

Уживао сам у гледању компаније ThoughtWorks у многим тешким апликацијама у предузећу, што је довело до успешних испорука многим клијентима који су ријетко видели успјех. Наша искуства су била одлична демонстрација да се агилним методама, дубоко контроверзним и неповереним када смо написали манифесто пре десет година, могли успешно да се искористе.

Постоји много укуса агилног развоја тамо, али у томе што радимо тамо је централна улога за аутоматско тестирање. Аутоматско тестирање је од почетка био основни приступ екстремном програмирању и та филозофија је била највећа инспирација за наш агилни рад. Тако смо стекли пуно искуства у коришћењу аутоматизованих тестова као кључног дела развоја софтвера.

Аутоматско тестирање може изгледати лако када је презентирано у уџбенику. И заправо су основне идеје стварно прилично једноставне. Али у штедљивом печењу пројекта испоруке појављују се суђења која често не добијају велику пажњу у текстовима. Као што сам добро знао, аутори имају навику да се скупљају кроз многе детаље како би добили кључну тачку. У мојим разговорима са нашим тимовима за испоруку, један понављајући проблем у којем смо ушли су тестови који су постали непоуздани, тако непоуздани да људи не обраћају пажњу на то да ли они пролазе или не успевају. Примарни узрок ове непоузданости јесте да су неки тестови постали недетерминистички.

Тест је недетерминистичан када понекад пролази и понекад пропада, без икаквих приметних промена у коду, тестовима или околини. Такви тестови не успевају, онда их поново покренете и они прођу. Неуспех тестирања за такве тестове је наизглед случајан.

Недодирљивост може смањити сваку врсту теста, али је посебно склона да утиче на тестове са широким опсегом, као што су прихватање или функционални тестови.


Зашто не детерминистички тестови су проблем

Недетеретистички тестови имају два проблема, прво су бескорисни, друго, они су вирулентна инфекција која може потпуно уништити читав тест суите. Као резултат тога, они морају да се реше што пре, пре него што се угрози цјелокупни гасовод.

Почећу са ширењем на њиховом бескорисности. Примарна предност имати аутоматске тестове је да они обезбеђују механизам за откривање буба делујући као регресије тестови [1]. Када регресија теста иде црвена, знате да имам тренутни проблем, често због буба се увукла у систем без тебе реализације.

Има такву грешку детектор има огромне предности. Најочигледније то значи да можете пронаћи и поправити грешке тек након што су уведене. Не само да ли то ти топле дивљаке, јер си убио бубе брзо, такође олакшава њихово уклањање, јер знате да је грешка ушао са последњег скуп измена које су свеже у глави. Као резултат знате где да тражите грешке, што је више од половине битку у то скуасхинг.

Други ниво корист је у томе што сте стекли поверење у својој буг детектора, стичете храбрости да се велике промене, знајући да када се зезаш, буг детектор ће отићи и можете поправити грешку брзо. [2] Без  ове екипе су уплашени да би промене код потребама, како би се одржавати чистим, што доводи до трули базе код и паду брзине развоја.

Проблем са не-детерминистичких тестова је да када оду црвена, немаш појма да ли је њено због буба, или само део не-детерминистички понашања. Обично са овим тестовима не-детерминистички неуспех је релативно честа, тако да на крају одбацујући рамена када су ови тестови иду црвено. Када почнете да игноришу неуспех регресија тест, онда то тест је бескорисно и могла би да га бацим. [3]

Заиста, заиста би требало да одбаците недетерминистички тест, јер ако немате инфективни квалитет. Ако имате комплет од 100 тестова са 10 недеретистичких тестова у њима, онда ће тај пакет често пропасти. У почетку људи ће погледати извјештај о грешци и приметити да су неуспјехови у недеретистичким тестовима, али ускоро ће изгубити дисциплину. Када се та дисциплина изгуби, онда ће се неуспјех у здравим детерминистичким тестовима игнорисати. У том тренутку изгубили сте целу игру и могли бисте се ослободити свих тестова.


Карантин

Мој главни циљ у овом чланку је да представим уобичајене случајеве недетерминистичких тестова и како да елиминишем не-детерминизму. Али пре него што стигнем, нудим један кључни савет: огранићите своје не-детерминистичке тестове. Ако имате недетерминистичке тестове, држите их у другој тестној групи за ваше здравије тестове. На тај начин ћете наставити да пазите на оно што се дешава са вашим здравим тестовима и добијете добре повратне информације од њих.

Поставите било који не-детерминистички тест у карантинском подручју. (Али брзо тестирајте карантиниране тестове.)

Затим питање је шта урадити са тестираним апартманима за карантину. Оне су бескорисне као тестови регресије, али имају будућност као радне предмете за чишћење. Не бисте требали напустити такве тестове, јер сви тестови које имате у карантину не помажу вам у покривању регресије.

Опасност овде је да се тестови и даље бацају у карантин и заборављају, што значи да систем за детекцију грешке еродира. Као резултат, вреди имати механизам који осигурава да тестови не буду дуго у карантину. Наишао сам на различите начине да то урадим. Један је једноставна нумеричка граница: нпр. дозволити само 8 тестова у карантину. Када погодите границу, морате провести вријеме како бисте обрисали све тестове. Ово има предност приликом чишћења вашег тестирања ако то желите. Још један пут је поставити временско ограничење колико дуго тест може бити у карантину, као што је не више од недељу дана.

Општи приступ са карантину је да предузме карантина тестове из главног распоређивања цевовода, тако да и даље имати свој редован процес израде. Међутим добар тим може бити агресивнији. Наш  Мингле  тим ставља свој карантин апартман у плану распоређивања један корак након својих здравих тестовима. На тај начин се може добити повратну информацију од здравих тестовима, али је такође приморан да осигура да се сортира се на карантина тестове брзо. [4]


Недостатак исолатион

Да би тестови могли да буду поуздано покретани, морате имати јасну контролу над окружењем у којем воде, тако да на почетку теста имате добро познато стање. Ако један тест ствара неке податке у бази података и оставља да лежи около, може покварити покрет другог теста који се може ослонити на различито стање базе података.

Зато сматрам да је веома важно фокусирати се на држање тестова изолованих. Правилно изоловани тестови могу се покренути у било којој секвенци. Како долазите до већег оперативног опсега функционалних тестова, постаје све теже да се тестови изолују. Када пратите не-детерминистички, недостатак изолације је чест и узнемирујући разлог.

Постоји неколико начина за добијање изолације – било да увек обнављате почетно стање из нуле или осигурате да сваки тест прочисти исправно после себе. Уопштено, ја волим прву, јер је често лакше – а посебно је лакше пронаћи извор проблема. Ако тест не успе јер није исправно направио почетно стање, онда је лако видети који тест садржи грешку. Међутим, са чишћењем, један тест ће садржати грешку, али ће други тест пропасти – тешко је пронаћи стварни проблем.

Држите тестове изоловане једни од других, тако да извршење једног теста неће утицати на друге.

Полазећи од бланк државе је обично лако са јединице тестовима, али може бити много теже са функционалним тестовима  [5] – посебно  ако имате много података у бази података који треба да буде тамо. Обнова базу података сваки пут може додати пуно времена да тест ради, тако да се залаже за прелазак на чишћења стратегије. [6]

Један трик који је згодно када користите базе података, је да спроведе своје тестове у трансакције, а затим враћање трансакцију на крају теста. На тај начин менаџер трансакција чисти за вас, смањење шансе грешака [7].

Још један приступ је направити појединачну изградњу неизменљивог почетног уређаја пре него што покренете групу тестова. Затим проверите да ли тестови не мењају почетно стање (или ако то учине, они обнављају промене у сузбијању). Ова тактика је склонија грешкама него реконструкција уређаја за сваки тест, али може бити вредно ако је потребно предуго да сваки пут направи уређај.

Иако су базе података чести узрок проблема са изолацијом, постоји и доста пута које можете добити и у меморији. Посебно треба бити упознат са статичким подацима и синглетонима. Добар пример за овакав проблем је контекстуално окружење, као што је тренутно пријављени корисник.

Ако имате експлицитно срушење у тесту, будите пажљиви у погледу изузетака који се јављају током срушења. Ако се то деси, тест може проћи, али проузроковати неуспјех изолације за накнадне тестове. Дакле, уверите се у то да ако имате проблем у сузбијању, то чује гласан бука.

Неки људи више воле да стављају мање пажње на изолацију и више на дефинисање јасних зависности на тестовима силе да се покрећу у одређеном редоследу. Више волим изолацију јер вам даје више флексибилности у покретању подскупова тестова и паралелизирања тестова.


Асинхрони понашање

Асинхронија је благонаклоница која вам омогућава да држите програм који одговара док узимате дугорочне задатке. Позиви Ајак-а омогућавају прегледачу да остану одговарајући док се враћају на сервер за више података, асинхрони порука омогућава процесу сервера да комуницира са другим системом, а да није везан за њихову лагану латенцију.

Али у тестирању, асинхронија може бити проклетство. Уобичајена грешка је да баците у спавање:

        //pseudo-code
        makeAsyncCall;
        sleep(aWhile);
        readResponse;

Ово може да вас угризе на два начина. Прво ћете желети да поставите време за спавање довољно дуго да даје довољно времена да добијете одговор. Али то значи да ћете пуно времена провести идите на чекању на одговор, тиме ћете успорити тестове. Други угриз је да, колико год дуго спавате, понекад то неће бити довољно. Постојаће неке промене у окружењу које ће вам узроковати превазилажење сна – и добићете лажни неуспјех. Као резултат тога, снажно вас позивам да никада не користите голим спавањем овако.

Постоје две тактика које можете учинити за тестирање је асинхрони одговор. Први је за асинхрони сервис да повратни позив који се може назвати када завршите. Ово је најбоља, јер то значи да никада неће морати да чекају дуже него што је потребно да  [8]. Највећи проблем са овим је да окружење треба да буде у стању да уради ово, а онда пружалац услуга мора да то уради. Ово је једна од предности које има развојни тим интегрисан са тестирањем – ако могу да обезбеде повратни позив онда хоће.

Никада не користите голе слеепове да бисте чекали асинхрони одговор: користите повратни позив или гласање.

Друга опција је анкетирање на одговору. Ово је више него само гледање једном, али редовно гледајући, нешто овако

        //pseudo-code
        makeAsyncCall
        startTime = Time.now;
        while(! responseReceived) {
          if (Time.now - startTime > waitLimit) 
            throw new TestTimeoutException;
          sleep (pollingInterval);
        }
        readResponse

Суштина овог приступа је да можете подесити pollingInterval на прилично мале вредности, а знамо да је то максимална количина мртвог времена ћете изгубити чека одговор. То значи да можете да подесите  waitLimit веома висок, што смањује шансу да га ударају, осим ако се нешто озбиљно није пошло наопако. [9]

Уверите се да користите јасну класу изузетака која указује да је ово неуспешна пробна провјера. Ово ће помоћи да се јасно покаже шта се десило ако се то деси, и можда допустите напреднијим тестним упртама да узму у обзир ове информације на свом екрану.

Вредности времена, а нарочито  waitLimit, никада не би требало да буде буквално вредности. Уверите се да су увек вредности које се лако може подесити у ринфузи, или помоћу константе или поставити кроз рунтиме окружење. На тај начин, ако желите да их подесите (и ви ћете) можете их све подесити брзо.

Све ово савет је згодан за асинц позиве где се очекују одговор од провајдера, али шта је са онима у којима нема одговора. То су позиви у којима се позивају команду на нешто и очекивати да се то догоди без икаквог признања. Ово је најтежих случај, јер можете тестирати вашу очекује одговор, али нема ништа да се уради да се открије квар осим временско-оут. Ако понуђач је нешто правите ви то може носити осигуравајући провајдера реализује неки начин указује на то да се то ради – у основи неки облик цаллбацк. Чак и ако само код тестирања га користи, вредело је – иако често ћете наћи овакав функционалности је корисна за друге сврхе превише [10]. Ако понуђач је посао туђи, можете пробати убедити, али иначе може да се заглави. Иако је ово такође случај када се користе  Test Doubles за удаљене услуге вреди (што ћу разговарати више у наредном поглављу).

Ако имате неуспех у нечем асинхроном, тако да уопште не одговара, онда ћете увек чекати временска ограничења и ваш тестни пакет ће потрајати неуспешно. Да би се супротставили овоме, добра је идеја да користите тест дима како бисте провјерили да ли асинхрони сервис уопће одговара и одмах заустави тестирање ако није.

Можете такође често нежељених корак у асинхроније потпуно. Герард Месзарос је  понизан образац објекта  каже да кад год имате неку логику која је у хард-на-тест окружењу, требало би изоловати логику која вам је потребна за тестирање из тог окружења. У овом случају то значи ставити већи део логике потребно је да тестирате на месту где можете тестирати синхроно. Асинхрони понашање треба да буде минимална (скроман) могуће, тако да не треба толико тестирање тога.

Књига Герард Месзароса,  xUnit тест паттернс, садржи пуно добрих модела за израду тестова.


Ремоте сервицес

Понекад ме питају да ли ThoughtWorks ради на било који интеграцијски рад, што ми је нешто забавно јер скоро да ниједан пројекат који радимо то не укључује фер интеграцију. По својој природи, апликације за предузеће подразумевају велику комбинацију података из различитих система. Ове системе одржавају други тимови који раде на сопственим распоредима, тимовима који често користе веома различиту софтверску филозофију нашег агилног приступа који је снажно тестиран на тестирање.

Тестирање са оваквим даљинским системом доноси бројне проблеме, а недодирљивост је високо на листи. Често удаљени системи немају тестни систем који можемо позвати, што значи да се удари у систем уживо. Ако постоји систем за тестирање, можда није довољно стабилан да обезбеди детерминистичке одговоре.

У овој ситуацији је важно да се обезбеди детерминизам, тако да је време да се постигне за  Test Doubles  – компонента која личи на даљинском сервиса, али је заправо само Претенд верзија која имитира понашање на даљинском система.Двоструки потребе да се подеси тако да даје праву врсту одговора у интеракцији са нашим системом, али на неки начин смо контролу. На овај начин можемо обезбедити детерминизам.

Коришћење двојника има мана, поготово када се тестирање у широком обиму. Како можемо бити сигурни да су двоструки понаша на исти начин на који даљински систем ради? Можемо се изборили са овим поново користећи тестове, облик теста који ја називам  Уговор тестови. Они воде исти интеракцију са даљинским системом и двострука, и проверите да је две утакмице. У овом случају “утакмици” не може да значи долази са истим резултатом (због не-детерминисмс), али резултати које деле исту основну структуру. Интеграција Уговор Тестови морају да се ради често, али не и део јастука цевовода нашег система. Периодични рад на основу стопе промене даљинског система је обично најбоље.

За писање ове врсте тестова дублу, ја сам велики фан  Селф инитиализинг фалсификати  – јер су веома једноставни за управљање.

Неки људи су чврсто против коришћења  Test Doubles  у функционалним тестовима, верујући да морате тестирати са стварном везом, како би се осигурала енд-то-енд понашање. Док саосећам са њиховим аргументима, аутоматске тестови су бескорисни ако су недетерминистички. Тако да било предност коју добија од разговора са стварном система преплављен са потребом да се искорени не-детерминизам [11].


Време

Мало је ствари које су више не-детерминистички него позив на системском часовнику. Сваки пут када га зову, добијате нову резултат, и све тестове који зависе од њега тако може променити. Питајте за све тодос због у наредних сат времена, а редовно се другачији одговор [12].

Најважнија ствар је да се осигура да увек завршити системски сат са рутине које могу да буду замењени са засеје вредност за тестирање. Сат Стуб се може подесити на одређено време и замрзнути у то време, омогућавајући ваши тестови да има потпуну контролу над својим покретима. На тај начин можете да синхронизујете тест податке са вредностима у засеје сат. [13] [14]

Једна ствар за ово гледање јесте да ли ће на крају ваши тестови можда почети да имају проблеме јер је сувише стара, а ви имате конфликте са другим факторима заснованим на времену у вашој апликацији. У овом случају можете премјестити податке и ваше семе сатова на нове вриједности. Када ово урадите, уверите се да је то једина ствар коју радите. На тај начин можете бити сигурни да су сви тестови који не успевају због временског кретања у тестним подацима.

Увек обришите системски сат, тако да се лако може замијенити за тестирање.

Још једна област у којој време може да буде проблем је када се ослањате на друге понашања из такта. Једном сам видео систем који генерисани случајне кључеве на основу сата вредностима. Овај систем је почео неуспеха када је премештен у бржем машину која би могла издвојити више личних карата у једном Цлоцк Тицк. [15]

Чуо сам много проблема због директне позиве упућене системски сат да бих се залажу за проналажење начина за коришћење анализе код открити било какве директне позиве системски сат и пали на градити тамо. Чак и једноставна регек-провера може уштедјети фрустрирајуће отклањање грешака седницу након позива на један рано.


Ресоурце цурење

Ако ваша апликација има неку врсту извора ресурса, то ће довести до неуспелих тестова, јер то је само који тест проузрокује да цурење извора прелази границу која добије отказ. Овај случај је неугодан јер сваки тест може прекинути због овог проблема. Ако није случај да један тест није недетерминистичан, онда је цурење извора добар кандидат за истраживање.

Због истицања ресурса, мислим на сваки ресурс који апликација мора да управља узимањем и пуштањем у рад. У окружењима без меморије, очигледан пример је меморија. Управљање меморијом учинило је много да уклони овај проблем, али још увијек треба управљати другим ресурсима, као што су везе базе података.

Обично најбољи начин за решавање оваквих ресурса је кроз Ресоурце Поол. Ако ово урадите онда је добра тактика да конфигуришете базен величине од 1 и направите бацање изузетка ако добије захтев за ресурс када га нема. На тај начин први тест који ће тражити ресурс након цурења неће успети – што олакшава проналажење теста проблема.

Ова идеја о ограничавању величине базена ресурса је о повећању ограничења да би се погрешке појавиле у тестовима. Ово је добро јер желимо да се грешке показују у тестовима, тако да их можемо поправити пре него што се манифестују у производњи. Овај принцип се може користити и на друге начине. Једна прича коју сам чуо је био систем који је генерирао насумично назване привремене датотеке, није их исправно исправио и срушио се приликом судара. Врло тешко је пронаћи ову врсту грешке, али један начин да се манифестује јесте да заглавите рандомизатора за тестирање тако да увијек врати исту вриједност. На тај начин можете брже да површите проблем.


Fusnote

1: Да, знам да многи заговорници TDD-а сматрају да је основна врлина тестирања начин на који захтијева дизајн и дизајн. Слажем се да је ово велика предност, али сматрам да је регресиони пакет највећа предност коју нам дају аутоматизовани тестови. Чак и без TDD тестова вриједи трошак за то.

2: Понекад, наравно, неуспјех теста је због промјене у ономе што код треба да уради, али тест није ажуриран да одражава ново понашање. Ово је у суштини грешка у тестовима, али је једнако лако поправити ако је одмах ухваћен.

3: Постоји корисна улога за недетерминистичке тестове. Испитивања која се замеђују из рандомизатора могу помоћи у лову на ивице. Тестови перформанси увек ће се вратити различитим вредностима. Међутим, овакви тестови се сасвим разликују од аутоматских тестова регресије, који су мој фокус овде.

4: Ово добро функционише за тим Минглеа јер су довољно вјешти да брзо пронађу и поправи не-детерминистичке тестове и довољно дисциплинују како би осигурали брзо. Ако вам је изградња остала сломљена дуго због ваших карантинских тестова, нећете изгубити вредност континуиране интеграције. Дакле, за већину тимова саветујем одржавање тестова из карантина из главног цјевовода.

5: Овде нема дефинитивних тврдих дефиниција, али ја користим терминологију раног екстремног програмирања коришћења “тест јединице” што значи нешто фино-зрно и “функционално тестирање” као тест који је крајњи до крајњи и функција повезана.

6: Један трик је креирање иницијалне базе података и копирање помоћу команди датотечног система пре него што га отворите за сваки пробни рад. Копије датотечног система су често брже од учитавања података помоћу команди базе података.

7: Наравно да овај трик ради само када можете провести тест без обављања било каквих трансакција.

8: Иако вам је потребан временски распоред у случају да никада не добијете одговор – и тај временски период је подложан истој опасности када се преселите у друго окружење. На срећу можете поставити тај временски распоред да буде прилично висок, што минимизира шансе да вас гризе.

9: У том случају, међутим тестови ће се одвијати веома споро. Можда бисте желели да размотрите прекидање целокупног тест пакета ако достигнете ограничење за чекање.

10: Ако се ваше асинхроно понашање покрене из УИ-а, често је добар избор УИ-а да је неки индикатор који приказује асинхрону операцију у току. Имајући ово као део УИ-а, такође помаже у тестирању јер су кукице које су потребне за заустављање овог индикатора могу бити исте кукице као и откривање када се напредује тестна логика.

11: Постоје и друге предности кориштења двоструког теста у овим околностима, чак и ако је даљински систем детерминистичан. Често вријеме одзива је сувише споро за коришћење удаљеног система. Ако можете разговарати само са системом уживо, онда ваши тестови могу генерисати значајне и неприлагођене оптерећења на том систему.

12: Могао би се ресеед вашу датасторе за сваки тест на основу тренутног времена. Али то је пуно посла, а испуњено је могућим грешкама у времену.

13: У овом случају пуњење сата је уобичајен начин да се прекине изолација, сваки тест који га користи треба да обезбеди да се правилно иницијализује.

14: Једна од мојих колега воли да примени пробну вожњу непосредно пре и после поноћи како би ухватила тестове који користе тренутно време и претпостављају да је исти дан на сат или два касније. Ово је посебно добро у времену као последњи дан у месецу.

15: Иако, наравно, ово није увијек не-детерминистичка грешка, већ једна због промене у окружењу. У зависности од тога колико је убрзано кретање сат времена на доделу ид-а, то може довести до недетерминистичког понашања.

Priznanja

Као и обично, морам захвалити многим колегама ThoughtWorks-а за размјену искустава и тиме обезбедити материјал за састављање овог чланка.

Мицхаел Диетз, Данило Сато, Бадринатх Јанакираман, Матт Саваге, Кристан Вингрис и Брандон Биерс прочитали су чланак и дали ми неке додатне информације.

Ед Сикес ме је подсетио на приступ кориштења копије датотеке датотеке у датотеци базе података за креирање иницијалних база података за сваки тест.

Значајно ревизија

14. април 2011: прво објављено

24. марта 2011: нацрт објављен за преглед у ThoughtWorks-у

16. фебруар 2011: започели чланак