Pull to refresh

Mocking private в JavaScript

Reading time 3 min
Views 3K

Проблема


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

Articles