Як поводитися зі статичними класами корисності при проектуванні для тестабельності


62

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

У різних місцях нам необхідно використовувати статичні допоміжні методи, такі як ImageIO та URLEncoder (обидва стандартні Java API) та різні інші бібліотеки, що складаються здебільшого статичних методів (наприклад, бібліотеки Apache Commons). Але надзвичайно важко перевірити ті методи, які використовують такі статичні класи помічників.

У мене є кілька ідей для вирішення цієї проблеми:

  1. Використовуйте макетну рамку, яка може глузувати зі статичних класів (наприклад, PowerMock). Це може бути найпростішим рішенням, але якимось чином відчуваю, що здається.
  2. Створіть миттєві класи обгортки навколо всіх цих статичних утиліт, щоб їх можна було вводити в класи, які ними користуються. Це звучить як відносно чисте рішення, але я боюся, що ми в кінцевому підсумку створимо дуже багато цих класів з обгортки.
  3. Витягуйте кожен виклик цих статичних допоміжних класів у функцію, яку можна переосмислити і протестувати підклас класу, який я насправді хочу перевірити.

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

Яка найкраща стратегія зберігати класи, які використовують ці статичні помічники?


Я не впевнений, що ви маєте на увазі під "достовірними та / або офіційними джерелами", але я згоден з тим, що @berecursive написав у своїй відповіді. PowerMock існує з причини, і це не повинно відчувати себе "відмовою", особливо якщо ви не хочете писати курси обгортки самостійно. Остаточні та статичні методи - це біль, коли мова йде про одиничне тестування (і TDD). Особисто? Я використовую метод 2, який ви описали.
Деко

"достовірні та / або офіційні джерела" - лише один із варіантів, який ви можете обрати, починаючи виграш за питання. Що я насправді маю на увазі: досвід чи посилання на статті, написані експертами TDD. Або будь-який досвід того, хто стикався з тією ж проблемою ...

Відповіді:


34

(Боюсь, офіційних джерел тут немає, боюсь, це не так, як є специфікація того, як добре перевірити. Просто моя думка, яка, сподіваюся, буде корисною.)

Коли ці статичні методи представляють справжні залежності, створіть обгортки. Тож для таких речей, як:

  • ImageIO
  • HTTP-клієнти (або що-небудь ще, що стосується мережі)
  • Файлова система
  • Отримання поточного часу (мій улюблений приклад того, де допомагає введення залежності)

... має сенс створити інтерфейс.

Але з багатьох методів Apache Commons, мабуть, не слід глузувати / підробляти. Наприклад, візьміть метод з'єднання списку рядків, додавши коску між ними. Немає сенсу знущатися над цим - просто нехай статичний дзвінок виконує свою нормальну роботу. Не потрібно або потрібно замінювати нормальну поведінку; ви не маєте справу з зовнішнім ресурсом або з чимось важким для роботи, це лише дані. Результат передбачуваний, і ви ніколи не хочете, щоб це було інакше, ніж те, що воно вам все одно дасть.

Я підозрюю, що, видаливши всі статичні дзвінки, які справді є зручними методами, передбачувані, "чисті" результати (наприклад, base64 або кодування URL), а не точки введення у цілий великий безлад логічних залежностей (наприклад, HTTP), ви побачите, що це цілком практично робити правильно з справжніми залежностями.


20

Це, безумовно, сумнівне питання / відповідь, але для того, що варто, я подумав, що я кину два мої центи. З точки зору методу TDD-стилю 2, безумовно, є підхід, який слідує за ним до листа. Аргумент методу 2 полягає в тому, що якщо ви коли-небудь хотіли замінити реалізацію одного з цих класів - скажімо, ImageIOеквівалентну бібліотеку - тоді ви могли б це зробити, зберігаючи довіру до класів, які використовують цей код.

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

Сказавши це, PowerMock існує не просто так. Це досить відома проблема, що тестування, коли задіяні статичні методи, є серйозно болючим, отже, і створенням PowerMock. Я думаю, що вам потрібно зважити свої варіанти з точки зору того, скільки роботи буде, щоб обернути всі ваші класи помічників, а не використовувати PowerMock. Я не думаю, що «відмовитися» від використання PowerMock - я просто відчуваю, що загортання класів дає вам більше гнучкості у великому проекті. Чим більше публічних контрактів (інтерфейсів) ви можете забезпечити більш чистому поділ між наміром та реалізацією.


1
Додатковий випуск, в якому я не впевнений: якщо ви реалізуєте обгортки, чи застосовуватимете б ви всі методи класу, що обертається, або лише ті, які зараз потрібні?

3
Дотримуючись спритних ідей, ви повинні робити найпростішу справу, що працює, і уникати роботи, яка вам не потрібна. Тому вам слід викрити лише ті методи, які вам справді потрібні.
Ассаф Стоун

@AssafStone погодився

Будьте обережні з PowerMock, всі маніпуляції класу, які він має зробити, щоб знущатися над методами, мають великі накладні витрати. Ваші тести будуть набагато повільнішими, якщо ви їх широко використовуєте.
bcarlso

Чи справді вам доведеться багато писати на обгортці, якщо ваше тестування / міграція поєднується з прийняттям бібліотеки DI / IoC?

4

Як орієнтир для всіх, хто також має справу з цією проблемою і натрапив на це питання, я опишу, як ми вирішили вирішити цю проблему:

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

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



1

Я працюю у великій страховій компанії, і наш вихідний код складає до 400 МБ чистих файлів Java. Ми розробляли всю програму, не думаючи про TDD. З січня цього року ми розпочали тестування з джуніту для кожного окремого компонента.

Найкращим рішенням нашого відділу було використання об'єктів Mock у деяких методах JNI, які залежали від системи (написані на С), і як такий, ви не могли точно оцінювати результати щоразу на кожній ОС. У нас не було іншого варіанту, ніж використовувати насмішковані класи та конкретні реалізації методів JNI спеціально для тестування кожного окремого модуля програми для кожної ОС, яку ми підтримуємо.

Але це було дуже швидко, і він працює досить добре до цих пір. Рекомендую - http://www.easymock.org/


1

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

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


1

Іноді я використовую варіант 4

  1. Використовуйте шаблон стратегії. Створіть клас утиліти зі статичними методами, які делегують реалізацію до примірника інтерфейсу, що підключається. Зашифруйте статичний ініціалізатор, який підключається до конкретної реалізації. Підключіть макетну програму для тестування.

Щось на зразок цього.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

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

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.