Проверка ошибки с наблюдаемыми в сервисах
Допустим, у меня есть компонент, который подписывается на сервисную функцию:
export class Component {
...
ngOnInit() {
this.service.doStuff().subscribe(
(data: IData) => {
doThings(data);
},
(error: Error) => console.error(error)
);
};
};
Вызов подписки принимает две анонимные функции в качестве параметров, мне удалось настроить тест работающего модуля для функции данных, но Карма не примет покрытие для ошибки.
![enter image description here]()
Я пытался шпионить за функцией console.error, выдавал ошибку, а затем ожидал, что шпион был вызван, но это не совсем так.
Мой юнит-тест:
spyOn(console,'error').and.callThrough();
serviceStub = {
doStuff: jasmine.createSpy('doStuff').and.returnValue(Observable.of(data)),
};
serviceStub.doStuff.and.returnValue(Observable.throw(
'error!'
));
serviceStub.doStuff().subscribe(
(res) => {
*working test, can access res*
},
(error) => {
console.error(error);
console.log(error); //Prints 'error!' so throw works.
expect(console.error).toHaveBeenCalledWith('error!'); //Is true but won't be accepted for coverage.
}
);
Какова наилучшая практика для тестирования анонимных функций, подобных этим? Какой минимум для обеспечения тестового покрытия?
Ответы
Ответ 1
Вы можете просто смоделировать объект ошибки Observable throw, например, Observable.throw({status: 404})
, и проверить блок ошибок Observable.
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService, 'method')
.and.returnValue(Observable.throw({status: 404}));
Обновление 2019 года:
Поскольку некоторым людям лень читать комментарии, позвольте мне написать это здесь:
Рекомендуется использовать ошибки для Rxjs
import { throwError } from 'rxjs'
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService,'method').and.returnValue(throwError({status: 404}));
Ответ 2
Непосредственно укажите цель кода, который вы показываете, который пытается протестировать макет службы. Проблема покрытия связана с тем, что компонент и обратный вызов ошибки не вызывались (который вызывается только при возникновении ошибки).
То, что я обычно делаю для большинства моих наблюдаемых сервисов, заключается в создании макета, методы которого просто возвращаются сами. Макет службы имеет метод subscribe
, который принимает обратные вызовы next
, error
и complete
. Пользователь макета получает, чтобы настроить его для добавления ошибки, чтобы функция error
вызывалась или добавляла данные, поэтому метод next
вызывается. Мне нравится больше всего в том, что все это синхронно.
Ниже что-то вроде того, что я обычно использую. Это просто абстрактный класс для других макетов. Он обеспечивает базовую функциональность, которую обеспечивает наблюдаемое. Расширяющаяся макетная служба должна просто добавлять нужные ей методы, возвращаясь к методу.
import { Subscription } from 'rxjs/Subscription';
export abstract class AbstractMockObservableService {
protected _subscription: Subscription;
protected _fakeContent: any;
protected _fakeError: any;
set error(err) {
this._fakeError = err;
}
set content(data) {
this._fakeContent = data;
}
get subscription(): Subscription {
return this._subscription;
}
subscribe(next: Function, error?: Function, complete?: Function): Subscription {
this._subscription = new Subscription();
spyOn(this._subscription, 'unsubscribe');
if (next && this._fakeContent && !this._fakeError) {
next(this._fakeContent);
}
if (error && this._fakeError) {
error(this._fakeError);
}
if (complete) {
complete();
}
return this._subscription;
}
}
Теперь в ваших тестах вы просто делаете что-то вроде
class MockService extends AbstractMockObservableService {
doStuff() {
return this;
}
}
let mockService;
beforeEach(() => {
mockService = new MockService();
TestBed.configureTestingModule({
providers: [{provide: SomeService, useValue: mockService }],
declarations: [ TestComponent ]
});
});
it('should call service success', () => {
mockService.content = 'some content';
let fixture = TestBed.createComponent(TestComponent);
// test component for success case
});
it('should call service error', () => {
mockService.error = 'Some error';
let fixture = TestBed.createComponent(TestComponent);
// test component for error case
// this should handle your coverage problem
});
// this assumes you have unsubscribed from the subscription in your
// component, which you should always do in the ngOnDestroy of the component
it('should unsubscribe when component destroyed', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
fixture.destroy();
expect(mockService.subscription.unsubscribe).toHaveBeenCalled();
})