Тестирование обещания в 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);
});
})
)