解讀Jasmine的Spy機制

衆所周知,Angular所用的單元測試框架是Karma+Jasmine,最近在寫Angular的Unit Test的時候,在Given「建立測試條件」部分會在不少地方用到Spy去模擬和監測函數調用,而jasmine爲咱們提供的關於Spy的函數有不少種,好比createSpyObj,createSpy,SpyOn等等,而這些方法命名類似可是用法卻不相同,經常讓人容易混淆而產生不少錯誤,下面就經過研讀Jasmine關於Spy的源碼來弄清楚這些Spy函數究竟是幹什麼的,在什麼場合下使用它們。
先從createSpyObj開始研究:node

j$.createSpyObj = function(baseName, methodNames) {
    var baseNameIsCollection = j$.isObject_(baseName) || j$.isArray_(baseName);

    if (baseNameIsCollection && j$.util.isUndefined(methodNames)) {
      methodNames = baseName;
      baseName = 'unknown';
    }

    var obj = {};
    var spiesWereSet = false;

    //若是參數2是method的數組,則調用createSpy(base.method)
    if (j$.isArray_(methodNames)) {
      for (var i = 0; i < methodNames.length; i++) {
        obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]);
        spiesWereSet = true;
      }
    }
    //若是參數2是method:returnValue的鍵值對組成的對象,則除了調用createSpy(base.method),還用「and.returnValue」來定義了方法的返回值
     else if (j$.isObject_(methodNames)) {
      for (var key in methodNames) {
        if (methodNames.hasOwnProperty(key)) {
          obj[key] = j$.createSpy(baseName + '.' + key);
          obj[key].and.returnValue(methodNames[key]);
          spiesWereSet = true;
        }
      }
    }

    if (!spiesWereSet) {
      throw 'createSpyObj requires a non-empty array or object of method names to create spies for';
    }

    return obj;
  };
};

再來看SpyOn:數組

this.spyOn = function(obj, methodName) {
    //開始是一連串的錯誤處理,這些錯誤是在寫UT的時候常常出現的錯誤,能夠對號入座
      if (j$.util.isUndefined(obj) || obj === null) {
        throw new Error(getErrorMsg('could not find an object to spy upon for ' + methodName + '()'));
      }

      if (j$.util.isUndefined(methodName) || methodName === null) {
        throw new Error(getErrorMsg('No method name supplied'));
      }

      if (j$.util.isUndefined(obj[methodName])) {
        throw new Error(getErrorMsg(methodName + '() method does not exist'));
      }

      if (obj[methodName] && j$.isSpy(obj[methodName])  ) {
        if ( !!this.respy ){
          return obj[methodName];
        }else {
          throw new Error(getErrorMsg(methodName + ' has already been spied upon'));
        }
      }

      var descriptor;
      try {
        descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
      } catch(e) {
        // IE 8 doesn't support `definePropery` on non-DOM nodes
      }

      if (descriptor && !(descriptor.writable || descriptor.set)) {
        throw new Error(getErrorMsg(methodName + ' is not declared writable or has no setter'));
      }

      var originalMethod = obj[methodName],
      //這裏調用了createSpy,createSpy的param1是這個Spy的名字,意義不大;param2是要去Spy的函數
        spiedMethod = j$.createSpy(methodName, originalMethod),
        restoreStrategy;

      if (Object.prototype.hasOwnProperty.call(obj, methodName)) {
        restoreStrategy = function() {
          obj[methodName] = originalMethod;
        };
      } else {
        restoreStrategy = function() {
          if (!delete obj[methodName]) {
            obj[methodName] = originalMethod;
          }
        };
      }

      currentSpies().push({
        restoreObjectToOriginalState: restoreStrategy
      });

      obj[methodName] = spiedMethod;

      return spiedMethod;
    };

再來看一下createSpyObj和spyOn共同用到的方法createSpy(),也能夠單獨調用:app

j$.createSpy = function(name, originalFn) {
    return j$.Spy(name, originalFn);
  };

很簡單,就是調用了j$.Spy這個方法,
繼續看最底層的Spy():框架

function Spy(name, originalFn) {
    var numArgs = (typeof originalFn === 'function' ? originalFn.length : 0),
      //作了一個包裝函數,做爲虛擬調用
      wrapper = makeFunc(numArgs, function () {
        return spy.apply(this, Array.prototype.slice.call(arguments));
      }),
      //Spy策略:處理Spy的and屬性:callThrough執行調用, returnValue指定返回值, callFake執行指定函數,throwError拋出異常,stub原始狀態
      spyStrategy = new j$.SpyStrategy({
        name: name,
        fn: originalFn,
        getSpy: function () {
          return wrapper;
        }
      }),
      
      callTracker = new j$.CallTracker(),
      spy = function () {
        /**
         * @name Spy.callData
         * @property {object} object - `this` context for the invocation.
         * @property {number} invocationOrder - Order of the invocation.
         * @property {Array} args - The arguments passed for this invocation.
         */
        var callData = {
          object: this,
          invocationOrder: nextOrder(),
          args: Array.prototype.slice.apply(arguments)
        };

        callTracker.track(callData);
        var returnValue = spyStrategy.exec.apply(this, arguments);
        callData.returnValue = returnValue;

        return returnValue;
      };

    function makeFunc(length, fn) {
      switch (length) {
        case 1 : return function (a) { return fn.apply(this, arguments); };
        case 2 : return function (a,b) { return fn.apply(this, arguments); };
        case 3 : return function (a,b,c) { return fn.apply(this, arguments); };
        case 4 : return function (a,b,c,d) { return fn.apply(this, arguments); };
        case 5 : return function (a,b,c,d,e) { return fn.apply(this, arguments); };
        case 6 : return function (a,b,c,d,e,f) { return fn.apply(this, arguments); };
        case 7 : return function (a,b,c,d,e,f,g) { return fn.apply(this, arguments); };
        case 8 : return function (a,b,c,d,e,f,g,h) { return fn.apply(this, arguments); };
        case 9 : return function (a,b,c,d,e,f,g,h,i) { return fn.apply(this, arguments); };
        default : return function () { return fn.apply(this, arguments); };
      }
    }

    for (var prop in originalFn) {
      if (prop === 'and' || prop === 'calls') {
        throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon');
      }

      wrapper[prop] = originalFn[prop];
    }

    wrapper.and = spyStrategy;
    wrapper.calls = callTracker;

    return wrapper;
  }

  return Spy;
};

由此能夠獲得,createSpyObj、createSpy、SpyOn、Spy這幾個方法的調用關係:
圖片描述函數

它們適用的場合如圖所示:
圖片描述單元測試

解釋:
createSpyObj:本來沒有對象,無中生有地去建立一個對象,而且在對象上建立方法,而後去spy上面的方法
spyOn:本來有對象,對象上也有方法,只是純粹地在方法上加個spy
createSpy:本來有對象,可是沒有相應的方法,虛擬地建立一個方法(虛線),在虛擬的方法上去spy。若是對象上原來有方法,也能夠用createSpy去spy,也就是不管有沒有這個方法,createSpy都會去spy你指定的方法。測試


常見的出錯信息:
基本上出錯的信息都是在spyOn函數上,摘錄出來以備查找緣由:ui

  1. 'could not find an object to spy upon for ' + methodName + '()'

spy的對象爲null或undefinedthis

  1. 'No method name supplied’

spy的方法爲null或undefinedspa

  1. methodName + '() method does not exist'

spy的方法不存在對象上(spyOn必需要在存在的方法上去spy)

  1. methodName + ' has already been spied upon'

已經有一個spy在這個方法上了,看看有沒有地方已經spy了它

相關文章
相關標籤/搜索