#javascript #angular #unit-testing #inheritance #jasmine
#javascript #angular #модульное тестирование #наследование #jasmine
Вопрос:
export class Parent implements OnInit {
ngOnInit(): void {
// huge amount of different services calls
}
}
export class Child extends Parent implements OnInit {
ngOnInit(): void {
super.ngOnInit();
// a few more functions
}
}
Как разработать модульный тест, чтобы охватить дочерний ngOnInit, не издеваясь над всеми сервисными функциями для родительского ngOnInit?
Мои попытки были примерно такими:
let child: Child;
const mockParent = {
ngOnInit: jasmine.createSpy('ngOnInit')
};
child = new Child(); // base object is created already
Object.getPrototypeOf(child) = jasmine.createSpy('Parent').and.callFake(() => mockParent); // so this doesn't work
Комментарии:
1. Если дочерний элемент вызывает super.ngOnInit(), то, похоже, от этого зависит его логика. Если вы хотите написать правильные тестовые примеры, вам просто нужно издеваться над всеми зависимостями.
2. родительский ngOnInit уже охвачен модульными тестами. Почему я должен делать это снова?
Ответ №1:
Есть решение, как шпионить за функцией родительского класса.
Parent.prototype.ngOnInit = jasmine.createSpy('ngOnInit');
Однако решение недостаточно безопасно. Давайте посмотрим на пример:
class Mobile {
sport: string;
setSport(): void {
this.sport = 'Football';
}
}
describe('MobileClass', () => {
const mobile: Mobile = new Mobile();
it('#setSport', () => {
mobile.setSport();
expect(mobile.sport).toBe('Football');
});
});
class Desktop extends Mobile {
isFootball: boolean;
setSport(): void {
super.setSport();
this.isFootball = this.func(this.sport);
}
func(sp: string): boolean {
return sp === 'Football' ? true : false;
}
}
describe('DesktopClass', () => {
const desktop: Desktop = new Desktop();
it('#setSport', () => {
Mobile.prototype.setSport = jasmine.createSpy('setSport');
desktop.sport = 'Basketball';
desktop.setSport();
expect(Mobile.prototype.setSport).toHaveBeenCalled();
expect(desktop.isFootball).toBe(false);
});
it('#func', () => {
// 2 cases covered
...
});
});
Выше мы подсмотрели функцию базового класса setSport. Оба теста прошли успешно.
Теперь представьте, что в базовом классе внесены некоторые изменения, например, константа ‘Football’ заменена на ‘Tennis’ в базовом классе и его модульном тестировании. В этом случае модульные тесты для обоих классов пройдут успешно.
Давайте откажемся от идеи издевательства над базовым классом. У нас будет:
describe('DesktopClass', () => {
const desktop: Desktop = new Desktop();
it('#setSport', () => {
desktop.setSport();
expect(desktop.isFootball).toBe(true);
});
});
В первом случае оба теста прошли успешно, но если мы изменим ‘Football’ на ‘Tennis’ в базовом классе и его модульном тестировании, то теперь тест для Desktop завершится неудачей.
Это довольно распространенная ошибка, когда большие команды работают над большим проектом и вносят изменения в несколько файлов, но забывают о других, потому что модульные тесты для обоих прошли успешно.
Напоследок я хотел бы сослаться на статью «Издевательство — это запах кода» Эрика Эллиотта, особенно на некоторые цитаты:
Что такое тесная связь?
Связь подклассов: Подклассы зависят от реализации и всей иерархии родительского класса: самая жесткая форма связи, доступная в OO design.
Что вызывает тесную связь?
Мутация против неизменяемости, Побочные эффекты против чистоты / изолированные побочные эффекты и т.д.
Сохранение вызовов базового класса нарушает, с некоторой точки зрения, термин модульного тестирования и может потребовать гораздо большего количества макетов для служб, используемых в базовом классе. И нам нужно будет переместить эти макеты в отдельный файл, чтобы сохранить их сухими. Дважды подумайте, что выбрать: более быстрый и простой код или дополнительную страховку от ошибок.