Всем добрый день!
Предположим, что у нас есть такая "библиотечная" функция:
// Возвращает полный возраст человека на текущий день
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 строк - т.е. применить рефакторинг тип "выделить метод" не получится).