[英]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添加then
和catch
(如果使用Observable
则subscribe
)方法,因此它就像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. 不需要
async
或fakeAsync
,它可以通过飞扬的颜色。
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 链接到
OnInit
: https : //github.com/angular/angular/blob/2.0.0-beta.6/modules/angular2/src/core/linker/interfaces.ts#L79-L122
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.