Проблема
Иногда нам необходимо протестировать скрытые (closure) функции или интерфейс, изолировав использование скрытых функций. Также порой требуется задать исходное состояние скрытых переменных или наоборот считать их состояние после теста. В этом случае мы строим дополнительный набор функций, обеспечивающий доступ внутрь объекта. Часто этот набор бывает очень громоздким и направлен только для обеспечения тестируемости модуля.
Но есть простой способ избежать написания этого кода.
Лирическое отступление
Я новичек в JavaScript и в Unit Tests. Возможно это решение уже используется вовсю, но я нашёл его сам и спешу поделиться с другими.
Решение
Я выяснил, что широко известная (и ненавистная многим) функция
eval()
исполняет заданный код в лексичеком контексте её вызова.Что же это нам дает?
Вместо сложного API мы можем добавить всего лишь одну функцию, которая выполнит всю работу.
this.evalInContext = function(cmd){eval(cmd);}
Пример
Объект может выглядеть следующим образом:
function factory(params){
var outerVar;
function outerFunc(){}
function MyClass(params2){
var innerVar;
function innerFunc(){}
this.instanceVar = 0;
this.instanceFunc(){}
this.evalInContext = function(cmd){eval(cmd);}
}
return new Myclass(params);
}
При таком использовании все аргументы, а также внешние, внутренные и объектные функции и переменные будут дoступны коду, переданному функции
eval ()
.Использование
var myObj = factory(); // create an object
myObj.evalInContext("outerVar=10;innerVar='zzz';this.instanceVar=new Date();"); //set private and instance variables
myObj.evalInContext("innerFunc();outerFunc(111);"); // call functions
Расширения
Чтобы установить простые значения в переменных или вызвать функцию с простыми парамтрами можно использовать прямой вызов
evalInContext()
. Но что делать если нам необходимо работать с более сложными объектами.Я создал несколько простых функций для расширения базовой функциональности
evalInContext()
.Установка и получение сложных переменных
Так выглядит setter:
function setVar(obj, name, value){
obj.evalInContext("name + " = arguments[1]", value);
}
Мы передаем значение в виде параметра функции
evalInContext()
и используем arguments
для присвоения данных. Таким образом можно передать любой тип данных.Getter работает похожим образом, но использует другой простой трюк:
function getVar(obj, name){
var res = {};
obj.evalInContext("arguments[1]['" + name + "'] = " + name + ";", res);
return res[name];
}
Мы передаем пустой объект в качестве контейнера для возвращаемого значения, поскольку
return
не может быть использован в функции eval()
.UPD: В комментариях предложили более интересный вариант. И по мне более правильный.
Замещение функции (mocking)
Поскольку имя функции является переменной, мы можем использовать уже имеющийся функционал:
function replaceFunction(obj, name, replacement) {
var orig = getVar(obj, name);
setVar(obj, name, replacement);
return orig;
}
Вызов функции со сложными параметрами
Мы могли бы сделать обертку для вызова функции, но проще получить саму функцию и вызывать её привычным способом. Кроме того получив её один раз можно вызывать сколько душе угодно.
Для получения функции можно использовать уже имеющийся в наличии getter.
Подводные камни
Конечно это решение не идеально. Например рефакторинг может запросто споткнуться об использование названий внутренних переменных и функций в строках да ещё и вне модуля. Это может серьёзно осложнить поддержку тестов.
Кроме того эту функцию нужно непременно удалять из финального кода, поскольку она полностью рушит все концепции безопасности кода.