03 June 2008
Зміна в коді вбила 30% тестів!
У нас в компанії є цікава фішка, це неформальні побрехеньки ;), і все це з крутим ім'ям TechTalks. В більшості випадків ми або слухаємо когось, або дискутуємо. Дискусія попереднього тижня була призначена Unit Testінгу. Одним з цікавих питань був subj. Наразі саме про це я хочу і розповісти.
Деякі з можливих ситуацій.
Зміна в структурі даних.
Наприклад, зміна назви колонки. Тут можна сказати тільки те що це вада як дизайна, так і кодінгу. Дизайну, тому що не обміркували такі зміни, а кодінг, тому що, це достаньо просто зробити типізовано.
Висновки.
- Все повинно бути типізованим та інкапсульованим (використання типізованого DataSet не для отримання даних, це одразу баг, тому що не ви, так хтось інший спробує з логіки отримати данні через DatRow).
- На час дизайну потрібно міркувати про такі зміни. Краще написати більше коду, але зарадити таким проблем, фікси таких проблем на багато дорожчі за 30 рядків кода.
Зміна в інфраструктурі.
Наприклад, зміна кода відтворення залежностей (IoC контейнер чи щось таке), або зміна в системі логгінгу. В контексті цих проблем потрібно нагадати що тест не повинен залежати від будь чого.
Наведу приклад з логгінгом. Наприклад ми зробили логгінг сінглтоном і завжди його використовуємо. Так тести починають писати усіляку фігню в логи. Зрозуміло що можна переконфігурувати логгінг так щоб він писав в інше місце. Але якщо припустити що проект великий то підтримка таких конфігурацій це ще той головняк. Для цього є набагато зручніше рішення, це Dependecy Inversion, нажаль сьогодні я про це розповісти не зможу бо це достатньо велика тема. Коротко це можливість змінювати реалізацію, щось на кшталт паттерну Strategy. Так за допомогою DI достатньо зробити Mock реалізацію, наприклад, NullLogger для того щоб відв'язати тести від налаштувань та і всього іншого миру.
Висновки.
- Тести не повинні залежати від інфраструктури. Для цього дуже добре підходить принцип Dependency Inversion (та його конкретні реалізації паттерни Dependecy Injection (Inversion Of Control) та Service Locator).
Об'єкти зі спільною логікою, Utility, Helper об'єкти.
Іноді багато логіки виносять в Utility класи, часто це пояснюється рефакторінгом для зменшення дублювання коду. Вбивав би... Так це рефакторінг, але ж є ООП, там усілякі інкапсуляції, тощо...
Такі класи призводять до дуже непередбачуваних результатів, в процесі розробки в ці методи додається купа логіки, комплексність збільшується, і.. так 30% відсотків тестів вбито, тому що ми додали додаткове очікування (expectation). Приклад таких очікувань це те що я отримаю список кастомерів, а з учора цей клас повертає тільки кастомерів, які промарковані як активні.
Як на мене то найбільш прийнятним рішенням буде рефакторінг в окремі класи (наприклад XxxxCalculatior, XxxxWorker, XxxxxProcessor, тощо) а потім же тестування цих класів. З точки зору архітектури іноді це навіть потрібно робити, адже якщо у вас є розрахунок вартості, то ValueCalculator, буде набагато зрозумілішим, за метод CalculateValue(). Як додатковий бенефіт це спрощення методів, адже є можливість створювати поля, а не пропихувати данні через усі методи.
Висновки.
- Якщо в вас впало 30% відсотків тестів, і ви побачили ці симптоми, то так вам і потрібно. Рефакторінг вам в руки. Зробіть методи такими щоб вони були unambiguous. Неможливо? Зробіть класи.
- Виносити логіку в окремі класи. XxxxCalculatior, XxxxWorker, XxxxxProcessor. Інодні постає проблема "публічності" таких класів. Є декілька рішень. Наприклад internal, або додатковий неймспейс (impl, Implmentation, тощо), або інша збірка.
Тестування приватних методів
Приклад, є клас, в ньому є приватний метод, через публічний інтерфейс до нього дуже складно добратись. Можливо потрібно якась супер пупер комбінація параметрів, яку складно зробити, чи щось з оточення, наприклад метод працює тільки коли OutOfMemoryException...
Знаходимо шлях як викликати ці приватні методи. Тут вам і рефлексія, і враппери які будує студія, і спеціальні публічні методи яки будуть викликати приватні методи... Тестуємо ці методи як завжди. (Більшої ахінеї я ще не писав (але я вчусь ;))... )
Перед тим як щось тестувати потрібно розуміти що окрім покриття коду, є ще здоровий глузд. Якщо метод(або частина методу) ніколи не буде викликано, то постає питання не про тестувати чи ні, а про потрібен чи не потрібен цей метод.
З комбінацією параметрів, дещо складніше, рішення це рефакторінг реалізації в окремі класи, та тестування окремих класів.
Висновки
- Якщо приватний метод неможливо викликати з публічного інтерфейсу, краще його видалити. Не залишайте метод на майбутнє, на мож знадобиться.
- Якщо до приватного метода складно дістатися, рефакрінг в окремий клас.
Супер метод (SuperMethod GOF).
Це такий паттерн розробки, коли у вас є один метод який робить все! Це може бути або метод на 2Мб, або "краще зроблений" метод який викликає 300 приватних методів. Чомусь в 90% випадків цей метод приймає в параметрах DataSet...
При всіх бенефітах (читайте GOF) таких методів у них є велика вада. Для того щоб протестувати такий метод потрібно десь 300 юніт тестів. Зрозуміло що змінивши щось в такому методі ми достатньо легко отримаємо 30% вбитих тестів...
Як завжди це вада дизайну. Адже в більшості випадків такий метод порушує Single Responsibility Principle, а отже чи не будь яка зміна буде мати сайд ефекти на більше аніж одну Resposibility.
Висновки
- Якщо у вас такий метод є, то тут допоможе тільки рефакторінг. Декомпозуйте! Робіть все так щоб воно дозволяло сказати що ви не порушуєте Single Responsibility Principle.
- Давайте своїм тестам імена по яким можна зрозуміти що вони тестують. А потім подивіться чи вдасться описати назвою методу тест для такого монстра?
Глобальні Висновки
- Розробляйте не порушуючи Single Responsibility Principle, використовуйте декомпозицію;
- Використовуйте Dependency Inversion, щоб відокремити залежності;
- Не бійтеся коли 30% тестів впало. Це ж добре. Уявіть що у вас немає цих тестів, і новини про проблему ви отримаєте від тестерів, а ще гірше від користувачів.
- Не бійтеся проблем, питайте, в більшості випадків проблеми вже вирішені.
- Розробляйте з "testing in mind". Навіть якщо поки що ви не плануєте Unit Tests. Спробуйте уявити, чи легко буде ваш функціонал викликати, чи не потрібно тонни усіляких налаштувань?
Вчу українську, багато працюю. Цікавлюсь моделюванням небезпек. Більшість часу витрачаю на .Net.