Про ScalaCheck

Частина 1. Введення.

ScalaCheck - це комбінаторна бібліотека, що значно полегшує написання модульних тестів на Scala. У ній використовується підхід property-based тестування, вперше реалізований у бібліотеці QuickCheck для Haskell. Існує безліч реалізацій QuickCheck: як для Java, C, так і для інших мов і платформ. Використання цього підходу дозволяє значно скоротити час розробки тестів.

Ця серія статей багато в чому схожа на мою попередню, присвячену Parboiled, тому структура оповіді буде схожою. Я розповім вам, для чого все це потрібно, потім ми навчимося дивитися на світ крізь призму властивостей та генераторів, а потім перейдемо до складніших речей. Зацікавило? Прошу під кат.

Структура циклу

Модульне тестування є одним із найважливіших підходів у розробці програмного забезпечення. Навіть якщо ваша програма при компіляції проходить перевірки лише на рівні системи типів, це ще означає, що у ній відсутні логічні помилки. Це означає, що якою б потужною не була ваша мова програмування, без тестування коду не обійтися. Проте, вартість тестування дуже висока: крім витрачених людино-годин потрібно витрачати нелюдо-усилия на рутинне написання модульних тестів. Через це багато замовників економлять на тестуванні, чим багато програмістів користуються з великою радістю: модульні тести писати нудно (але потрібно!).

Погодьтеся, покривати величезну кількість випадків і писати однотипні синтаксичні конструкції день у день – задоволення сумнівне. На жаль, навіть типобезпечний код функціональними мовами потрібно тестувати. І хоча саме застосування функціональної парадигми значно полегшує модульне тестування — відсутністьпобічних ефектів дозволяє розглядати менше крайових випадків і зовсім забути про тестування стану, кількість тестуючого коду це значно не скорочує. Крім того, незважаючи на те, що ручне тестування дасть вам деяку впевненість у вашому коді, воно не може гарантувати, що згодом не виявиться якийсь окремий випадок, який ви пропустили з уваги.

Підхід ScalaCheck дозволяє перейти на наступний рівень абстракції та уникнути значної частини цієї рутини, одночасно підвищивши читаність тестуючого коду та скоротивши його обсяг (що позитивно позначиться на його ремонтопридатності). Покриття вашого коду тестами також помітно зросте. І щоб досягти цього, потрібно лише познайомитись із кількома новими концепціями.

Що таке властивості

Насамперед, давайте розберемося в тому, що є властивістю. Якщо двома словами, то властивість — це деяке логічне висловлювання, що пов'язує вхідні значення функції, що тестується, і отримані на них результати. При цьому саме слово «властивість» тут слід розуміти не в побутовому програмістському сенсі (що належать деякому об'єкту дані), а в математичному — як певний закон чи правило, справедливі цілком для певної кількості об'єктів. Згадайте, наприклад, властивості асоціативності чи дистрибутивності в алгебрі речових чисел.

При тестуванні властивостей нас не хвилюють деталі реалізації тестованої функції, а тільки її вхідні і вихідні дані, специфікація. Візьмемо тепер для прикладу таку просту властивість, записану мовою Лейбніца:

Якщо ви не розумієте лейбницького, не впадайте у відчай: далі все буде на Scala.

ScalaCheck дозволяє записати його практично в оригінальнійматематичної форми:

Українською це читається приблизно так: «Для будь-якого речового числа x, не рівного нулю, x² завжди більше за нуль».

Автор чудово розуміє, що тип Double - це ні крапельки не ℝ, проте для спрощення викладу абсолютною коректністю формулювань доведеться пожертвувати.

Як бачите, властивість являє собою вищий рівень абстракції, ніж традиційні assertion-тести в JUnit. Однак, зрештою все зводиться до них: на основі абстрактного опису властивостей ScalaCheck генерує цілком конкретні тести окремих значень, які можна порівняти за якістю з тими, які б ви писали руками.

Властивості - не теорії

У JUnit4 з'явився цікавий механізм під загальною назвою теорій. (Ох, вони б ще гіпотезами їх назвали ...) Теорії працюють дуже схожим на ScalaCheck чином, тільки не вміють генерувати випадкові вхідні дані і виконувати мінімізацію (shrinking).

На жаль, я не вигадав для цілком зрозумілого терміна «shrinking» кращого перекладу на українську, ніж «мінімізація». Звучить не зовсім коректно, але краще, ніж інші мої спроби.

Отже, що таке теорія у поданні JUnit? Насамперед, це спеціальний тип модульного тесту. Теорія перевіряє, чи дійсно певна умова істинно для кожного елемента із заданої множини тестових даних (вони називаються data points). Це дозволяє один раз запрограмувати логіку перевірки, а потім швидко прогнати її на різних наборах даних.

Щоб ваш метод став теорією, його потрібно відповідним чином проанотувати: додати @Theory . Вхідні дані анотуються як @DataPoint. Цього достатньо, щоб раннер здогадався запустити тест кілька разів: один раз для кожного дата-поінта. Ось невеликийприклад, нахабно запозичений із документації JUnit:

Цей механізм схожий на те, що використовує ScalaCheck. Є лише дві маленькі відмінності:

  • ScalaCheck сам генерує тестові дані;
  • якщо на деякому наборі даних тест падає, ScalaCheck намагається добити його контр-прикладом простіше, підібрати якийсь більш простий окремий випадок для полегшення подальшого налагодження.

Саме ці відмінності дозволяє ScalaCheck здобути перемогу над теоріями як за обсягом коду, так і за кількістю тестів. Але якщо вам все ж таки цікаво дізнатися більше про теорії, ви можете почитати про них тут.

Плюси та мінуси property-based підходу

Всупереч можливим підозрам читача, ScalaCheck створювався не для того, щоб повністю витіснити ScalaTest або горезвісний JUnit, а для того, щоб внести до процесу модульного тестування додаткові переваги, як-от:

  • Лаконічність: менше коду – вище покриття порівняно зі стандартним (assertion-based) підходом
  • Високорівневість: ми фокусуємося на вхідних даних взагалі, а чи не на окремих випадках.
  • Мінімізація: коли щось зламалося, нам допоможуть знайти де саме (або ми допоможемо собі самі).
  • Наявність сутностей, які простіше тестувати як єдине ціле, ніж тестувати їх покомпонентно.

Тим не менш, властивість-орієнтоване тестування – не срібна куля. У цього підходу є й недоліки:

  • Час тестування гальмованих функцій змушує вити на місяць з туги. (Хоча можливо, це і плюс. Моїй практиці відомі випадки, коли використання ScalaCheck змушувало всіх учасників проекту задуматися про оптимізацію.)
  • Хибне відчуття безпеки. З ScalaCheck легко повірити, що ми щопокрили тестами все, що могли, але найчастіше це зовсім не так.
  • Нечутливість до граничних умов.
  • Рівномірне випадкове розподілення тестових наборів. Загалом це добре, але не завжди зручно.

Коли використати?

ScalaCheck можна використовувати так само, як будь-який інший фреймворк для тестування. Вам доведеться думати трохи інакше, але, в кінцевому підсумку, ScalaCheck дозволить вам написати практично будь-який тест - правда, не завжди найефективнішим, зручним та читаним способом. Однак, є області, де ScalaCheck по-справжньому хороший:

  • код, украй чутливий до вхідних даних;
  • кінцеві автомати або будь-які системи, що залежать від стану;
  • парсери (це те, для чого використовую ScalaCheck особисто я);
  • різноманітні перетворювачі даних:
  • валідатори;
  • класифікатори;
  • агрегатори;
  • сортувальники і т.д.
  • Spark RDD, а також мапери та редьюсери для Hadoop.
  • ScalaCheck

    Особливості бібліотеки

    • компактна бібліотека (менше двадцяти файлів із кодом);
    • відсутність додаткових залежностей;
    • підтримка тестів із внутрішнім станом (stateful testing);
    • відмова від використання java.util.Random як генератор псевдовипадкових чисел (більше того, ScalaCheck уважно стежить за тим, щоб випадкові тестові набори не повторювалися, використовуючи для цього пошук із поверненням).
    • підтримка scala-js та Dotty.

    Підготовчі роботи

    Після того, як ми визначилися з тим, чи потрібна нам властивість-орієнтоване тестування та ScalaCheck зокрема, давайте приступимо до підготовчих робіт. Додайте наступну залежність у вашпроект (я розраховую що ви, шановний читачу, вже перейшли на Scala 2.12):

    Також передбачається, що ви використовуєте останню версію. Існує проблема при використанні застарілих версій бібліотеки у зв'язці зі Scala 2.12. Будьте обережні.

    Як вже було сказано раніше, ScalaCheck побудований на двох основних концепціях: на властивостях та генераторах. Властивості добре висвітлені у багатьох блогах, у тому числі й українськомовних. Тому більше уваги я намагатимусь приділити генераторам. У цій частині ми ознайомимося як з властивостями, так і з генераторами.

    Трохи про властивості

    Властивість являє собою мінімальний модуль, що тестується. Представлено екземпляром класу org.scalacheck.Prop. Найпростіший приклад:

    Трохи про генератори

    Насправді генератори доводиться писати не рідше, ніж характеристики. Щоб скористатися ними, вам слід проімпортувати org.scalacheck.Gen .

    Також у ScalaCheck є набір готових генераторів для стандартних типів:

    Ви також можете об'єднувати наявні генератори, застосовуючи до них map і for comprehension:

    А у нас тут можна отримати грант на тестовий період Яндекс.Хмари. Варто лише у полі «секретний пароль» запровадити «Хабр»