在Angular2(TypeScript)中进行单元测试/模拟窗口属性

29

我正在为Angular2中的服务编写一些单元测试。

在我的服务中,我有以下代码:

var hash: string; hash = this.window.location.hash;

然而,当我运行包含此代码的测试时,它会失败。

我希望能够利用Window的所有功能,但由于我正在使用PhantomJs,我认为这不可能(我也尝试过Chrome,结果相同)。

在AngularJs中,我会试图模拟$Window(或者至少是相关属性),但是由于Angular2单元测试文档不多,所以我不确定该怎么做。

有人可以帮帮我吗?


这似乎相当简单明了。可能是一个XY问题,因为路由器已经将哈希抽象化了,这种抽象化一直到DOM位置 - Estus Flask
5个回答

35

在Angular 2中,您可以使用@Inject()函数通过使用字符串令牌将窗口对象命名来进行注入,如下所示:

在Angular 2中,您可以使用@Inject()函数通过使用字符串令牌将窗口对象命名来进行注入,如下所示

  constructor( @Inject('Window') private window: Window) { }

@NgModule中,您必须使用相同的字符串提供它:

@NgModule({
    declarations: [ ... ],
    imports: [ ... ],
    providers: [ { provide: 'Window', useValue: window } ],
})
export class AppModule {
}

然后您也可以使用令牌字符串进行模拟

beforeEach(() => {
  let windowMock: Window = <any>{ };
  TestBed.configureTestingModule({
    providers: [
      ApiUriService,
      { provide: 'Window', useFactory: (() => { return windowMock; }) }
    ]
  });

这在Angular 2.1.1中有效,截至2016-10-28是最新版本。

在Angular 4.0.0 AOT中不起作用。 https://github.com/angular/angular/issues/15640


这在测试时可以工作,但在AoT编译中不行(虽然可以编译,但会有警告并且应用程序在浏览器中崩溃)。 - Dunos
尝试使用AoT构建(同时在构造函数中将window类型定义为any以解决另一个错误),但是当我尝试访问在另一个文件中设置的window自定义属性时,我的生产构建崩溃了。我改为按照这里的解决方案操作,它起作用了:https://dev59.com/01sX5IYBdhLWcg3wJcqW#37176929 - Timespace
这是在每个测试中动态更改windowMock对象时的正确答案。 - EugenSunic
它对我有效:Angular 10 - Joand

9

正如@estus在评论中提到的那样,最好从路由器中获取哈希值。但是为了直接回答您的问题,您需要将window注入到使用它的位置,以便在测试期间可以模拟它。

首先,在angular2提供程序中注册window - 如果您在许多地方都使用它,则可能是全局的某个位置:

import { provide } from '@angular/core';
provide(Window, { useValue: window });

这段代码告诉 Angular 当依赖注入询问类型Window时,它应该返回全局 window。现在,在你使用它的地方,你要将它注入到你的类中,而不是直接使用全局变量。
import { Component } from '@angular/core';

@Component({ ... })
export default class MyCoolComponent {
    constructor (
        window: Window
    ) {}

    public myCoolFunction () {
        let hash: string;
        hash = this.window.location.hash;
    }
}

现在你已经准备好在测试中模拟该值了。
import {
    beforeEach,
    beforeEachProviders,
    describe,
    expect,
    it,
    inject,
    injectAsync
} from 'angular2/testing';

let myMockWindow: Window;
beforeEachProviders(() => [
    //Probably mock your thing a bit better than this..
    myMockWindow = <any> { location: <any> { hash: 'WAOW-MOCK-HASH' }};
    provide(Window, {useValue: myMockWindow})
]);

it('should do the things', () => {
    let mockHash = myMockWindow.location.hash;
    //...
});

1
你也可以直接注入 constructor(window:Window) 并像这样提供它 provide(Window, {useValue: window}) 或者 provide(Window, {useClass: MyWindowMock})。如果有可用的类型,则无需使用字符串键。 - Günter Zöchbauer
2
从核心中删除了provide,将其更改为providers: [ {provide: Window, useValue: window}, ] - Anand Rockzz
这真是救命稻草。让我能够在不重定向页面的情况下模拟所需的测试。适用于Angular 11。 - Cuga
类型错误:无法设置未定义的属性(设置“href”)- 当尝试 this.window.location.href = 'abc' 时。 - java-addict301

5
在RC4方法之后,provide()已被弃用,因此处理RC4后的方法为:
  let myMockWindow: Window;

  beforeEach(() => {
    myMockWindow = <any> { location: <any> {hash: 'WAOW-MOCK-HASH'}};
    addProviders([SomeService, {provide: Window, useValue: myMockWindow}]);
  });

我花了一些时间才搞明白它是如何工作的。


1
该语法在Angular2的发布版本中已经被弃用。 - Klas Mellbourn

2

使用内置工厂的注入令牌似乎是不错的选择。

我将这些用于任何浏览器全局对象,例如window、document、localStorage、console等。

core/providers/window.provider.ts

import { InjectionToken } from '@angular/core';

export const WINDOW = new InjectionToken<Window>(
    'Window',
    {
        providedIn: 'root',
        factory(): Window {
            return window;
        }
    }
);

注入攻击:

constructor(@Inject(WINDOW) private window: Window)

单元测试:

const mockWindow = {
  setTimeout: jest.fn(),
  clearTImeout: jest.fn()
};

TestBed.configureTestingModule({
  providers: [
    {
      provide: WINDOW,
      useValue: mockWindow
    }
  ]
});

这对我来说是一个相当确定的答案。我将我的注入令牌放入了app.module文件中,然后只需在需要设置位置href的所有组件中导入/注入它即可。 - Ben Thomson

0

我真的不明白为什么没有人提供最简单的解决方案,这是 Angular 团队推荐的测试服务的方法,你可以在 这里 看到。在大多数情况下,你甚至不需要处理 TestBed 的东西。

此外,你也可以将这种方法用于组件和指令。在这种情况下,你不会创建一个组件实例,而是创建一个类实例。这意味着你也不必处理组件模板中使用的子组件。

假设你能够将 Window 注入到你的构造函数中

constructor(@Inject(WINDOW_TOKEN) private _window: Window) {}

只需在您的 .spec 文件中执行以下操作:

describe('YourService', () => {
  let service: YourService;
  
  beforeEach(() => {
    service = new YourService(
      {
        location: {hash: 'YourHash'} as any,
        ...
      } as any,
      ...
    );
  });
}

我不关心其他属性,因此我通常会将其类型转换为any。如有需要,您可以自由地包括所有其他属性并适当地进行类型设置。

如果您需要在模拟属性上使用不同的值,您可以简单地对它们进行监视,并使用jasmine的returnValue更改该值:

const spy: any = spyOn((service as any)._window, 'location').and.returnValue({hash: 'AnotherHash'});

或者

const spy: any = spyOn((service as any)._window.location, 'hash').and.returnValue('AnotherHash');

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