Тести, тести, тести… Вони стали частиною моєї практики доволі нещодавно. На початку моєї карʼєри про них і не знали, а далі зазвичай все зводилося до класичного "у нас немає часу і ресурсів".
А потім вони зненацька увійщли в моє життя, як то кажуть — "увірвалися з ноги". Ми мали підтримувати 95% покриття тестами на UI, що призводило до доволі очікуваного результату — покривалися рядки коду, а не логіка чи поведінка. Відповідно, і якість таких тестів була передбачуваною.
Моє ж ставлення до девелоперського тестування суттєво змінилося після переходу до DataRobot. Тут теж вимагається додавати тести до функціоналу, але з однією суттєвою відмінністю — вони мають бути доцільні.
Питання юніт та інтеграційних тестів на фронтенді доволі неоднозначне. Що тестувати? Як? Наскільки ретельно? Трактування може відрізнятися від команди до команди, від ліда до ліда. Але є дуже проста рамка, яка дозволяє мені лишатися в межах здорового ґлузду.
Почнемо з юнітів. Ними я покриваю саме логіку. Не поведінку. Для цього дуже зручно користуватися підходом "one hook to rule them all", бо в такому випадку я можу перевірити основні сценарії, не покладаючись на розмітку і
.toBeInTheDocument, якщо мене цікавить, чи правильна комбінація даних на виході.Але, якщо подумати, то це вже й не юніт, а, по суті, інтеграційний тест. Бо в такому випадку я перевіряю якраз правильність роботи різних функцій та розрахунків разом. А от справжні юніт-тести — це вже рівент окремих функцій та, вимушено, кастомних хуків, які виконують щось одне.
І покриваю я зазвичай далеко не усі кейси. Мені достатньо перевірити так званий happy-path, потім пусті дані і нарешті невалідні. Якісь супер екзотичні комбінації ми залишаємо на випадок, якщо воно дійсно потім десь вилізе багом, а до того приймаємо, що система може надати нам лише очікуваний набір параметрів.
Чому я не перевіряю екзотику? Бо якщо таке трапляється, то це, швидше за все, баг в іншому компоненті системи, і виправляти треба саме його. Тести моїх модулів перевіряють рамки, очікувані саме для них. Я не повинен передбачати якісь чудернацькі сценарії поза межами відповідальности мого функціоналу.
Тож таке просте правило — якщо на умовному проді в мою систему прийшли неочікувані дані, то першим припущенням буде, що некоректно працює саме модуль, який їх надає. Це дозволяє мені залишати власні тести сфокусованими і не перевіряти чужі помилки.
Щодо інтеграційних UI тестів. Тут дещо складніше скласти ту рамку, бо не завжди зрозуміла межа відповідальності. Ще менш зрозуміла межа між тим, де це ще інтеграційний тест кількох UI компонент, а де вже повноцінний end-to-end.
Якщо користуватися підходом з єдиним state-hook в компоненті, то мені не треба перевіряти UI-flow, аби дізнатися, чи змінює натискання кнопки видимість якогось елемента. Для цього є типу інтеграційний тест для цього хука, де я змінюю один набір даних і перевіряю, чи змінився стан потрібним мені чином.
І переконатися, що UI реагує правильно, дуже просто. Якщо тест, в якому ми перевірили зміну стану, працює правильно, то для перевірки UI нам достаньо просто замокати різні зліпки цього стану і дивитися, чи вірно відбувається рендер HTML. Це позбавляє мене необхідності емулювати e2e-тести.
Покриття усіх сценаріїв тут теж стає не обовʼязковим. Якщо у нас правильно протестовані попередні шари: функції, хуки, стейт, то нам достатньо перевірити кілька ключових комбінацій даних для відображення UI.
А ще у нас є золоте правило — додавати тест на баг-фікси. Це треба в тому випадку, якщо ми усе-таки не передбачили якийсь валідний інпут, і таки існує якась комбінація вхідних параметрів, яка може зламати логіку. Але якщо використовувати ось такий шаровий підхід до тестів, то це означатиме, що такий випадок доволі рідкісний і стосується саме edge-cases. А едж-кейси на те й едж, що їх важко передбачити.
Ну якось так. Пишіть тести, вчіть теорію тестування — ви собі потім подякуєте. А за нагоди поговоримо про тести та AI.
@babichdev
P.S. Росія — кончена країна кончених людей. Головне ж для нас — триматися купки. Обіймаю усіх.