简体   繁体   English

在Angular 2 ngOnInit中测试承诺

[英]Testing promise in Angular 2 ngOnInit

I have an Angular 2 component I am trying to put under test, but I am having trouble because the data is set in the ngOnInit function, so is not immediately available in the unit test. 我有一个Angular 2组件我试图进行测试,但是我遇到了麻烦,因为数据是在ngOnInit函数中设置的,因此在单元测试中不能立即使用。

user-view.component.ts: 用户view.component.ts:

import {Component, OnInit} from 'angular2/core';
import {RouteParams} from 'angular2/router';

import {User} from './user';
import {UserService} from './user.service';

@Component({
  selector: 'user-view',
  templateUrl: './components/users/view.html'
})
export class UserViewComponent implements OnInit {
  public user: User;

  constructor(
    private _routeParams: RouteParams,
    private _userService: UserService
  ) {}

  ngOnInit() {
    const id: number = parseInt(this._routeParams.get('id'));

    this._userService
      .getUser(id)
      .then(user => {
        console.info(user);
        this.user = user;
      });
  }
}

user.service.ts: user.service.ts:

import {Injectable} from 'angular2/core';

// mock-users is a static JS array
import {users} from './mock-users';
import {User} from './user';

@Injectable()
export class UserService {
  getUsers() : Promise<User[]> {
    return Promise.resolve(users);
  }

  getUser(id: number) : Promise<User> {
    return Promise.resolve(users[id]);
  }
}

user-view.component.spec.ts: 用户view.component.spec.ts:

import {
  beforeEachProviders,
  describe,
  expect,
  it,
  injectAsync,
  TestComponentBuilder
} from 'angular2/testing';
import {provide} from 'angular2/core';
import {RouteParams} from 'angular2/router';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';

import {UserViewComponent} from './user-view.component';

import {UserService} from './user.service';

export function main() {
  describe('User view component', () => {
    beforeEachProviders(() => [
      provide(RouteParams, { useValue: new RouteParams({ id: '0' }) }),
      UserService
    ]);

    it('should have a name', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
      return tcb.createAsync(UserViewComponent)
        .then((rootTC) => {
          spyOn(console, 'info');

          let uvDOMEl = rootTC.nativeElement;
          rootTC.detectChanges();

          expect(console.info).toHaveBeenCalledWith(0);
          expect(DOM.querySelectorAll(uvDOMEl, 'h2').length).toBe(0);
        });
    }));

  });
}

The route param is getting passed correctly, but the view hasn't changed before the tests are run. 路径参数正确传递,但视图在运行测试之前未更改。 How do I set up a test that happens after the promise in ngOnInit is resolved? 如何设置在ngOnInit中的promise解决后发生的测试?

IMO the best solution for this use case is to just make a synchronous mock service . IMO这个用例的最佳解决方案是创建一个同步模拟服务。 You can't use fakeAsync for this particular case because of the XHR call for templateUrl . 由于对templateUrl的XHR调用,您不能对此特定情况使用fakeAsync And personally I don't think the "hack" to make ngOnInit return a promise is very elegant. 而且我个人认为ngOnInit返回诺言的“黑客”非常优雅。 And you should not have to call ngOnInit directly, as it should be called by the framework. 而且您ngOnInit直接调用ngOnInit ,因为它应该由框架调用。

You should already be using mocks anyway, as you are only unit testing the component, and don't want to be dependent on the real service working correctly. 您应该已经在使用模拟,因为您只是对组件进行单元测试,并且不希望依赖于真正的服务正常工作。

To make a service that is synchronous, simple return the service itself from whatever methods are being called. 要创建一个同步的服务,简单地从正在调用的任何方法返回服务本身。 You can then add your then and catch ( subscribe if you are using Observable ) methods to the mock, so it acts like a Promise . then ,您可以向mock添加thencatch (如果使用Observablesubscribe )方法,因此它就像Promise For example 例如

class MockService {
  data;
  error;

  getData() {
    return this;
  }

  then(callback) {
    if (!this.error) {
      callback(this.data);
    }
    return this;
  }

  catch(callback) {
    if (this.error) {
      callback(this.error);
    }
  }

  setData(data) {
    this.data = data;
  }

  setError(error) {
    this.error = error;
  }
}

This has a few benefits. 这有一些好处。 For one it gives you a lot of control over the service during execution, so you can easily customize it's behavior. 例如,它可以在执行期间为您提供对服务的大量控制,因此您可以轻松地自定义其行为。 And of course it's all synchronous. 当然,这一切都是同步的。

Here's another example. 这是另一个例子。

A common thing you will see with components is the use of ActivatedRoute and subscribing to its params. 您将看到组件的一个常见问题是使用ActivatedRoute并订阅其params。 This is asynchronous, and done inside the ngOnInit . 这是异步的,在ngOnInit完成。 What I tend to do with this is create a mock for both the ActivatedRoute and the params property. 我倾向于做的是为ActivatedRoute params属性创建一个模拟。 The params property will be a mock object and have some functionality that appears to the outside world like an observable. params属性将是一个模拟对象,并具有一些对外部世界可见的功能,如可观察的。

export class MockParams {
  subscription: Subscription;
  error;

  constructor(private _parameters?: {[key: string]: any}) {
    this.subscription = new Subscription();
    spyOn(this.subscription, 'unsubscribe');
  }

  get params(): MockParams {
    return this;
  }

  subscribe(next: Function, error: Function): Subscription {
    if (this._parameters && !this.error) {
      next(this._parameters);
    }
    if (this.error) {
      error(this.error);
    }
    return this.subscription;
  }
}

export class MockActivatedRoute {
  constructor(public params: MockParams) {}
}

You can see we have a subscribe method that behaves like an Observable#subscribe . 您可以看到我们有一个subscribe方法,其行为类似于Observable#subscribe Another thing we do is spy on the Subscription so that we can test that it is destroyed. 我们做的另一件事是监视Subscription以便我们可以测试它是否被销毁。 In most cases you will have unsubscribed inside your ngOnDestroy . 在大多数情况下,您将在ngOnDestroy取消订阅。 To set up these mocks in your test you can just do something like 要在测试中设置这些模拟,您可以执行类似的操作

let mockParams: MockParams;

beforeEach(() => {
  mockParams = new MockParams({ id: 'one' });
  TestBed.configureTestingModule({
    imports: [ CommonModule ],
    declarations: [ TestComponent ],
    providers: [
      { provide: ActivatedRoute, useValue: new MockActivatedRoute(mockParams) }
    ]
  });
});

Now all the params are set for the route, and we have access to the mock params so we can set the error, and also check the subscription spy to make sure its been unsubscribed from. 现在所有的params都设置为路径,我们可以访问模拟参数,因此我们可以设置错误,并检查订阅间谍以确保取消订阅。

If you look at the tests below, you will see that they are all synchronous tests. 如果你看下面的测试,你会发现它们都是同步测试。 No need for async or fakeAsync , and it passes with flying colors. 不需要asyncfakeAsync ,它可以通过飞扬的颜色。

Here is the complete test (using RC6) 这是完整的测试(使用RC6)

import { Component, OnInit, OnDestroy, DebugElement } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

@Component({
  template: `
    <span *ngIf="id">{{ id }}</span>
    <span *ngIf="error">{{ error }}</span>
  `
})
export class TestComponent implements OnInit, OnDestroy {
  id: string;
  error: string;
  subscription: Subscription;

  constructor(private _route: ActivatedRoute) {}

  ngOnInit() {
    this.subscription = this._route.params.subscribe(
      (params) => {
        this.id = params['id'];
      },
      (error) => {
        this.error = error;
      }
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

export class MockParams {
  subscription: Subscription;
  error;

  constructor(private _parameters?: {[key: string]: any}) {
    this.subscription = new Subscription();
    spyOn(this.subscription, 'unsubscribe');
  }

  get params(): MockParams {
    return this;
  }

  subscribe(next: Function, error: Function): Subscription {
    if (this._parameters && !this.error) {
      next(this._parameters);
    }
    if (this.error) {
      error(this.error);
    }
    return this.subscription;
  }
}

export class MockActivatedRoute {
  constructor(public params: MockParams) {}
}

describe('component: TestComponent', () => {
  let mockParams: MockParams;

  beforeEach(() => {
    mockParams = new MockParams({ id: 'one' });
    TestBed.configureTestingModule({
      imports: [ CommonModule ],
      declarations: [ TestComponent ],
      providers: [
        { provide: ActivatedRoute, useValue: new MockActivatedRoute(mockParams) }
      ]
    });
  });

  it('should set the id on success', () => {
    let fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    let debugEl = fixture.debugElement;
    let spanEls: DebugElement[] = debugEl.queryAll(By.css('span'));
    expect(spanEls.length).toBe(1);
    expect(spanEls[0].nativeElement.innerHTML).toBe('one');
  });

  it('should set the error on failure', () => {
    mockParams.error = 'Something went wrong';
    let fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    let debugEl = fixture.debugElement;
    let spanEls: DebugElement[] = debugEl.queryAll(By.css('span'));
    expect(spanEls.length).toBe(1);
    expect(spanEls[0].nativeElement.innerHTML).toBe('Something went wrong');
  });

  it('should unsubscribe when component is destroyed', () => {
    let fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    fixture.destroy();
    expect(mockParams.subscription.unsubscribe).toHaveBeenCalled();
  });
});

Return a Promise from #ngOnInit : #ngOnInit返回一个Promise

ngOnInit(): Promise<any> {
  const id: number = parseInt(this._routeParams.get('id'));

  return this._userService
    .getUser(id)
    .then(user => {
      console.info(user);
      this.user = user;
    });
}

I ran into the same issue a few days back, and found this to be the most workable solution. 几天前我遇到了同样的问题,发现这是最可行的解决方案。 As far as I can tell, it doesn't impact anywhere else in the application; 据我所知,它不会影响应用程序中的任何其他位置; since #ngOnInit has no specified return type in the source's TypeScript, I doubt anything in the source code is expecting a return value from that. 因为#ngOnInit在源的TypeScript中没有指定的返回类型,所以我怀疑源代码中的任何东西都期望返回值。

Link to OnInit : https://github.com/angular/angular/blob/2.0.0-beta.6/modules/angular2/src/core/linker/interfaces.ts#L79-L122 链接到OnInithttps//github.com/angular/angular/blob/2.0.0-beta.6/modules/angular2/src/core/linker/interfaces.ts#L79-L122

Edit 编辑

In your test, you'd return a new Promise : 在您的测试中,您将返回一个新的Promise

it('should have a name', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
  // Create a new Promise to allow greater control over when the test finishes
  //
  return new Promise((resolve, reject) => {
    tcb.createAsync(UserViewComponent)
      .then((rootTC) => {

        // Call ngOnInit manually and put your test inside the callback
        //
        rootTC.debugElement.componentInstance.ngOnInit().then(() => {
          spyOn(console, 'info');

          let uvDOMEl = rootTC.nativeElement;
          rootTC.detectChanges();

          expect(console.info).toHaveBeenCalledWith(0);
          expect(DOM.querySelectorAll(uvDOMEl, 'h2').length).toBe(0);

          // Test is done
          //
          resolve();
        });

      });
    }));

  }

I had the same issue, here is how I managed to fix it. 我有同样的问题,这是我设法解决它的方式。 I had to use fakeAsync and tick. 我不得不使用fakeAsync和tick。

fakeAsync(
      inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
        tcb
        .overrideProviders(UsersComponent, [
          { provide: UserService, useClass: MockUserService }
        ])
        .createAsync(UsersComponent)
        .then(fixture => {
          fixture.autoDetectChanges(true);
          let component = <UsersComponent>fixture.componentInstance;
          component.ngOnInit();
          flushMicrotasks();
          let element = <HTMLElement>fixture.nativeElement;
          let items = element.querySelectorAll('li');
          console.log(items);
        });
      })
    )

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM