@KaminskyIlya

Как выполнить Dependency Injection локальных переменных?

Всем добрый день!

Предположим, что у нас есть такая "библиотечная" функция:
// Возвращает полный возраст человека на текущий день
public static int getYearsOld(Date birthday)
{
    // Эту переменную необходимо мокировать
    Date today = new Date();

    return getYear(today) - getYear(birthday);
}

Нужно написать для нее тест. Кажется, что это сделать легко:
@Test
public void testGetYearsOld()
{
    assertEquals( 23, getYearsOld(new SimpleDateFormat("dd-MM-yyyy").parse("15-07-1991")) );
}

Но на самом деле, такой тест после отладки даст сбой уже через несколько месяцев.
Чтобы этого не произошло, мы должны динамически менять либо ожидаемую константу (23), либо дату рождения ("15-07-1991"). При этом мы явно/неявно воспроизведем тестируемый алгоритм и можем допустить ошибку дважды: в реализации и в тесте. Тем более, если тест писал один и тот же программист.

Было бы не плохо, если бы нам удалось в тело функции "подсовывать" нужную дату. Иными словами, мы нуждаемся в инъекции зависимости для локальной переменной.

Обычно в таких ситуациях предлагают прибегнуть к IoC и DI следующим образом: либо передавать переменную через аргумент самой функции, либо локальную переменную превратить в поле класса. А в вопросе, как инициализировать эту переменную, далее идут варианты: устанавливать сеттером, в конструкторе, через рефлешн.

Но такой подход к реализации функции вызывает некоторые проблемы. Получается, чтобы только провести тест я должен:
1. Повысить уровень локальной переменной, тем самым сделав ее доступной всем членам класса. А если эта переменная больше нигде не используется? Зачем тогда были сделаны вообще локальные переменные? А если функция вообще статическая?
2. Нарушить инкапсуляцию и раскрыть детали реализации функции, переместив ее "кишки" на уровень выше, сделав доступной всем в классе.
3. Сохранять значение локальной переменной, даже после того, как она уже не нужна. А не нужна она становится сразу же после выхода из функции. Т.е. сборщик мусора не освободит занятую ею память до тех пор, пока существует экземпляр класса. А если это синглтон, живущий, пока живет приложение?
4. В случае добавления сеттера, конструктора или состава аргумента функции - еще и изменить публичный API.
И остается вопрос: что делать еще с over9000 таких же локальных переменных, используемых в других функциях. Все выносить в поля? А если у них одинаковые имена? Переиспользовать их? А как же многопоточность?
В общем, такие подходы на проверку оказываются, мягко скажем, не очень.

Есть, правда, еще один похожий вариант: передача классу специальной фабрики, порождающей значения для локальных переменных. Но как то это не то...

Ах, если бы можно было писать так:
public static int getYearsOld(Date birthday)
{
    @InjectForTest(name = "Date1")
    Date today = new Date();

    return getYear(today) - getYear(birthday);
}

@Test
public void testGetYearsOld()
{
    MockContext ctx = getContext();
    ctx.put( "Date1", new Date() );

    assertEquals( ..., getYearsOld(...) );
}


Может кто знает иные варианты? Или я вообще к решению подошел не с той стороны?
Хочу предостеречь, что речь идет не о конкретном приведенном примере, а вообще. У функции может быть много таких локальных переменных (хотя при этом она будет не более 10-15 строк - т.е. применить рефакторинг тип "выделить метод" не получится).
  • Вопрос задан
  • 2439 просмотров
Решения вопроса 1
делаем mock для класса Date и всё. пример можно глянуть тут
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы