Angular 디렉티브 테스트와 Spectator
Angular에서 디렉티브는 컴포넌트의 재사용성과 기능 확장을 담당하는 핵심 요소입니다. 그러나 디렉티브 테스트는 복잡한 DOM 조작과 이벤트 시뮬레이션으로 인해 많은 보일러플레이트 코드가 발생하기 쉽습니다. Spectator는 Angular 테스트에 특화된 유틸리티 라이브러리로, 직관적인 API와 강력한 DOM 쿼리 기능을 제공하여 테스트 코드의 가독성과 유지보수성을 크게 향상시킵니다.
환경 설정 및 프레임워크 통합
Spectator는 Jest, Vitest 등 주요 테스팅 프레임워크와 원활하게 통합됩니다. 프로젝트에 Spectator를 도입하려면 패키지 매니저를 통해 의존성을 설치하면 됩니다.
npm install @ngneat/spectator --save-dev
프레임워크별 전용 엔트리 포인트를 제공하므로, Jest를 사용한다면 @ngneat/spectator/jest를, Vitest를 사용한다면 @ngneat/spectator/vitest를 임포트하여 타입 안전성을 보장받을 수 있습니다.
createDirectiveFactory를 활용한 테스트 베드 구축
createDirectiveFactory 함수는 디렉티브 테스트 환경을 신속하게 구성하는 핵심 팩토리입니다. 이 함수를 사용하면 TestBed 설정 과정을 생략하고 테스트 로직에 집중할 수 있습니다.
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest';
import { HighlightDirective } from './highlight.directive';
describe('HighlightDirective', () => {
let spectator: SpectatorDirective<HighlightDirective>;
const setupDirective = createDirectiveFactory(HighlightDirective);
beforeEach(() => {
spectator = setupDirective(`<p appHighlight>Target Text</p>`);
});
});
외부 모듈이나 종속성이 필요한 복잡한 디렉티브의 경우, 팩토리 옵션을 통해 추가 구성 요소를 주입할 수 있습니다.
const setupDirective = createDirectiveFactory({
directive: HighlightDirective,
imports: [CommonModule],
providers: [ThemeService]
});
입력(Input) 속성 바인딩 검증
디렉티브가 외부 데이터를 올바르게 수신하는지 확인하려면 입력 속성 테스트가 필수적입니다. Spectator는 Angular Signal과 연동된 바인딩 헬퍼를 제공하여 상태 변경을 손쉽게 테스트할 수 있습니다.
describe('Input Properties', () => {
let spectator: Spectator<TextScaleComponent>;
const scale = signal(1.5);
const setupComponent = createComponentFactory({
component: TextScaleComponent,
bindings: [inputBinding('scaleFactor', scale)],
});
beforeEach(() => (spectator = setupComponent()));
it('should update input binding correctly', () => {
expect(spectator.component.scaleFactor).toBe(1.5);
scale.set(2.0);
spectator.detectChanges();
expect(spectator.component.scaleFactor).toBe(2.0);
});
});
출력(Output) 이벤트 리스닝 검증
디렉티브에서 발생하는 이벤트를 상위 컴포넌트가 올바르게 감지하는지 테스트하기 위해 outputBinding과 DOM 인터랙션 메서드를 조합하여 사용합니다.
describe('Output Events', () => {
let spectator: Spectator<NotifyComponent>;
let notifyCount = 0;
const setupComponent = createComponentFactory({
component: NotifyComponent,
bindings: [outputBinding('notified', () => notifyCount++)],
});
beforeEach(() => (spectator = setupComponent()));
it('should emit output event on interaction', () => {
expect(notifyCount).toBe(0);
spectator.click('.notify-btn');
expect(notifyCount).toBe(1);
});
});
특정 이벤트 객체를 포함하여 직접 트리거해야 하는 경우 triggerEventHandler를 활용하면 정확한 이벤트 페이로드를 전달할 수 있습니다.
spectator.triggerEventHandler('.notify-btn', 'click', { detail: { id: 101 } });
양방향(Two-Way) 데이터 바인딩 동기화 테스트
양방향 바인딩이 적용된 디렉티브는 내부 상태와 외부 시그널 간의 데이터 동기화를 엄격하게 검증해야 합니다.
describe('Two-Way Data Binding', () => {
let spectator: Spectator<ExpandableComponent>;
const expanded = signal(false);
const setupComponent = createComponentFactory({
component: ExpandableComponent,
bindings: [twoWayBinding('isExpanded', expanded)],
});
beforeEach(() => (spectator = setupComponent()));
it('should synchronize two-way bound properties', () => {
expect(expanded()).toBe(false);
expanded.set(true);
spectator.detectChanges();
expect(spectator.component.isExpanded).toBe(true);
spectator.component.isExpanded = false;
spectator.component.isExpandedChange.emit(false);
spectator.detectChanges();
expect(expanded()).toBe(false);
});
});
호스트(Host) 바인딩 및 DOM 조작 확인
호스트 요소의 클래스, 스타일, 속성을 동적으로 변경하는 디렉티브의 경우, spectator.element를 통해 DOM 상태를 직접 단언할 수 있습니다.
describe('Host Element Bindings', () => {
let spectator: Spectator<ThemeComponent>;
const darkMode: WritableSignal<boolean> = signal(false);
const setupComponent = createComponentFactory<ThemeComponent>({
component: ThemeComponent,
imports: [ThemeComponent],
bindings: [inputBinding('isDark', darkMode)],
});
beforeEach(() => (spectator = setupComponent()));
it('should apply host classes based on state', () => {
expect(spectator.element.classList.contains('theme-dark')).toBe(false);
darkMode.set(true);
spectator.detectChanges();
expect(spectator.element.classList.contains('theme-dark')).toBe(true);
});
});
종속성 주입 및 서비스 모킹
비즈니스 로직이 포함된 서비스나 외부 API에 의존하는 디렉티브를 테스트할 때는 providers 배열을 통해 목(Mock) 객체를 주입하여 테스트의 격리성을 유지합니다.
const setupDirective = createDirectiveFactory({
directive: HighlightDirective,
providers: [
{ provide: LoggerService, useValue: jasmine.createSpyObj('LoggerService', ['log']) }
]
});
테스트 케이스 내부에서는 inject 메서드를 통해 모킹된 서비스 인스턴스에 접근하고, 특정 메서드가 호출되었는지 검증합니다.
const logger = spectator.inject(LoggerService);
expect(logger.log).toHaveBeenCalledWith('Highlight applied');