コンポーネントがあります。内部で、ngOnInit関数はコンポーネントの別の関数を呼び出してユーザーリストを取得します。 2つのテットシリーズを作成します。
Fixture.detectChanges()を呼び出すときのngOnInitトリガーを使用した最初のテストは適切に動作します。
私の問題は、refresh関数をテストするときです。fixture.detectChanges()を呼び出すと、ngOnInitがトリガーされ、結果がどこから来て、refresh()関数が適切にテストされるかどうかを知ることができません。
refresh()
メソッドの2回目のテストの前に、ngOnInit()
を「削除」または「ブロック」する方法があるので、fixture.detectChanges()
?
overrideComponent
を調べてみましたが、ngOnInit()
を削除できないようです。
または私の場合、fixture.detectChanges
を使用する以外の変更を検出する方法はありますか?
コンポーネント、スタブサービス、および仕様ファイルのコードを次に示します。
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { UserManagementService } from '../../shared/services/global.api';
import { UserListItemComponent } from './user-list-item.component';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
public userList = [];
constructor(
private _userManagementService: UserManagementService,
) { }
ngOnInit() {
this.getUserList();
}
onRefreshUserList() {
this.getUserList();
}
getUserList(notifyWhenComplete = false) {
this._userManagementService.getListUsers().subscribe(
result => {
this.userList = result.objects;
},
error => {
console.error(error);
},
() => {
if (notifyWhenComplete) {
console.info('Notification');
}
}
);
}
}
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
async,
fakeAsync,
ComponentFixture,
TestBed,
tick,
inject
} from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
// Components
import { UserListComponent } from './user-list.component';
// Services
import { UserManagementService } from '../../shared/services/global.api';
import { UserManagementServiceStub } from '../../testing/services/global.api.stub';
let comp: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let service: UserManagementService;
describe('UserListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [UserListComponent],
imports: [],
providers: [
{
provide: UserManagementService,
useClass: UserManagementServiceStub
}
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents();
}));
tests();
});
function tests() {
beforeEach(() => {
fixture = TestBed.createComponent(UserListComponent);
comp = fixture.componentInstance;
service = TestBed.get(UserManagementService);
});
it(`should be initialized`, () => {
expect(fixture).toBeDefined();
expect(comp).toBeDefined();
});
it(`should NOT have any user in list before ngOnInit`, () => {
expect(comp.userList.length).toBe(0, 'user list is empty before init');
});
it(`should get the user List after ngOnInit`, async(() => {
fixture.detectChanges(); // This triggers the ngOnInit and thus the getUserList() method
// Works perfectly. ngOnInit was triggered and my list is OK
expect(comp.userList.length).toBe(3, 'user list exists after init');
}));
it(`should get the user List via refresh function`, fakeAsync(() => {
comp.onRefreshUserList(); // Can be commented, the test will pass because of ngOnInit trigger
tick();
// This triggers the ngOnInit which ALSO call getUserList()
// so my result can come from getUserList() method called from both source: onRefreshUserList() AND through ngOnInit().
fixture.detectChanges();
// If I comment the first line, the expectation is met because ngOnInit was triggered!
expect(comp.userList.length).toBe(3, 'user list after function call');
}));
}
import { Observable } from 'rxjs/Observable';
export class UserManagementServiceStub {
getListUsers() {
return Observable.from([
{
count: 3,
objects:
[
{
id: "7f5a6610-f59b-4cd7-b649-1ea3cf72347f",
name: "user 1",
group: "any"
},
{
id: "d6f54c29-810e-43d8-8083-0712d1c412a3",
name: "user 2",
group: "any"
},
{
id: "2874f506-009a-4af8-8ca5-f6e6ba1824cb",
name: "user 3",
group: "any"
}
]
}
]);
}
}
私はいくつかの「回避策」を試みましたが、それが少しであることがわかりました。
例えば:
it(`should get the user List via refresh function`, fakeAsync(() => {
expect(comp.userList.length).toBe(0, 'user list must be empty');
// Here ngOnInit is called, so I override the result from onInit
fixture.detectChanges();
expect(comp.userList.length).toBe(3, 'ngOnInit');
comp.userList = [];
fixture.detectChanges();
expect(comp.userList.length).toBe(0, 'ngOnInit');
// Then call the refresh function
comp.onRefreshUserList(true);
tick();
fixture.detectChanges();
expect(comp.userList.length).toBe(3, 'user list after function call');
}));
ライフサイクルフック(ngOnInit
)が呼び出されないようにするのは間違った方向です。この問題には2つの原因が考えられます。テストが十分に分離されていないか、テスト戦略が間違っています。
角度ガイドはかなり テスト分離について具体的かつ意見があります :
ただし、多くの場合、Angularに依存しない分離された単体テストを使用して、アプリケーションクラスの内部ロジックを探索する方が生産的です。多くの場合、このようなテストは小さく、読み取り、書き込み、および保守が簡単です。
隔離されたテストはクラスをインスタンス化し、そのメソッドをテストするだけです
userManagementService = new UserManagementServiceStub;
comp = new UserListComponent(userManagementService);
spyOn(comp, 'getUserList');
...
comp.ngOnInit();
expect(comp.getUserList).toHaveBeenCalled();
...
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalled();
分離されたテストには欠点があります-TestBedテストにはありますが、DIにはテストされません。視点とテスト戦略に応じて、分離テストは単体テストと見なされ、TestBedテストは機能テストと見なされます。また、優れたテストスイートには両方を含めることができます。
上記のコードでshould get the user List via refresh function
testは明らかに機能テストであり、コンポーネントインスタンスをブラックボックスとして扱います。
いくつかのTestBed単体テストを追加してギャップを埋めることができますが、それらはおそらく、孤立したテストを気にしないほど十分に堅固です(後者の方が確かに正確ですが)。
spyOn(comp, 'getUserList');
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalledTimes(1);
...
spyOn(comp, 'getUserList');
spyOn(comp, 'ngOnInit').and.callThrough();
tick();
fixture.detectChanges();
expect(comp.ngOnInit).toHaveBeenCalled();
expect(comp.getUserList).toHaveBeenCalledTimes(1);
it(`should get the user List via refresh function`, fakeAsync(() => {
let ngOnInitFn = UserListComponent.prototype.ngOnInit;
UserListComponent.prototype.ngOnInit = () => {} // override ngOnInit
comp.onRefreshUserList();
tick();
fixture.detectChanges();
UserListComponent.prototype.ngOnInit = ngOnInitFn; // revert ngOnInit
expect(comp.userList.length).toBe(3, 'user list after function call');
}));