在Javascript中模拟window.location.href

81

我有一些针对使用window.location.href的函数编写的单元测试,这并不理想,我更希望将其作为参数传递,但在实现中这是不可能的。我只是想知道是否可以模拟这个值,而不会真正导致我的测试运行器页面跳转到该URL。

  window.location.href = "http://www.website.com?varName=foo";    
  expect(actions.paramToVar(test_Data)).toEqual("bar"); 

我正在使用Jasmine作为我的单元测试框架。


1
当我想在with语句内调用一个函数时,我遇到了同样的问题。你是如何解决的? - wizztjh
10个回答

58

最好的方法是创建一个辅助函数,然后对其进行模拟:

 var mynamespace = mynamespace || {};
    mynamespace.util = (function() {
      function getWindowLocationHRef() {
          return window.location.href;
      }
      return { 
        getWindowLocationHRef: getWindowLocationHRef
      }
    })();

现在,不要直接在您的代码中使用window.location.href,而是使用以下方法。然后,每当您需要返回模拟值时,您可以替换此方法:

mynamespace.util.getWindowLocationHRef = function() {
  return "http://mockhost/mockingpath" 
};
如果您想要窗口位置的特定部分(如查询字符串参数),则还应创建帮助程序方法并将解析过程留在主代码之外。一些框架(如jasmine)具有测试间谍功能,不仅可以模拟函数返回所需的值,还可以验证它是否被调用:
spyOn(mynamespace.util, 'getQueryStringParameterByName').andReturn("desc");
//...
expect(mynamespace.util.getQueryStringParameterByName).toHaveBeenCalledWith("sort");

23

我提出两个解决方案,这些方案在此前的帖子中已经被暗示过:

  • Create a function around the access, use that in your production code, and stub this with Jasmine in your tests:

    var actions = {
        getCurrentURL: function () {
            return window.location.href;
        },
        paramToVar: function (testData) {
            ...
            var url = getCurrentURL();
            ...
        }
    };
    // Test
    var urlSpy = spyOn(actions, "getCurrentURL").andReturn("http://my/fake?param");
    expect(actions.paramToVar(test_Data)).toEqual("bar");
    
  • Use a dependency injection and inject a fake in your test:

    var _actions = function (window) {
        return {
            paramToVar: function (testData) {
                ...
                var url = window.location.href;
                ...
            }
        };
    };
    var actions = _actions(window);
    // Test
    var fakeWindow = {
       location: { href: "http://my/fake?param" }
    };
    var fakeActions = _actions(fakeWindow);
    expect(fakeActions.paramToVar(test_Data)).toEqual("bar");
    

2
在 Jasmine 2.x 中,第一个示例中使用的语法似乎有些变化。 .andReturn() 应该被替换为 .and.returnValue() - Phil Gyford

10
你需要模拟本地上下文并创建你自己的版本windowwindow.location对象。
var localContext = {
    "window":{
        location:{
            href: "http://www.website.com?varName=foo"
        }
    }
}

// simulated context
with(localContext){
    console.log(window.location.href);
    // http://www.website.com?varName=foo
}

//actual context
console.log(window.location.href);
// http://www.actual.page.url/...
如果您使用 with,那么所有变量(包括 window!)首先将从上下文对象中查找,如果不存在,则从实际上下文中查找。

30
在严格模式下,不允许使用'with'语句。 - isherwood
看起来你不能用它编写可读的测试。我认为@Kurt Harriger提出的解决方案要好得多。-1 - Bastian Voigt
2
with(obj) {}语句已被弃用,在严格模式下无效。 - Alex
3
“with” 是什么意思? - Jim
“with”已经过时了。那个答案已经有10年的历史了,即使在那时,“with”也不再使用了。 - Andris

6

有时候,您可能有一个库会修改window.location,您想让它正常运行但也要进行测试。如果是这种情况,您可以使用闭包将您所需的引用传递给库,例如:

/* in mylib.js */
(function(view){
    view.location.href = "foo";
}(self || window));

在您的测试中,在包含库之前,您可以全局重新定义self,库将使用模拟self作为视图。

var self = {
   location: { href: location.href }
};

在您的库中,您还可以像以下这样做,因此您可以在测试的任何时候重新定义self:
/* in mylib.js */
var mylib = (function(href) {
    function go ( href ) {
       var view = self || window;
       view.location.href = href;
    }
    return {go: go}
}());

在现代浏览器中,如果不是全部都支持,self默认已经是一个指向window的引用。在实现Worker API的平台中,在Worker内部self是全局作用域的引用。在node.js中,既没有self也没有window定义,因此如果需要,可以这样做:

self || window || global

如果node.js确实实现了Worker API,这可能会发生变化。

4
以下是我采取的模拟window.location.href和/或任何可能存在于全局对象中的方法。
首先,不要直接访问它,将其封装在一个模块中,其中对象通过getter和setter进行保留。以下是我的示例。我正在使用require,但这里不是必需的。
define(["exports"], function(exports){

  var win = window;

  exports.getWindow = function(){
    return win;
  };

  exports.setWindow = function(x){
    win = x;
  }

});

现在,通常情况下您在代码中会使用 window.location.href 这样的语句,而现在您需要这样做:

var window = global_window.getWindow();
var hrefString = window.location.href;

最后设置完成后,您可以通过将窗口对象替换为您想要替代的虚假对象来测试您的代码。

fakeWindow = {
  location: {
    href: "http://google.com?x=y"
  }
}
w = require("helpers/global_window");
w.setWindow(fakeWindow);

这将改变窗口模块中的win变量。它最初设置为全局window对象,但现在设置为您放入的假窗口对象。因此,替换后,代码将获取您的虚拟窗口对象及其放置的虚拟href。

3
这类似于cpimhoff的建议,但是它使用Angular中的依赖注入。我想我会添加这个,以防其他人在寻找Angular解决方案时来到这里。
在模块中(可能是app.module),添加一个窗口提供程序,如下所示:
@NgModule({
...
  providers: [
    {
      provide: Window,
      useValue: window,
    },
  ],
...
})   

然后,在使用window的组件中,在构造函数中注入window。

constructor(private window: Window)

现在,在使用窗口时,请改用组件属性。
this.window.location.href = url

有了这个设置,您可以使用TestBed在Jasmine测试中设置提供程序。

    beforeEach(async () => {
      await TestBed.configureTestingModule({
        providers: [
          {
            provide: Window,
            useValue: {location: {href: ''}},
          },
        ],
      }).compileComponents();
    });

1
这对我有效:

delete window.location;
window.location = Object.create(window);
window.location.href = 'my-url';

1
仅适用于Jest,不适用于Karma(无法删除属性位置)。 - petronius
如果使用TS,您可以尝试这样做:delete (<any>window).location; - AngularBoy
1
在运行时也不起作用,最终我创建了一个私有方法来返回路径名,并使用 spyOn(component, "getLocationPathname").and.returnValue("/someroute"); 进行监视,同时加上了 // @ts-ignore 来忽略私有方法。 - petronius

0

在我看来,这个解决方案是对cburgmer的一种小改进,因为它允许你在源代码中将window.location.href替换为$window.location.href。尽管我使用的是Karma而不是Jasmine,但我相信这种方法对于两者都适用。同时我还添加了对sinon的依赖。

首先是一个服务/单例:

function setHref(theHref) {
    window.location.href = theHref;
}
function getHref(theHref) {
    return window.location.href;
}

var $$window = {
        location: {
            setHref: setHref,
            getHref: getHref,
            get href() {
                return this.getHref();
            },
            set href(v) {
                this.setHref(v);
            }
        }
    };
function windowInjectable() {  return $$window; }

现在我可以通过将windowInjectable()注入为$window来在代码中设置location.href,如下所示:

function($window) {
  $window.location.href = "http://www.website.com?varName=foo";
}

并在单元测试中模拟它,看起来像这样:

sinon.stub($window.location, 'setHref');  // this prevents the true window.location.href from being hit.
expect($window.location.setHref.args[0][0]).to.contain('varName=foo');
$window.location.setHref.restore();

获取器/设置器语法可以追溯到IE 9,并且根据https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set得到广泛支持。


0

这是我的通用解决方案,需要在生产代码中导入额外的内容,但不需要依赖注入或编写单独的包装函数,如getHref()

基本上,我们将窗口放入一个单独的文件中,然后我们的生产代码间接地从该文件中导入窗口。

在生产环境中,windowProxy === window。 在测试中,我们可以改变导出windowProxy的模块,并使用新的临时值进行模拟。

// windowProxy.js

/*
 * This file exists solely as proxied reference to the window object
 * so you can mock the window object during unit tests.
 */
export default window;

// prod/someCode.js
import windowProxy from 'path/to/windowProxy.js';

export function changeUrl() {
  windowProxy.location.href = 'https://coolsite.com';
}

// tests/someCode.spec.js
import { changeUrl } from '../prod/someCode.js';
import * as windowProxy from '../prod/path/to/windowProxy.js';

describe('changeUrl', () => {
  let mockWindow;
  beforeEach(() => {
    mockWindow = {};
    windowProxy.default = myMockWindow;
  });

  afterEach(() => {
    windowProxy.default = window;
  });

  it('changes the url', () => {
    changeUrl();
    expect(mockWindow.location.href).toEqual('https://coolsite.com');
  });
});

-1
你需要在保持在同一页面的同时伪造window.location.href。在我的情况下,这个片段完美地工作。
$window.history.push(null, null, 'http://server/#/YOUR_ROUTE');
$location.$$absUrl = $window.location.href;
$location.replace();

// now, $location.path() will return YOUR_ROUTE even if there's no such route

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接