Чому я хочу поговорити про тестування? В моїй попередній команді було 6 розробників і 1 тестувальник. При цьому time to market у нового функціонала був маленький.
Зараз я змінив команду. В ній 3 розробника і 3 тестувальника. На перший погляд, час реалізації нового функціоналу значно більший. Здається, - це через відсутнійсть правильної тест стратегії і подходів. Тому хочу поділитись інструментами, які допомагають писати тести швидше і якісніше.

Почнемо від Unit тестів і до E2E, від всім знайомих інструментів до тих, які ви, ймовірно, не знали.

Unit test

  • На цьому рівні тестуємо всю бізнес логіку проекту. Всі валідації, конвертації, виклики функцій і результати їх роботи.

  • Що не тестуємо? Будь-яку взаємодію з базою, чергою, стороніми сервісами.

Dependencies

  • Як не тестувати залежності? Всі залежності на бази, сервіси, черги повинні передаватися до модулю ззовні. Взаємодія з ними здійснюється через інтерфейси. Підмінити реалізацію інтерфейсу можна за допомогою бібліотек Moq або NSubstitute.
    Ці бібліотеки надають можливість повернути конкретне значення виклику функції. Або перевірити з якими аргументами і скільки разів викликалась функція.

  • А у функціональних мовах? Всі залежності функції мають передаватись до неї у вигляді аргументів. Тип такого фргументу - функція. Тож під час тестування можна передати свою тестову функцію яка повртає конкретне значення.

Test Scenario

  • Скільки сценаріїв може тестувати тест? Один. Притримуємось підходу AAA. Arrange - підготували дані для тесту. Act - виконали дію. Assert - перевірили результат. Як тільки хочеться ще далі написати Act, Assert - зупитяємо себе, та пишемо наступний тест.

  • Допоміжний інструмент для цього правила - ім`я тесту. По імені повинно бути зрозуміло що він перевіряє. Як тільки тест називається абстрактно filter should work correct - це сигнал, що в тесті більше одного бізнес правила, або немає явного сценарію перевірки.

Test Data

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

  • AutoFixture - бібліотека, яка вміє створювати складні обєкти і наповнювати їх даними. Дані можуть бути випадкові або згенеровані по заданим правилам.
    У бібліотеки є інтеграції з усіма популярними тест фреймворками (XUnit, NUnit, MSTest). Після підключення достатньо позначити тест атрибутом AutoData, і додати параметр до тестової функції. Бібліотека автоматично створить обєкт і передасть його в якості аргументу до тесту. Далі достатньо змінити тільки окремі дані, що впливають на результат.
    [Theory, AutoData] void should_not_authorize_underage(Person person)

  • Імутабельні структури даних. Якщо це C# - то використовуємо records (в F#, Scala є щось подібне). Створюємо обєкти з даними за замовчуванням, а в тесті змінюємо тільки поля, які впливають на результат:
    var underage = _defaultPerson with { Age = 17};

Integration test

  • На цьому етапі тестуємо… інтеграцію. Інтеграцію модулів між собою, реєстацію залежностей, потік даних. В деяких випадках інтеграцію з базами, чергами.

  • Важливо, що на цьому етапі вже має бути протестована основна бізнес логіка і інтеграційні тести не повинні її дублювати. Дуже часто на Integration рівні достатньо пари тестів.

  • Важливим інструментом на етапі інтеграційного тестування є WebApplicationFactory. Він дозволяє запустити хост веб додатку з тесту. Без PowerShell, Docker-Compose і інших скриптів.
    Окрім просто запуску, важливою є можливість доналаштування хосту: зміна environment variables, реєстрація додаткових залежностей, або зміна існуючих. Наприклад у нас є модуль з капчою і щоб можна було якось потестити додаток, то цей модуль можна замінити на mock який завжди повертає позитивний результат. Ну або підмінити модуль по взаємодіє зі сороннім банківським сервісом.

E2E

  • Тут вже все по-дорослому. Треба запускати всі сервіси, бази, черги. Оскільки на попередніх етапах ми вже перевірили всю бізнес логіку, взаємодію між модулями, між компонентами, то залишається зовсім мало: конфігурації, потік даних… ну може ще щось.

  • Важливо, щоб тести запускались однією командою (dotnet test) або кнопкою в вашій IDE. Без додаткових налаштувань і конфігурацій, бо ніхто не буде читати мануали. Інструмент який в цьому допоможе - Testcontainers або FluentDocker. Це бібліотеки, що дають можливість управляти Docker контейнерами прямо з тесту.

  • Перед початком підняли всі залежності у вигляді окремих Docker контейнерів, або docker-compose. Прогнали E2E тести. Вимкнули контейнери.