Unit тестове #1

Може би това, което най-често ми се налага да обяснявам на приятели/колеги е какво представляват unit тестовете и защо за бога си усложняваме живота като ги пишем. Наистина, когато за първи път започнах да работя и големия лош TL ми каза да пиша „тестове за кода на проекта“, бая се оцъклих. Дълго време гледах тъпо JUnit апито и се чудих какво по дяволите се иска от мен да направя с него и по какъв начин това ще помогне на проекта. Но както казват, мъдростта идва с времето – след месец-два не просто му бях хванал цаката, но вече въобще не мога да си представя разработката на сериозно приложение от екип, без да се пишат тестове. И понеже това ми е любима тема, ще се опитам да драсна едно „цикълче“ от статийки, които да разясняват идеята – или поне как аз я разбирам.

Та, две приказки за това какво представляват на unit тестовете. Нали се сещате как всеки път след като добавите няколко реда в програмата си я разцъквате, за да видите дали всичко е наред? Идеята е да напишете код, който да прави това вместо вас. Този тест използва вашите функции/класове точно както вие бихте ги ползвали, само дето вместо да рендира интерфейс се опитва да провери дали кодът работи както трябва. Самият той трябва да може да се стартира без никакви параметри, да не очаква никакъв вход от потребител и накрая само да може да каже „Тестовете минаха успешно“ или „Тестовете не минаха успешно“. Ще се опитам да дам пример:

Туко що са ви наели на работа в Shmoogle – млада, но амбиционзна фирма, която иска да преобрази интернет пространството с уникалните си способности да търси и намира разни неща из него. Вас са ви настанили в екипа, който се занимава с текстов анализ на намерените интересности из нета. Шефът ви е много притеснен от това, че в интернет се използват твърде много съкращения, а търсачката ви все още няма механизъм да го поеме. Затова, отговорната ви задача е следната – трябва да напишете модул, който да открива съкращения. Т.е. трябва да откривате, че в изречението „Today the Honorable Order of Macintosh Operators accepted a new member“ има съкращението H.O.M.O. Пукате с пръсти, стартирате vim-а и започвате да пишете на Python.

Първия проблем с който се сблъсквате е, че думи като of и the не трябва да влизат в съкращенията. За целта първо ще ги изтривате от текста, а после ще търсите най-дългите поредици от думи които започват с главни букви. И понеже си спомняте с умиление студенстките си години, когато сте писали на perl и никой нищо не ви е разбирал, почвате яко с регулярни изрази. Излизате със следния код:

import re

def abbr(text):
    """Изважда съкращението от някакво име"""
    capitalWords = re.findall(‘[A-Z]\w+’, text)
    return .join([word[0] for word in capitalWords])

def findAbbr(text):
    """Намира първото съкращение в даден текст и го връща"""
    text = re.sub(‘of|the’, , text)

    match = re.search("([A-Z]\w+\s+)+", text)
    if match:
        return abbr(text[match.start():match.end()])
    else:
        return None

След това обаче си спомняте, как някой ви беше говорил за unit test-ове. Викате си „хъм, що пък не – и без това ще впечатля team leader-а си“. В резултат, съчинявате следния код:

import unittest

class AbbreviationTests(unittest.TestCase):
    def testAbbreviations(self):
        self.assertEquals("HOMO", abbr("Honorable Order of Macintosh Operators"))
        self.assertEquals("DIY", abbr("Do It Yourself"))
        self.assertEquals("GNU", abbr("GNU’s Not Unix"))

    def testFindAbbr(self):
        text = """The leader of the Honorable Order of Macintosh Operators to
        Bill Gates: "Get a Mac or die!" """


        found = findAbbr(text)
        self.assertEquals("HOMO", found")

А сега лирическо отклонение за малко теория. Най-разпространения модел на unit тестове е следния – всеки представлява клас, който има тестови методи. Ролята на тези методи е да определят дали някаква част от кода ви работи коректно или не. Името на всеки от тях започва с test, а самия метод не приема никакви аргументи и не връща резултати. Фреймуърка който ползвате е сравнително умен и открива тези класове, след което изпълнява всеки от методите и съответно ви генерира някакви резултати дали всичко е било успешно.

Assertions (или асертации, както съвсем безвкусно ще им викам отсега нататък) са изрази, които се четат така – „Това условие трябва да е изпълнено. Ако не е така, значи нещо в кода не е наред“. Съответно, self.assertEquals(a, b) се чете като „Ако a и b не са еднакви, значи кода нещо не работи“. Всеки тестов метод съдържа поне една асертация. Смята се, че той минава успешно, ако всички асертации минат успешно. В противен случай, теста fail-ва.

Та, въоражени с това познание вие пускате вашия unit тест. Резултата е следното:

.F
======================================================================
FAIL: testFindAbbr (__main__.AbbreviationTests)
–––––––––––––––––––––––-
Traceback (most recent call last):
  File "<stdin>", line 29, in testFindAbbr
AssertionError: ‘HOMO’ != ‘T’

–––––––––––––––––––––––-
Ran 2 tests in 0.001s

Почесвате се тъпо по-главата, след което възкликвата „Аха! Логично!“. Като си експериментирахте с „bla bla bla Honorable Order of Macintosh Operators“ нямаше проблем, ама в тоза изречение се дъним. Очевидно „The“ няма да се съкрати като T. Бе, нека направим съкращенията да изискват поне две думи. Във findAbbr променяте регулярния израз да изглежда така:

:python
match = re.search("([A-Z]\w+\s+){2,}", text)

Сега се усмихвате широко и пускате теста.

..
–––––––––––––––––––––––-
Ran 2 tests in 0.003s

OK

Това вече е друго! Сега отивате в съседната стая и викате лошия брадат TL. Две минути той се блещи, мрънка си под носа и хъмка, след които се обръща къв вас и казва „Сакън! Аматьор! Не знаеш ли че тия регулярни изрази са много бавни?! А?! Тъп perl-аджия. Я веднага ги сменяй с некви for-ове и if-ове, че ще береме ядове“. Вие скръцвате със зъби, въздържате се от идеята да го нахраните с клавиатурта и започвате да му се обяснявате. Евентуално стигате до консенсус, че регулярния израз във findAbbr е приемлив, ако го компилирате в аванс, щото пък иначе кода много трудно ще се чете. Правите втори опит: :python import re

def _isCapital(word):
    return ‘A’ <= word[0] <= ‘Z’

def abbr(text):
    return “.join([word[0] for word in text.split(‘ ‘) if _isCapital(word)])

_abbrRegex = re.compile("([A-Z]\w+\s+){2,}")

def findAbbr(text):
    text = re.sub(‘of|the’, “, text)

    match = _abbrRegex.search(text)
    if match:
        return abbr(text[match.start():match.end()])
    else:
        return None

Пускате теста, обаче не минава. word[0] не важи, когато имате празен стринг. Не сте сигурни отде се взема, ама няма голямо значение – това е случай който трябва да се покрие.

def _isCapital(word):
    return ‘A’ <= word[0] <= ‘Z’ if len(word) > 0 else False

Този път теста минава. Вие щастливо го пращате в SVN-а и продължавате да си кодите успешно. След няколко месеца ви писва от тъпия тийм лидър и отивате в друга фирма. На програмиста който заема вашето място му се налага да разшири леко функционалността – например да поддържа съкращения, които са на езици различни от английски. Очевидно той ще трябва да пипа вашия код – и то не само _isCapital, но и регулярния израз. Каквото и да е положението, след като направи своите промени, той може да пусне вашия тест и да види дали не е прецакал кода ви. А може и да го разшири с тестове за други езици.

Надявам се с този опит да успях да ви покажа какво представлява един unit тест и да загатна част от причините да се пишат такива работи. Сега ще се опитам да наблегна на облагите от цялата тая работа. Процеса е следния – когато имате да променяте нещо, вие си пишете кода и тестовете за него. След като кода ви кефи достатъчно, а теста минава, пускате всички тестове написани по проекта досега. Ако те минават – чудесно, може да пращате в SVN-а, да си починете малко браузейки FlashBG и след това да продължавате с работата. Но ако тестовете не минават – имате проблем. Ще се поработите още 15-20 минути и ще ги фикснете. Но ако нямахте тестове написани за проекта? Като нищо проблема можеше да стигне до release версия и да го разберете по телефона от разгневен клиент, чиято база данни се е съсипала, защото не сте предвидили нещо.

А това е проблем, в който много често ще се натъквате в по-голям проект – за да добавите нова функционалност в модул B, трябва да промените това онова в някакъв модул A от който B зависи. Но модул C, който въобще не ви е в главата има някакви очаквания от модул A и с вашите промени може да го счупите. Може да промените леко значението на върната стойност или да забраните null като стойност на параметър, а C да предава именно това. Тестовете ви предпазват от такива фалове – понеже има код, който се уверява в това, че модул C работи като хората, вие може да уловите грешката от рано и да я поправите, преди да нанесе някакви по-сериозни щети. Те не просто ще ви спестят часове досадно цъкане, но и винаги може да ги ползвате като едно голяма CRC проверка дали кода ви е стабилен или имате някакъв проблем в него. И не само това – когато видите съобщението за 175 успешно изпълнени теста, вие сте много по-уверени във вашия софтуер, шефа е много по-уверен във вашите умения, а клиента е много по-уверен в услугите на фирмата.

Искрено се надявам да разбрахте какво и защо представляват unit тестовете от този пост. Разбира се, има още много което може да се каже по въпроса – на човек му отнема доста известно време, докато схване за какво си заслужава да тества и кое е губене на време. Има е една много приятна практика – Test Driven Development – с която може да извлечете още повече ползи от тестовете които пишете. Надявам се да спиша няколко реда за тях по-натам. А ако тази статия ви е била полезна, имате какво да добавите/корегирате или просто искате да поощрите некадърните ми творчески опити, то моля пуснете едно коментарче. Въобще няма да ви се разсърдя 😉

5 thoughts on “Unit тестове #1

  1. Unit тестовете наистина са полезно допълнение към ‘живота’ на едно приложение и го съпътстват при промените на модулите, от които то е изградено. Не случайно те са основно занимание и на QAE пичовете в екипа, ако фирмата може да си позволи наемането на отделни такива. Ако ли не, разработчиците се заемат с тази работа.

    Освен с JUnit и ръчното писане на тестове, съществува и още един вид Automation testing tools за тестване на приложенията. Те биват главно 2 вида: -Script and test /подобни на JUnit, дори включващи го/ -Record and replay

    Първите са вече познати от горната статия (като малко предимство е разкършването им, някои допълнителни опции, ITE – Integrated Testing Environment и др), а вторите позволяват създаване на сценарии по запис на действията на ползвателя. По този начин се стартира апликацията в режим Record, log- ват се действията на мишката и клавиатурата и после се възпроизвеждат, при което се следи изходния поток за exception- и или съобщения, а през това време се стартира теста. Логовете могат да се export- ват като чист текст, HTML или XML /някои позволяват и ползване на log4J/. Различните tools предлагат различна ефективност – работа през конзола или GUI ITE, обръщане към скриптове/junits или работа директно с events и прочие.

    Полезни tool- чета за такъв тестинг са Marathon, Jacareto, Abbot /open source/ и други /комерсиални/

  2. Добро въведение в Unit тестовете! Регулярните изрази не са ми силата, но пак се разбира за какво става дума и дано въведеш нови поклонници в лоното на Test църквата 🙂

  3. Unit тестовете ги пишат девелоперите (не QA-тата). В най-добрия случай ги пишат преди кода (ако са добри XP зийлоти, и ги ползват като спецификация).

  4. Аз, макар и noob, отново имах какво да науча от тук. Благодаря

Вашият коментар

Вашият имейл адрес няма да бъде публикуван. Задължителните полета са отбелязани с *