심화 가이드
테스트

서비스 테스트하기

서비스에는 보통 컴포넌트가 사용하는 애플리케이션의 비즈니스 로직이 담겨 있습니다. 서비스를 테스트하면 컴포넌트나 템플릿에 의존하지 않고 로직 자체가 제대로 동작하는지 확인할 수 있습니다.

이 문서에서는 Angular CLI 프로젝트에 기본으로 포함된 Vitest를 사용합니다. 테스트 환경 설정은 테스트 개요를 참고하세요.

서비스 테스트하기

기본 산술 연산을 수행하는 Calculator 서비스를 살펴봅시다.

calculator.ts

import {Service} from '@angular/core';

@Service()
export class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }

  subtract(a: number, b: number): number {
    return a - b;
  }
}

이 서비스를 테스트하려면 각 테스트에 독립적인 테스트 환경을 만드는 Angular 테스트 유틸리티 TestBed를 설정합니다. TestBed는 의존성 주입을 구성하고 서비스 인스턴스를 가져올 수 있게 해서, 실제 애플리케이션에서 Angular가 구성 요소를 연결하는 방식을 재현합니다.

calculator.spec.ts

import {TestBed} from '@angular/core/testing';
import {beforeEach, describe, expect, it} from 'vitest';
import {Calculator} from './calculator';

describe('Calculator', () => {
  let service: Calculator;

  beforeEach(() => {
    // Calculator 서비스를 주입합니다.
    // 서비스가 `@Service`를 사용하므로 Angular에서 사용할 수 있습니다
    service = TestBed.inject(Calculator);
  });

  it('adds two numbers', () => {
    expect(service.add(1, 2)).toBe(3);
  });

  it('subtracts two numbers', () => {
    expect(service.subtract(5, 3)).toBe(2);
  });
});

위 예제의 beforeEach 블록은 각 테스트 전에 새로운 서비스 인스턴스를 주입합니다. 그래서 이전 테스트의 상태가 남지 않고 각 테스트가 독립적으로 실행됩니다.

의존성이 있는 서비스 테스트하기

대부분의 서비스는 정상 동작하기 위해 다른 서비스에 의존합니다. TestBed는 기본으로 의존성의 실제 구현을 제공하므로, 테스트에서도 애플리케이션이 사용하는 실제 코드 경로를 실행합니다. 하지만 의존성이 복잡하거나 느리거나 결과를 예측하기 어려운 경우도 있습니다. 이럴 때는 직접 제어할 수 있는 구현으로 교체할 수 있습니다.

TaxCalculator로 주문의 최종 금액을 계산하는 OrderTotal 서비스를 살펴봅시다.

tax-calculator.ts

import {Service} from '@angular/core';

@Service()
export class TaxCalculator {
  calculate(subtotal: number): number {
    return subtotal * 0.05;
  }
}

order-total.ts

import {inject, Service} from '@angular/core';
import {TaxCalculator} from './tax-calculator';

@Service()
export class OrderTotal {
  private taxCalculator = inject(TaxCalculator);

  total(subtotal: number): number {
    return subtotal + this.taxCalculator.calculate(subtotal);
  }
}

이 예제에서 OrderTotal은 inject()로 Angular 의존성 주입 시스템에 TaxCalculator를 요청합니다. TestBed는 기본으로 실제 TaxCalculator를 제공하며, 이처럼 간단한 계산에는 적합합니다. 하지만 TaxCalculator에 복잡한 로직이나 네트워크 요청이 있거나 결과를 예측하기 어렵다면, 직접 제어할 수 있는 구현으로 교체하는 것이 좋을 수 있습니다.

의존성을 스텁으로 교체하기

스텁(stub)은 의존성이나 메서드를 예측할 수 있는 값을 반환하는 구현으로 교체하는 방법입니다. 이를 활용하면 테스트 결과를 검증하기 쉽습니다.

실제 TaxCalculator에 의존하지 않고 OrderTotal을 테스트하려면 TestBed 설정에 스텁을 제공하세요.

order-total.spec.ts

import {TestBed} from '@angular/core/testing';
import {beforeEach, describe, expect, it, vi, type Mocked} from 'vitest';
import {OrderTotal} from './order-total';
import {TaxCalculator} from './tax-calculator';

// Vitest의 `Mocked` 유틸리티 타입으로 스텁의 타입 안전성을 보장하고,
// `vi.fn()`으로 각 메서드의 목 함수를 생성합니다
const taxCalculatorStub: Mocked<TaxCalculator> = {
  calculate: vi.fn(),
};

describe('OrderTotal', () => {
  let service: OrderTotal;

  beforeEach(() => {
    // `mockReturnValue`로 스텁의 반환값을 지정합니다
    taxCalculatorStub.calculate.mockReturnValue(5);

    TestBed.configureTestingModule({
      // `providers` 배열에는 프로바이더 객체를 넣습니다. `provide`에는
      // 교체할 의존성을 지정하고 `useValue`에는 스텁을 정의합니다
      providers: [{provide: TaxCalculator, useValue: taxCalculatorStub}],
    });
    service = TestBed.inject(OrderTotal);
  });

  it('adds tax to the subtotal', () => {
    expect(service.total(100)).toBe(105);
  });
});

이렇게 설정하면 OrderTotal이 TaxCalculator를 요청할 때마다 TestBed는 taxCalculatorStub을 대신 사용합니다. 스텁은 항상 5를 반환하므로, TaxCalculator의 세율이 바뀌더라도 OrderTotal이 소계에 세금을 올바르게 더하는지 확인할 수 있습니다.

스파이로 상호작용 검증하기

스텁으로 의존성의 반환값을 제어할 수 있지만, 서비스가 의존성을 올바른 인자로 호출했는지도 확인해야 할 수 있습니다. 이때 함수의 호출 방식을 추적하는 스파이(spy)를 사용합니다. Vitest에서는 vi.fn()이 이 기능을 제공하므로 서비스 사이의 상호작용을 검증할 수 있습니다.

order-total.spec.ts

import {TestBed} from '@angular/core/testing';
import {afterEach, beforeEach, describe, expect, it, vi, type Mocked} from 'vitest';
import {OrderTotal} from './order-total';
import {TaxCalculator} from './tax-calculator';

const taxCalculatorStub: Mocked<TaxCalculator> = {
  calculate: vi.fn(),
};

describe('OrderTotal', () => {
  let service: OrderTotal;

  beforeEach(() => {
    taxCalculatorStub.calculate.mockReturnValue(5);

    TestBed.configureTestingModule({
      providers: [{provide: TaxCalculator, useValue: taxCalculatorStub}],
    });
    service = TestBed.inject(OrderTotal);
  });

  afterEach(() => {
    taxCalculatorStub.calculate.mockClear();
  });

  it('adds tax to the subtotal', () => {
    expect(service.total(100)).toBe(105);
  });

  // 스파이로 상호작용을 검증합니다
  it('calls the tax calculator', () => {
    service.total(100);
    expect(taxCalculatorStub.calculate).toHaveBeenCalledExactlyOnceWith(100);
  });
});

새 테스트는 OrderTotal이 합계를 계산할 때 TaxCalculator.calculate를 호출했는지 확인합니다. 서비스가 서로 올바르게 상호작용했는지 검증할 때 유용합니다.

HTTP 서비스 테스트하기

많은 서비스가 Angular HttpClient로 서버에서 데이터를 가져옵니다. Angular는 실제 네트워크 요청을 보내지 않고도 HTTP 응답을 제어할 수 있는 HttpClient 전용 테스트 유틸리티를 제공합니다.

HttpClient를 사용하는 서비스의 테스트 방법은 HTTP 테스트 문서를 참고하세요.