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

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

пользовательского 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 разрешено?

Ответ 1

Верните a 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();
        });

      });
    }));

  }

Ответ 2

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

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

Если вы посмотрите на приведенные ниже тесты, вы увидите, что все они являются синхронными тестами. Нет необходимости в 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();
  });
});

Ответ 3

У меня была такая же проблема, вот как мне удалось это исправить. Мне пришлось использовать 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);
        });
      })
    )