JavaRush/Java блог/Random UA/Java Unit Testing: методики, поняття, практика
Константин
36 рівень

Java Unit Testing: методики, поняття, практика

Стаття з групи Random UA
учасників
Сьогодні і не зустрінеш програми, не обвішані тестами, тому ця тема буде як ніколи актуальна для розробників-початківців: без тестів — нікуди. На правах реклами запропонуватиму переглянути мої минулі статті. У деяких із них зачіпаються тести (та й так статті будуть дуже корисні):
  1. Інтеграційне тестування БД за допомогою MariaDB для заміни MySql
  2. Реалізація мультимовності програми
  3. Збереження файлів у додаток та даних про них на БД
Розглянемо, які види тестування використовують у принципі, а потім детально вивчимо все, що потрібно знати про юніт-тестування.

Види тестування

Що таке тест? Як говорить Вікі: « Тест або випробування - спосіб вивчення глибинних процесів діяльності системи за допомогою приміщення системи в різні ситуації та відстеження доступних спостереженню змін у ній». Іншими словами, це перевірка правильності роботи нашої системи у тих чи інших ситуаціях. Все про Unit testing: методики, поняття, практика - 2Що ж, побачимо, які взагалі є види тестування:
  1. Модульне тестування (unit testing) – тести, завдання яких перевірити кожен модуль системи окремо. Бажано, щоб це були мінімально поділені шматочки системи, наприклад, модулі.

  2. Системне тестування (system testing) — тест високого рівня перевірки роботи більшого шматка програми чи системи загалом.

  3. Регресійне тестування (regression testing) — тестування, яке використовується для перевірки того, чи не впливають нові фічі або виправлені баги на існуючий функціонал програми та чи не з'являються старі баги.

  4. Функціональне тестування (functional testing ) - перевірка відповідності частини додатку вимогам, заявленим у специфікаціях, юзерстор і т.д.

    Види функціонального тестування:

    • тест «білої скриньки» (white box) на відповідність частини додатку вимогам із знанням внутрішньої реалізації системи;
    • тест «чорної скриньки» (black box) на відповідність частини додатку вимогам без знання внутрішньої реалізації системи.
  5. Тестування продуктивності (performance testing) — вид тестів, які пишуться визначення швидкості відпрацювання системи чи її частини під певним навантаженням.
  6. Тестування навантаження (load testing) — тести, призначені для перевірки стійкості системи при стандартних навантаженнях і для знаходження максимально можливого піку, при якому додаток працює коректно.
  7. Стрес-тестування (stress testing) — вид тестування, призначений для перевірки працездатності програми при нестандартних навантаженнях і визначення максимально можливого піку, при якому система не впаде.
  8. Тестування безпеки (security testing) – тести, що використовуються для перевірки безпеки системи (від атак хакерів, вірусів, несанкціонованого доступу до конфіденційних даних та інших радощів життя).
  9. Тестування локалізації (localization testing ) - це тести локалізації для програми.
  10. Юзабіліті тестування (usability testing) - вид тестування, спрямований на перевірку зручності використання, зрозумілості, привабливості та навчання для користувачів.
  11. Це все звучить добре, але як воно відбувається на практиці? Все просто: використовується піраміда тестування Майка Кона: Все про Unit testing: методики, поняття, практика - 4Це спрощений варіант піраміди: зараз її ділять більш дрібні деталі. Але сьогодні ми не перекручуватимемося і розглянемо найпростіший варіант.
    1. Unit — модульні тести, що застосовуються в різних шарах програми, що тестують найменшу логіку прикладної програми: наприклад, клас, але найчастіше — метод. Ці тести зазвичай намагаються максимально ізолювати від зовнішньої логіки, тобто створити ілюзію того, що решта програми працює в стандартному режимі.

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

    2. Integration – інтеграційне тестування. Воно перевіряє більші шматочки системи, тобто це або об'єднання кількох шматочків логіки (кілька методів чи класів), або коректність роботи із зовнішнім компонентом. Цих тестів зазвичай менше, ніж Unit, оскільки вони важкі.

      Як приклад інтеграційних тестів можна розглянути з'єднання з базою даних та перевірку правильного відпрацювання методів, що працюють з нею .

    3. UI — тести, які перевіряють роботу інтерфейсу користувача. Вони торкаються логіки на всіх рівнях програми, через що їх ще називають наскрізними. Їх як правило в рази менше, так вони найбільш важковагові і повинні перевіряти найнеобхідніші шляхи, що використовуються.

      На малюнку вище бачимо співвідношення площ різних частин трикутника: приблизно така сама пропорція зберігається у кількості цих тестів у реальній роботі.

      Сьогодні докладно розглянемо найвикористаніші тести — юніт-тести, тому що вміти ними користуватися на базовому рівні повинні всі Java-розробники, що поважають себе.

    Ключові поняття юніт-тестування

    Покриття тестів (Code Coverage) – одна з головних оцінок якості тестування програми. Це відсоток коду, який був покритий тестами (0-100%). На практиці багато хто женеться за цим відсотком, з чим я не згоден, тому що починається навішування тестів там, де вони не потрібні. Наприклад, у нас у сервісі є стандартні CRUD (create/get/update/delete) операції без додаткової логіки. Ці методи - суто посередники, що делегують роботу шару, що працює з репозиторієм. У цій ситуації нам нема чого тестувати: хіба те, що викликає цей метод — метод дао, але це не серйозно. Для оцінки покриття тестами зазвичай використовують додаткові інструменти: JaCoCo, Cobertura, Clover, Emma і т.д. Для більш детального вивчення даного питання на пару придатних статей: TDD (Test-driven development) – розробка через тестування. У рамках цього підходу насамперед пишеться тест, який перевірятиме певний код. Виходить тестування чорної скриньки: ми знаємо, що є на вході і знаємо, що має вийти на виході. Це дозволяє уникнути дублювання коду. Розробка через тестування починається з проектування та розробки тестів для кожної невеликої функціональності програми. У підході TDD, по-перше, розробляється тест, який визначає та перевіряє, що робитиме код. Основна мета TDD - зробити код більш зрозумілим, простим і без помилок. Все про Unit testing: методики, поняття, практика - 6Підхід складається з таких складових:
    1. Пишемо наш тест.
    2. Запускаємо тест, пройшов він чи ні (бачимо, що все червоне не психуємо: так і має бути).
    3. Додаємо код, який має задовольнити цей тест (запускаємо тест).
    4. Виконуємо рефакторинг коду.
    Виходячи з того, що модульні тести є найменшими елементами піраміди автоматизації тестування, TDD заснований на них. За допомогою модульних тестів ми можемо перевірити бізнес-логіку будь-якого класу. BDD (Behavior-driven development) – розробка через поведінку. Цей підхід ґрунтується на TDD. Якщо говорити точніше, він використовує написані зрозумілою мовою приклади (як правило англійською), які ілюструють поведінку системи для всіх, хто бере участь у розробці. Не заглиблюватимемося в даний термін, оскільки він в основному зачіпає тестувальників та бізнес-аналітиків. Тестовий сценарій (Test Case) — сценарій, що описує кроки, конкретні умови та параметри, необхідні для перевірки реалізації коду, що тестується. Фікстури (Fixture)— стан середовища тестування, який необхідний успішного виконання випробуваного методу. Це заздалегідь заданий набір об'єктів та його поведінки у умовах.

    Етапи тестування

    Тест складається із трьох етапів:
    1. Завдання тестованих даних (фікстур).
    2. Використання тестованого коду (виклик тестованого методу).
    3. Перевірка результатів та звірка з очікуваними.
    Все про Unit testing: методики, поняття, практика - 7Щоб забезпечити модульність тесту, потрібно ізолюватися від інших верств програми. Зробити це можна за допомогою заглушок, моків та шпигунів. Мок (Mock) — об'єкти, які налаштовуються (наприклад, специфічно для кожного тесту) і дозволяють задати очікування викликів методів у вигляді відповідей, які ми плануємо отримати. Перевірки відповідності очікуванням проводяться через виклики до об'єктів Mock. Заглушки (Stub) — забезпечують жорстку відповідь на виклики під час тестування. Також вони можуть зберігати інформацію про дзвінок (наприклад, параметри або кількість цих дзвінків). Такі іноді називають своїм терміном - шпигун ( Spy ). Іноді ці терміни stubs та mockплутають: різниця в тому, що стаб нічого не перевіряє, а лише імітує заданий стан. А мок — це об'єкт, який має очікування. Наприклад, що даний метод класу повинен бути викликаний кілька разів. Іншими словами, ваш тест ніколи не зламається через «стаб», а ось через мок може.

    Середовище тестування

    Отже, тепер ближчий до справи. Для Java доступно кілька серед тестування (фреймворків). Найпопулярніші з них - JUnit та TestNG. Для нашого огляду ми використовуємо: Все про Unit testing: методики, поняття, практика - 8JUnit тест є методом, що міститься в класі, який використовується тільки для тестування. Клас зазвичай називається так само, як і клас, який він тестує з +Test в кінці. Наприклад, CarService→ CarServiceTest. Система складання Maven автоматично включає такі класи у тестову область. Насправді цей клас і називається тестовим. Трохи пройдемося за базовими інструкціями: @Test - визначення даного методу як тестований (по суті - метод, позначений цією інструкцією і є модульний тест). @Before— позначається метод, який виконуватиметься перед кожним тестом. Наприклад, заповнення тестових даних класу, читання вхідних даних тощо. д. @After — ставиться над методом, який викликатиме після кожного тесту (чистка даних, відновлення дефолтних значень). @ BeforeClass - ставиться над способом - аналог @ Before. Але цей метод викликається лише один раз перед усіма тестами для даного класу і тому має бути статичним. Він використовується для виконання більш важких операцій, наприклад підйом тестової БД. @AfterClass - протилежність @BeforeClass: виконується один раз для даного класу, але виконується після всіх тестів. Використовується, наприклад, очищення постійних ресурсів чи відключення від БД. @Ignore— зазначає, що метод нижче відключений і ігноруватиметься під час загального прогону тестів. Використовується в різних випадках, наприклад, якщо змінабо базовий метод і не встигли переробити тест. У разі ще бажано додати опис — @Ignore("Some description"). @ Test (expected = Exception.class) – використовується для негативних тестів. Це тести, які перевіряють, як поводиться метод у разі помилки, тобто тест очікує, що метод викине деякий виняток. Такий метод позначається інструкцією @Test, але із зазначенням помилки для вилову. @ Test (timeout = 100) - перевіряє, що метод виконується не більше ніж 100 мілісекунд. @Mock- використовується над полем клас для завдання даного об'єкта моком (це не з Junit бібліотеки, а з Mockito), і якщо нам буде необхідно, ми поставимо поведінку мока в конкретній ситуації, безпосередньо в методі тесту. @RunWith(MockitoJUnitRunner.class) - метод ставиться над класом. Він і є кнопкою для прогону тестів у ньому. Runner можуть бути різними: наприклад, є такі: MockitoJUnitRunner, JUnitPlatform, SpringRunner і т. д.). У JUnit 5 інструкцію @RunWith замінабо більш потужною інструкцією @ExtendWith. Погляньмо на деякі методи порівняння результатів:
    • assertEquals(Object expecteds, Object actuals)— перевіряє, чи рівні об'єкти, що передаються.
    • assertTrue(boolean flag)- Перевіряє, чи повертає передане значення - true.
    • assertFalse(boolean flag)- Перевіряє, чи повертає передане значення - false.
    • assertNull(Object object)– перевіряє, чи об'єкт є нульовим (null).
    • assertSame(Object firstObject, Object secondObject)- Перевіряє, чи посилаються передані значення на один і той же об'єкт.
    • assertThat(T t, Matcher<T> matcher)- Перевіряє, чи задовольняє t умові, вказаній у matcher.
    Ще є корисна форма порівняння з асертий - assertThat(firstObject).isEqualTo(secondObject) Тут я розповів про базові методи, тому що інші - це різні варіації наведених вище.

    Практика тестування

    А тепер розглянемо наведений вище матеріал на конкретному прикладі. Тестуватимемо метод для сервісу — update. Розглядати шар дао не будемо, тому що він у нас дефолтний. Додамо стартер для тестів:
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Отже, клас сервісу:
    @Service
    @RequiredArgsConstructor
    public class RobotServiceImpl implements RobotService {
       private final RobotDAO robotDAO;
    
       @Override
       public Robot update(Long id, Robot robot) {
           Robot found = robotDAO.findById(id);
           return robotDAO.update(Robot.builder()
                   .id(id)
                   .name(robot.getName() != null ? robot.getName() : found.getName())
                   .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
                   .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
                   .build());
       }
    }
    8 - витягуємо об'єкт, що оновлюється з БД 9-14 - створюємо об'єкт через білдер, якщо в об'єкті, що приходить, є поле - задаємо його, якщо ні - залишаємо те, що є в БД І дивимося наш тест:
    @RunWith(MockitoJUnitRunner.class)
    public class RobotServiceImplTest {
       @Mock
       private RobotDAO robotDAO;
    
       private RobotServiceImpl robotService;
    
       private static Robot testRobot;
    
       @BeforeClass
       public static void prepareTestData() {
           testRobot = Robot
                   .builder()
                   .id(123L)
                   .name("testRobotMolly")
                   .cpu("Intel Core i7-9700K")
                   .producer("China")
                   .build();
       }
    
       @Before
       public void init() {
           robotService = new RobotServiceImpl(robotDAO);
       }
    1 - наш Runner 4 - ізолюємо сервіс від шару дао, підставляючи мок 11 - задаємо для класу тестову сутність (ту, яку ми будемо юзати в якості хом'ячка, що випробуваний) 22 - задаємо об'єкт сервісу, який ми і будемо тестувати
    @Test
    public void updateTest() {
       when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
       when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
       Robot robotForUpdate = Robot
               .builder()
               .name("Vally")
               .cpu("AMD Ryzen 7 2700X")
               .build();
    
       Robot resultRobot = robotService.update(123L, robotForUpdate);
    
       assertNotNull(resultRobot);
       assertSame(resultRobot.getId(),testRobot.getId());
       assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
       assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
       assertEquals(resultRobot.getProducer(),testRobot.getProducer());
    }
    Тут ми бачимо чіткий поділ тесту на три частини: 3-9 - завдання фікстур 11 - виконання тестованої частини 13-17 - перевірка результатів Детальніше: 3-4 - задаємо поведінку для мока дао 5 - задаємо екземпляр, який ми будемо апдейтувати поверх нашого стандартного 11 - використовуємо метод і беремо результуючий екземпляр 13 - перевіряємо, що він не нуль 14 - звіряємо айді результату і задані аргументи методу 15 - перевіряємо, чи оновилося ім'я 16 - дивимося результат по cpu 17 - так як в екземплярі для оновлення поле, воно має залишитися тим самим, перевіряємо це. Все про Unit testing: методики, поняття, практика - 9Запускаємо: Все про Unit testing: методики, поняття, практика - 10Тест зелений, можна видихати)) Отже, підіб'ємо підсумки:тестування покращує якість коду та робить процес розробки більш гнучким та надійним. Уявіть собі, скільки сил ми витратимо при зміні дизайну програмного забезпечення з сотнями файлів класів. Коли ми маємо модульні тести, написані для всіх цих класів, ми можемо впевнено провести рефакторинг. І найголовніше – це допомагає нам легко знаходити помилки під час розробки. Гайз, на цьому у мене сьогодні все: сипемо лайки, пишемо коментарі))) Все про Unit testing: методики, поняття, практика - 11
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.