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

Здравейте всички. В тази статия ще научим всичко за модулното тестване във Flutter и Dart.

Дневен ред

  • „Какво е автоматизирано тестване?“
  • „Какво е тестване на единици?“
  • „Защо трябва да тестваме?“
  • „Какво можем да тестваме в единица?“
  • „Как да направя модулно тестване в Dart?“
  • „Добри практики за тестване на единици“
  • "Подигравателен"
  • „Различни начини за подигравки“
  • Мокито
  • MockTail

Какво е автоматизирано тестване?

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

Първо, трябва да разберем нещо. Автоматичното тестване е просто начин да се предотврати очакваните грешки в софтуера. Това не означава, че софтуерът няма грешки. Това просто означава, че софтуерът няма очаквани грешки.

„Тестването показва наличието, а не липсата на грешки.“ Едсгер Дайкстра

Цикълът винаги протича по следния начин:

Както виждаме, след фазата на разработка, ние пишем очаквани тестови случаи (TDD е изключение). След като премине всички тестови случаи, ние обикновено изпращаме софтуера. Докато софтуерът е в производство, ако възникне грешка, ние я коригираме и пишем тестовите случаи, за да предотвратим софтуера от същата грешка в бъдеще.

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

Какво е Unit Testing?

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

Единичното тестване е най-долният слой в тестовия пакет. В този слой тестваме вътрешната работа на всяка функция.

Единичният тест има три фази:

  • Подредете
  • действайте
  • Твърди

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

Във фазата Act ние изпълняваме модула с някакво състояние (предаване на аргументи) и съхраняваме резултата, ако има такъв.

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

Фазата Подреждане и действие не е задължителна.

Защо се нуждаем от единичен тест?

  • Единичното тестване е много лесно за писане и изпълнение. Така се спестява много време.
  • Единичното тестване ни помага да идентифицираме грешки на ранен етап. Така се спестяват много време и пари.
  • Тъй като пишем всички очаквани случаи на единицата, всеки може да разбере какво представлява тази единица. По този начин действа като по-добра документация.
  • Често няма да преработим кода си, мислейки, че това може да повреди модула. Наличието на модулни тестове ни дава увереността да преработим нашия код.
  • Отстраняването на грешки е просто. Тъй като знаем точно случаите, които се провалят, можем да определим точната единица, която причинява грешката.
  • Разглеждайки тестовите случаи, можем лесно да разберем какво представлява устройството. Така дългосрочната поддръжка е по-лесна.

Какво можем да тестваме в единица?

За да имаме добър модулен тестов пакет, трябва да разберем какво трябва да тестваме в един модул.

Обикновено модулното тестване се основава на следните аспекти:

  • Променливи на състоянието
  • Извиквания на функция/променлива
  • Функционални аргументи
  • Функцията се връща

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

Ето някои от случаите, които трябва да търсим в единица:

  • Проверете стойността на константата или крайната променлива.
  • Начални стойности на променливите на състоянието.
  • Проверете дали устройството извиква определена функция 1...n пъти.
  • Проверете дали устройството никога не извиква определена функция.
  • Уверете се, че променливите на състоянието са актуализирани според очакванията.
  • Резултатът от модула е същият като очаквания.
  • Ако има включен низ, списък или друг сложен DS, не забравяйте да проверите празните случаи, особено ако преминаваме през DS.
  • Проверете за null case (Само за nullable типове. Dart вече е null safe)
  • Проверете типа на променливата или аргумента (не е необходимо, ако използваме добре системата за типове на Dart)

Системата за нулева безопасност и тип на Dart спестява много тестови случаи във всички сценарии.

Как да направите модулно тестване в Dart?

  • Първо, трябва да включим зависимостта във файла pubspec.yaml.
  • Създайте тестов файл за dart с наставката _test. Помага на дартс анализатора да третира файла като тестов файл.

Ще тестваме площта на кръга.

sample_project/
  lib/
    area.dart
    main.dart
  test/
    area_test.dart

Ето тестовия файл на Dart. Всеки тестов файл трябва да има main() метод, в който стартира изпълнението.

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

Този тест очаква площта на кръга да бъде 3,141592, когато радиусът е 1. Тук мерната единица е методът circle().

  • Във фазата на подреждане (настройка) създадохме екземпляр за класа Area.
  • Във фазата Act(run) пуснахме модула, който искахме да тестваме, с някои данни.
  • Във фазата Assert(verify) сравнихме резултата с очаквания резултат.

Ето още един тест, който просто проверява стойността на гетъра pi:

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

Групиране на тестовите случаи

Често трябва да напишем много подобни тестови случаи. Методът group() ни помага да групираме подобни тестови случаи за лесно управление.

Всичко е наред, но създадохме екземпляра Area за всеки тест. С нарастването на тестовите случаи трябва да повторим същата стъпка за всеки тестов случай. Това не е добра практика.

Методът setUpApp() се изпълнява първо преди всички тестови случаи. Използвайки това, можем да намалим повторенията.

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

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

Лоша практика е да има общо състояние сред много тестови случаи.

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

Методът setUp() е подобен на метода setUpAll(), но разликата е, че той се изпълнява всеки път преди тестов случай в текущия обхват. Така всеки тестов случай получава свое собствено състояние.

Тук методът setUp() се изпълнява преди всеки тестов случай. По този начин има отделни екземпляри за всеки тест. Следователно няма държавни проблеми.

Винаги предпочитайте метода setUp() пред метода setUpAll(), освен ако обратното извикване не е бавно.

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

setUp(), setUpAll(), tearDown() и tearDownAll() могат да се използват вътре в group() за ограничаване на обхвата.

Добри практики за тестване на единици

  • Единичните тестове трябва да са бързи
  • Единичните тестове трябва да са прости
  • Единичните тестове трябва да бъдат детерминистични
  • Единичните тестове трябва да бъдат фокусирани
  • Повторението на кода е ОК в тестовете на модула
  • Описанието на теста трябва да е лесно разбираемо

Единичните тестове трябва да са бързи. Целият ни модулен тестов пакет трябва да може да работи в рамките на няколко минути, така че сами да стартираме тестовия пакет във фазата на разработка. Това помага за ранното отстраняване на грешки. Ако отнема повече време, тогава обикновено хвърляме тази работа в някакъв вид конвейер.

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

Единичните тестове трябва да бъдат детерминистични. Единичният тест трябва да се държи точно както преди, където и когато е бил тестван, без да променя изходния код. Единичният тест не трябва да зависи от никакви външни фактори като текущо време, база данни, уеб, собствен API и т.н. Обикновено ги подиграваме.

Единичните тестове трябва да бъдат фокусирани. Тестът на единица трябва да се фокусира само върху една единица. Не трябва да тестваме зависимостите в модулния тест.

Повторението на кода е ОК в тестовете на модула. Единичният тест трябва да се фокусира повече върху простотата, отколкото върху добрите практики за кодиране. Всеки трябва да разбере тестовия случай, без да търси неговите зависимости или текущото състояние на модула. Така че е добре да се повтарят някои части от кода, ако това подобрява лесното разбиране и простотата.

Описанието на модулния тест трябва да бъде лесно разбираемо. Доброто описание трябва да съдържа четири части:

  • Устройството, което ще тестваме
  • Текущото състояние на блока
  • Приносът, който ще дадем
  • Отговорът, който очакваме

В това описание на теста „площта на кръга с радиус 1 трябва да бъде 3.141592“, можем да го разделим по следния начин:

  • „Площта на кръга“ е единицата, която ще тестваме
  • „Радиус 1 е входът, който ще използваме
  • „3.141592“ е резултатът, който очакваме

Тази единица няма състояние.

Подигравателен

Основната идея зад тестването на единици е да изолираме и да се фокусираме върху текущата единица, която тестваме, а не върху поведението на външни зависимости. Но в повечето случаи трябва да зависим от външни зависимости като DB, уеб сървъри, API на платформата, външни устройства и т.н.

Да приемем, че текущата ни единица зависи от уеб API. Тестът работи бавно, но добре, когато сървърът е активен. Но когато сървърът е офлайн, тестът на модула е неуспешен. Това прави единичния тест непредвидим. Тъй като уеб сървърът няма да бъде под наш контрол. Не е наша грешка, когато уеб сървърът падне. Тук идва подигравката.

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

Да видим един пример.

Ще тестваме този fetch() метод в класа хранилище. Както виждаме, нашата единица зависи от метода RemoteDataSource’s fetch(), който комуникира с уеб API, което е външна зависимост. И така, трябва да закъсаме метода fetch() от RemoteDataSource.

Запушването се отнася до промяна на поведението на някаква част от устройството, без да се засяга нищо друго.

Има някои начини, с които можем да се подиграваме.

Внедряване на класа, на който трябва да се подиграваме

Тук сме внедрили класа RemoteDateSource на два макетни класа. MockRemoteDateSourceSuccess което се подиграва на случая на успех и MockRemoteDateSourceFailure което се подиграва на случая на провал.

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

  • В първия тест използвах екземпляра MockRemoteDateSourceSuccess, който имитира случая на успех и потвърдих, че резултатът е екземпляр на Student.
  • Във втория тест използвах екземпляра MockRemoteDateSourceFailure, който имитира случая на грешка и проверих, че извикването на метода хвърля изключение.

Разширяване на абстрактен клас, на който трябва да се подиграваме

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

Това помага за намаляване на някои ненужни реализации от конкретен клас.

Но и двете техники имат два проблема.

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

Тук влизат в действие подигравателните пакети.

Mockito — Фалшива библиотека за Dart

Този пакет решава и двата проблема. Идеята зад библиотеката Mockito е, че ние просто трябва да използваме @GenerateMocks() анотация, за да предадем класовете, които трябва да осмиваме. Той генерира макетния клас за нас с помощта на пакета build_runner (Не забравяйте да включите build_runner като dev_dependency).

Тук направихме частта за зарязване, използвайки метода when() от Mockito.

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

dart run build_runner build --delete-conflicting-outputs

build_runner ще генерира файл с име въз основа на файла, съдържащ анотацията @GenerateMocks. Тук, в примера repository_test.dart, импортираме генерираната библиотека като repository_test.mocks.dart.

  • Не е нужно да се местим никъде, за да видим логиката на внедряването. Всичко щеше да е точно там.
  • Генерирането на макетния клас се извършва от mockito, поради което е лесен за поддръжка.

Можем просто да се концентрираме върху мъничето, да действаме и да твърдим. Това е.

Единственият проблем с Mockito е генерирането на код. Можем да го преодолеем с MockTail.

MockTail — Без генериране на код

Mocktail се фокусира върху предоставянето на познат, прост API за създаване на макети в Dart (с нулева безопасност) без необходимост от ръчни макети или генериране на код.

Тази библиотека е написана от Феликс Ангелов, същият човек, който написа bloc, equatable и т.н. Тази библиотека дори има 100% покритие на кода, както другите пакети, които той е написал.

Това е. Без анотация и без генериране на код. Само един ред.

class MockRemoteDataSource extends Mock implements RemoteDateSource{}

Браво, хора! Готови сте за единично тестване.

Надявам се тази статия да ви е харесала.

Want to Connect?
Connect with me on LinkedIn, Twitter, GitHub.