Pull to refresh

Mocking private в JavaScript

JavaScript *
Sandbox

Проблема


Иногда нам необходимо протестировать скрытые (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.

Подводные камни


Конечно это решение не идеально. Например рефакторинг может запросто споткнуться об использование названий внутренних переменных и функций в строках да ещё и вне модуля. Это может серьёзно осложнить поддержку тестов.

Кроме того эту функцию нужно непременно удалять из финального кода, поскольку она полностью рушит все концепции безопасности кода.
Tags:
Hubs:
Total votes 22: ↑16 and ↓6 +10
Views 2.7K
Comments Comments 28