Пътуване към по-надежден софтуер, доставен по-рано

Обща йерархия на тестовете

През 2009 г. Майк Кон, пъргав евангелизатор и практик, измисли термина „Тестова пирамида“. От дъното до върха се състоеше от три слоя: Unit tests, Service Tests и UI Tests. Наименованието може да изглежда малко неясно, а самата пирамида е остаряла до известна степен. Но основната идея зад него е доста проста. Трябва да има голямо количество малки и бързи тестове от по-ниско ниво, обикновено наричани модулни тестове. Колкото по-високо се изкачвате в пирамидата, т.е. колкото по-груби са вашите тестове, количеството им трябва да намалява. Мотивите зад тази идея също са доста прости: евтино и лесно е да се пишат фини модулни тестове. Освен това те обикновено работят изключително бързо.

Всеки нов слой е скок към забележителна абстракция. Поне като говорим за backend код, най-практичните нива са тези, които съответстват на абстракционните слоеве. Те обикновено са следните, от най-високата до най-ниската:

  1. Тестове от край до край, ниво на пътуване на клиента. Те обикновено включват браузър, имитиращ поведението на реалния потребител.
  2. Крайни тестове, които включват транспорт. Те обикновено съответстват на нивото на контролера на вашето приложение. Такива тестове обикновено се наричат ​​интеграционни тестове.
  3. Единични тестове за класове, използвани в потребителска история. Това е вашето ниво на домейн.

Този списък е в съответствие с архитектурата на портовете и адаптерите, популяризирана от Робърт Мартин в неговата книга.

Единични тестове

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

Защо да нямате само модулни тестове, ако са толкова страхотни? Защото, когато трябва да преработите някаква потребителска история, ще бъдете обхванати от тестове на контролера. Ако няма такива, ще имате големи проблеми.

Интеграционни тестове

Концепцията за тест на единица има доста неясно определение. Интеграционен тест още повече.

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

Бъдете много внимателни, за да останете на същото ниво на абстракция. Например, ако тествате крайна точка за регистрация на поръчка, не тествайте дали има запис в база данни. Това е детайл от изпълнението. Днес използвате Postgres, а утре завършвате с Mongo. Вместо това извикайте крайната точка на API с подробности за поръчката, за да сте сигурни, че съдържа всички необходими данни. Какъв е смисълът от това? Интеграционните тестове са вашата предпазна мрежа. Когато подробностите за внедряването се променят, понякога драстично, искате да сте сигурни, че всичко работи според очакванията.

Писането на интеграционни тестове често включва дискусии за това на какво да се подигравате и на какво не. За мен ръководството е просто. Вашият код трябва да бъде най-крехката и податлива на грешки част в целия процес на тестване. Кажете, кое е по-надеждно, вашата база данни или кодът на вашето приложение? Обзалагам се, че е първото. А какво да кажем за мрежата? Мрежата е известна като крехка и бавна и това е причината мрежовите обаждания рядко да участват в тестове. Друг пример е, когато вашето приложение работи с външни устройства, които са извън вашия контрол. Например устройствата, които печатат чекове, са доста крехки. Не искате тестът ви да е неуспешен, защото принтерът няма хартия, мастила или нещо друго. В противен случай в крайна сметка ще тествате тези устройства, вместо да тествате кода си.

E2e тестове

Тестовете от край до край са последната линия на защита. Те тестват пълно пътуване на клиента, от потребителския интерфейс до бекенда, отново до потребителския интерфейс и така нататък. Винаги имам предвид принципа на Парето, когато ги пиша: 20% от всяко усилие дава 80% от резултата. Идентифицирайте вашите ключови бизнес процеси и ги автоматизирайте. Обикновено това е този, който носи на вашия бизнес най-много пари. Например регистрация на поръчка и по-нататъшна обработка, водеща до нейната доставка. По този начин, с такива тестове във вашия тестов пакет, няма шанс да въведете опустошителен бъг, така че гърбът ви ще бъде покрит.

Обратната страна е скоростта и надеждността, които липсват на e2e тестовете.
Тестовете, изпълнявани в конзола, са с около един порядък по-бързи в сравнение с тестовете с включен браузър. Така че въпреки че нещата може да изглеждат блестящи в началото, преди година можеше да се окажете с пакет от тестове, чието изпълнение отнема един час.

Освен това тестовете от край до край често са нестабилни. И колкото по-сложни са те, толкова по-нестабилни стават поради нарастващ брой непредвидени фактори. Някаква рядка странност на браузъра, неочаквано поведение на оформлението - всичко това допринася за цялостната нестабилност. Все пак има общоприето решение. Не завъртайте браузър; работи само през DOM. Така вашите тестове ще бъдат много по-бързи, но не 100% от край до край.

Наличието на тестове от край до край ви помага да разкриете неочаквано свързване, особено това, което е трудно забележимо – например случайно свързване между потребителски истории. Спомняте ли си как последния път променихте някаква потребителска история и друга неочаквано се повреди? Точно такъв е случаят с тестовете от край до край, обхващащи набор от потребителски истории, които формират определено пътуване на клиента.

Хората често обединяват e2e тестове с UI тестове, което е по-широко понятие.

UI тестове

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

Като цяло има две неща за тестване в потребителския интерфейс. Първото е поведението. Има безброй примери за функционалност, които могат да бъдат обхванати с модулни тестове на потребителския интерфейс. Изчисляване на общата сума на поръчката, връзка, водеща до правилен URL адрес, известие за предупреждение се показва в червено и т.н. Освен това не е нужно да тествате повечето от поведението от край до край, дори ако имате нужда от отговор от бекенда. Напишете мъниче и сте готови. Много добре може да се тества изолирано.
Jasmine и Mocha са добро място да започнете с UI модулни тестове.

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

Тестове в рамките на непрекъсната доставка

Ако искате да внедрявате софтуера си често и надеждно, наличието на тестово портфолио не е достатъчно. Ключът е автоматизирането му. Трябва да можете да внедрите своя код по всяко време на деня и нощта. Искате ли да разчитате на човешки същества, които могат да забравят да изпълнят тестове, преди да накарат кода? Искате ли да внедрите кода си само за да разберете, че не се компилира? Обзалагам се, че не го правите. Така че най-добрият съвет, който можете да получите, е да използвате CI/CD тръбопровод, защото той изключва човешкия фактор.

Освен това процесът на предоставяне на инфраструктура и внедряване са доста досадни и повтарящи се задачи, в които ние, хората, сме особено лоши. CI/CD тръбопроводите, напротив, са изключително добри в автоматизирането на тези задачи. Ръчните задачи не се мащабират. Ако имате нужда от един ръчен тестер сега, ще имате нужда от нарастващ брой от тях, докато системата ви расте. Така че вместо да отделяте все по-голямо количество време за извършването им, можете да автоматизирате всичките си процеси веднъж завинаги.

Често срещани клопки

Ръчно изпълнявани тестове
Липсата на автоматизация на тестовете е перфектна рецепта как да изоставите дейността по тестване на вашия екип. Няколко пъти, когато някой член на екипа забрави да изпълни тестове, преди да натисне кода, може да бъде достатъчно, за да бъдете разочаровани от цялото начинание за тестване. Как не? Използвайте подходящ CI/CD тръбопровод, който никога не забравя провеждането на вашите тестове преди внедряването.

Нестабилни тестове
Нестабилният тест е този, който се проваля от време на време. Има няколко проблема с този вид тестове.

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

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

Фунийка за сладолед
Малко количество евтини и бързи тестове, огромно количество бавни и скъпи (от гледна точка на времето, необходимо за написването им) UI тестове. Така една тънка тестова пирамида се изражда в тромава фунийка за сладолед.

Този конус се появява, когато започнете с повече тестове на високо ниво, отколкото трябва. Целият ви тестов пакет става бавен и грешките стават трудни за забелязване. Вместо да тествате, вие се озовавате в мрачна страна за отстраняване на грешки. Освен това подобен род тестове не са особено надеждни. Ако стартирате един и същ e2e тест, включващ браузър, сто пъти, шансовете са един от тези тестове да се провали - поради някои странности на браузъра.

Това не е истински код — това е просто тест!
Тестовият код по никакъв начин не се различава от останалата част от вашия код. Наистина не е пуснат в производство. Но все пак трябва да го поддържате. И разходите за поддръжка са много по-високи, отколкото при писането. Така че същите най-добри практики, които се използват в производствения код, се прилагат и към кода за тестване.

Желание за тестване на частен метод
Това е ясна индикация, че клас, към който принадлежи този частен метод, нарушава принципа на единичната отговорност. Това е Божествен обект, който знае твърде много и прави твърде много. Лечението е просто: извлечете този частен метод в неговия собствен клас и го покрийте с модулни тестове.

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

Структурата на тестовете точно отразява структурата на класовете
Това изглежда логично от пръв поглед, но погледнете го по този начин. Някои от вашите тестове трябва да ви служат като предпазна мрежа, която гарантира, че поведението на вашата система няма да се промени, когато промените нейната вътрешна реализация. По-често повечето от нашите класове от по-ниско ниво представляват тази вътрешна реализация. Така че всяка модификация в този клас би причинила съответния провал на теста. Поправянето му всеки път е трудна задача. Това, което Робърт Мартин препоръчва, е „тест на противоположна вариация“, което означава тестване само на наблюдавано поведение на клас. Което означава, че ако вашият клас A, осигуряващ някакво специфично за домейн поведение, използва куп специфични за изпълнението и стабилни помощници, не е необходимо да ги тествате. XTest би било добре. Така че няма нужда от тестване на цялото вътрешно внедряване, това е много крехко.

В заключителната

Тази тема е наистина огромна и едва съм надраскал повърхността тук. Предлагам ви да прочетете повече за него; „Разрастване на обектно-ориентиран софтуер, ръководен от тестване“ и „Изкуството на софтуерното тестване“ са страхотни източници на допълнително вдъхновение.