Тестирование обещания в Angular 2 ngOnInit

У меня есть компонент Angular 2, который я пытаюсь проверить, но у меня возникли проблемы, поскольку данные заданы в ngOnInit функция, поэтому не сразу доступна в модульном тесте.

пользовательский 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]);
  }
}

пользовательский 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);
        });
    }));

  });
}

Параметр маршрута передается правильно, но вид не изменился до запуска тестов. Как настроить тест, который происходит после разрешения обещания в ngOnInit?

3 ответа

Решение

Вернуть Promise от #ngOnInit:

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

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

Несколько дней назад я столкнулся с той же проблемой и обнаружил, что это наиболее подходящее решение. Насколько я могу судить, это не влияет нигде в приложении; поскольку #ngOnInit не имеет указанного возвращаемого типа в TypeScript источника, я сомневаюсь, что что-либо в исходном коде ожидает возвращаемого значения из этого.

Ссылка на OnInit: https://github.com/angular/angular/blob/2.0.0-beta.6/modules/angular2/src/core/linker/interfaces.ts#L79-L122

редактировать

В вашем тесте вы бы вернули новый 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();
        });

      });
    }));

  }

IMO лучшее решение для этого варианта использования - просто создать синхронный фиктивный сервис. Вы не можете использовать fakeAsync для этого конкретного случая из-за вызова XHR для templateUrl, И лично я не думаю, что "взломать", чтобы сделать ngOnInit вернуть обещание очень элегантно. И вам не нужно звонить ngOnInit непосредственно, как это должно быть названо в рамках.

В любом случае, вы уже должны использовать mocks, поскольку вы только тестируете компонент, и не хотите зависеть от правильной работы реальной службы.

Чтобы сделать службу синхронной, просто верните сам сервис из любых вызываемых методов. Затем вы можете добавить свой then а также catch (subscribe если вы используете Observable) методы издеваться, поэтому он действует как Promise, Например

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;
  }
}

Это имеет несколько преимуществ. С одной стороны, это дает вам большой контроль над сервисом во время выполнения, так что вы можете легко настроить его поведение. И конечно все это синхронно.

Вот еще один пример.

Общее, что вы увидите в компонентах, это использование ActivatedRoute и подписаться на его параметры. Это асинхронно, и делается внутри ngOnInit, То, что я склонен делать с этим, это создать насмешку для обоих ActivatedRoute и params имущество. 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) {}
}

Вы можете видеть, что у нас есть subscribe метод, который ведет себя как Observable#subscribe, Еще одна вещь, которую мы делаем, это шпионить за Subscription так что мы можем проверить, что он уничтожен. В большинстве случаев у вас будет отписаться внутри вашего ngOnDestroy, Чтобы настроить эти макеты в своем тесте, вы можете просто сделать что-то вроде

let mockParams: MockParams;

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

Теперь все параметры установлены для маршрута, и у нас есть доступ к фиктивным параметрам, чтобы мы могли установить ошибку, а также проверить шпиона подписки, чтобы убедиться, что он был отписан.

Если вы посмотрите на тесты ниже, вы увидите, что все они являются синхронными тестами. Нет необходимости async или же fakeAsync и это проходит с летающими цветами.

Вот полный тест (с использованием 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();
  });
});

У меня была такая же проблема, вот как мне удалось это исправить. Я должен был использовать fakeAsync и галочку.

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);
        });
      })
    )
Другие вопросы по тегам