Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
717 views
in Technique[技术] by (71.8m points)

jasmine - 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.

user-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:

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:

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

IMO the best solution for this use case is to just make a synchronous mock service . You can't use fakeAsync for this particular case because of the XHR call for templateUrl. And personally I don't think the "hack" to make ngOnInit return a promise is very elegant. And you should not have to call ngOnInit directly, as it should be called by the framework.

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. 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. This is asynchronous, and done inside the ngOnInit. What I tend to do with this is create a mock for both the ActivatedRoute and the params property. The params property will be a mock object and have some functionality that appears to the outside world like an observable.

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. Another thing we do is spy on the Subscription so that we can test that it is destroyed. In most cases you will have unsubscribed inside your 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.

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.

Here is the complete test (using 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();
  });
});

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...