Угловое тестирование, как предотвратить вызов ngOnInit для непосредственного тестирования метода
контекст
У меня есть компонент. Внутри него функция ngOnInit вызывает другую функцию компонента для получения списка пользователей. Я хочу сделать две серии тет:
- Сначала проверьте правильность запуска ngOnInit и заполните список пользователей.
- Во второй раз я хочу проверить мою функцию обновления, которая также вызывает getUserList()
Первый тест с триггером ngOnInit, когда я вызываю fixture.detectChanges(), работает правильно.
проблема
Моя проблема заключается в тестировании функции обновления: как только я вызываю fixture.detectChanges(), запускается ngOnInit, и тогда я не могу знать, откуда приходят мои результаты и будет ли моя функция refresh () проверена правильно.
Есть ли способ, до моей второй серии тестов на refresh()
метод, чтобы "удалить" или "заблокировать" 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');
}));
2 ответа
Предотвращение жизненного цикла крючка (ngOnInit
) из-за неправильного направления. Проблема имеет две возможные причины. Либо тест недостаточно изолирован, либо стратегия тестирования неверна.
Угловая направляющая довольно специфична и самоуверенна в тестовой изоляции
Однако часто более продуктивно исследовать внутреннюю логику классов приложений с изолированными модульными тестами, которые не зависят от Angular. Такие тесты часто меньше по размеру и их легче читать, писать и поддерживать.
Таким образом, изолированные тесты просто должны создавать экземпляр класса и тестировать его методы.
userManagementService = new UserManagementServiceStub;
comp = new UserListComponent(userManagementService);
spyOn(comp, 'getUserList');
...
comp.ngOnInit();
expect(comp.getUserList).toHaveBeenCalled();
...
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalled();
У изолированных тестов есть недостаток - они не проверяют DI, в то время как тесты TestBed делают. В зависимости от точки зрения и стратегии тестирования, отдельные тесты могут рассматриваться как юнит-тесты, а тесты TestBed могут рассматриваться как функциональные тесты. И хороший набор тестов может содержать оба.
В коде выше should get the user List via refresh function
Тест, очевидно, является функциональным тестом, он рассматривает экземпляр компонента как черный ящик.
Чтобы заполнить пробел, можно добавить пару модульных тестов 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');
}));
Лично я предпочитаю отменять компонент ngOnInit для каждого теста.
beforeEach(() => {UserListComponent.prototype.ngOnInit = () => {};....});