웹사이트 검색

각도 테스트에서 스파이를 사용하는 방법


소개

Jasmine 스파이는 함수 또는 메서드를 추적하거나 스텁하는 데 사용됩니다. 스파이는 함수가 호출되었는지 확인하거나 사용자 지정 반환 값을 제공하는 방법입니다. 스파이를 사용하여 서비스에 의존하는 구성 요소를 테스트하고 실제로 값을 얻기 위해 서비스의 메서드를 호출하는 것을 피할 수 있습니다. 이렇게 하면 종속성 대신 구성 요소 자체의 내부 테스트에 단위 테스트를 집중할 수 있습니다.

이 기사에서는 Angular 프로젝트에서 Jasmine 스파이를 사용하는 방법을 배웁니다.

전제 조건

이 자습서를 완료하려면 다음이 필요합니다.

  • Node.js를 로컬에 설치했습니다. Node.js를 설치하고 로컬 개발 환경을 만드는 방법에 따라 수행할 수 있습니다.
  • Angular 프로젝트 설정에 어느 정도 익숙합니다.

이 튜토리얼은 Node v16.2.0, npm v7.15.1 및 @angular/core v12.0.4에서 확인되었습니다.

1단계 - 프로젝트 설정

Angular의 단위 테스트 소개에서 사용한 것과 매우 유사한 예를 사용하겠습니다.

먼저 @angular/cli를 사용하여 새 프로젝트를 만듭니다.

  1. ng new angular-test-spies-example

그런 다음 새로 생성된 프로젝트 디렉토리로 이동합니다.

  1. cd angular-test-spies-example

이전에는 응용 프로그램에서 두 개의 버튼을 사용하여 0에서 15 사이의 값을 늘리거나 줄였습니다.

이 자습서에서는 논리를 서비스로 이동합니다. 이렇게 하면 여러 구성 요소가 동일한 중앙 값에 액세스할 수 있습니다.

  1. ng generate service increment-decrement

그런 다음 코드 편집기에서 increment-decrement.service.ts를 열고 콘텐츠를 다음 코드로 바꿉니다.

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

@Injectable({
  providedIn: 'root'
})
export class IncrementDecrementService {
  value = 0;
  message!: string;

  increment() {
    if (this.value < 15) {
      this.value += 1;
      this.message = '';
    } else {
      this.message = 'Maximum reached!';
    }
  }

  decrement() {
    if (this.value > 0) {
      this.value -= 1;
      this.message = '';
    } else {
      this.message = 'Minimum reached!';
    }
  }
}

코드 편집기에서 app.component.ts를 열고 콘텐츠를 다음 코드로 바꿉니다.

import { Component } from '@angular/core';
import { IncrementDecrementService } from './increment-decrement.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor(public incrementDecrement: IncrementDecrementService) { }

  increment() {
    this.incrementDecrement.increment();
  }

  decrement() {
    this.incrementDecrement.decrement();
  }
}

코드 편집기에서 app.component.html을 열고 콘텐츠를 다음 코드로 바꿉니다.

<h1>{{ incrementDecrement.value }}</h1>

<hr>

<button (click)="increment()" class="increment">Increment</button>

<button (click)="decrement()" class="decrement">Decrement</button>

<p class="message">
  {{ incrementDecrement.message }}
</p>

그런 다음 코드 편집기에서 app.component.spec.ts를 열고 다음 코드 줄을 수정합니다.

import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let debugElement: DebugElement;
  let incrementDecrementService: IncrementDecrementService;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers: [ IncrementDecrementService ]
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    debugElement = fixture.debugElement;

    incrementDecrementService = debugElement.injector.get(IncrementDecrementService);
  }));

  it('should increment in template', () => {
    debugElement
      .query(By.css('button.increment'))
      .triggerEventHandler('click', null);

    fixture.detectChanges();

    const value = debugElement.query(By.css('h1')).nativeElement.innerText;

    expect(value).toEqual('1');
  });

  it('should stop at 15 and show maximum message', () => {
    incrementDecrementService.value = 15;
    debugElement
      .query(By.css('button.increment'))
      .triggerEventHandler('click', null);

    fixture.detectChanges();

    const value = debugElement.query(By.css('h1')).nativeElement.innerText;
    const message = debugElement.query(By.css('p.message')).nativeElement.innerText;

    expect(value).toEqual('15');
    expect(message).toContain('Maximum');
  });
});

debugElement.injector.get을 사용하여 삽입된 서비스에 대한 참조를 얻는 방법에 주목하십시오.

이 방법으로 구성 요소를 테스트하는 것은 가능하지만 서비스에 대한 실제 호출도 이루어지며 구성 요소는 별도로 테스트되지 않습니다. 다음으로 스파이를 사용하여 메서드가 호출되었는지 확인하거나 스텁 반환 값을 제공하는 방법을 살펴보겠습니다.

2단계 - 서비스 메서드 염탐

Jasmine의 spyOn 함수를 사용하여 서비스 메서드를 호출하고 호출되었는지 테스트하는 방법은 다음과 같습니다.

import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let debugElement: DebugElement;
  let incrementDecrementService: IncrementDecrementService;
  let incrementSpy: any;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers: [ IncrementDecrementService ]
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    debugElement = fixture.debugElement;

    incrementDecrementService = debugElement.injector.get(IncrementDecrementService);
    incrementSpy = spyOn(incrementDecrementService, 'increment').and.callThrough();
  }));

  it('should call increment on the service', () => {
    debugElement
      .query(By.css('button.increment'))
      .triggerEventHandler('click', null);

    expect(incrementDecrementService.value).toBe(1);
    expect(incrementSpy).toHaveBeenCalled();
  });
});

spyOn은 클래스 인스턴스(이 경우 서비스 인스턴스)와 스파이할 메서드 또는 함수의 이름이 포함된 문자열 값의 두 가지 인수를 사용합니다.

여기서 우리는 또한 스파이에 .and.callThrough()를 연결했습니다. 그래서 실제 메서드는 여전히 호출될 것입니다. 이 경우 스파이는 메서드가 실제로 호출되었는지 확인하고 인수를 감시하는 데에만 사용됩니다.

다음은 메서드가 두 번 호출되었다고 어설션하는 예입니다.

expect(incrementSpy).toHaveBeenCalledTimes(2);

다음은 인수 error를 사용하여 메서드가 호출되지 않았다고 주장하는 예입니다.

expect(incrementSpy).not.toHaveBeenCalledWith('error');

서비스에서 메소드를 실제로 호출하지 않으려면 스파이에서 .and.returnValue를 사용할 수 있습니다.

우리의 예제 메서드는 아무것도 반환하지 않고 대신 내부 속성을 변경하기 때문에 이에 대한 좋은 후보가 아닙니다.

실제로 값을 반환하는 새 메서드를 서비스에 추가해 보겠습니다.

minimumOrMaximumReached() {
  return !!(this.message && this.message.length);
}

참고: 식 앞에 !!를 사용하면 값이 부울로 강제 변환됩니다.

또한 값을 가져오기 위해 템플릿에서 사용할 새 메서드를 구성 요소에 추가합니다.

limitReached() {
  return this.incrementDecrement.minimumOrMaximumReached();
}

이제 다음과 같이 제한에 도달하면 템플릿에 메시지가 표시됩니다.

<p class="message" *ngIf="limitReached()">
  Limit reached!
</p>

그런 다음 실제로 서비스에서 메서드를 호출하지 않고도 제한에 도달하면 템플릿 메시지가 표시되는지 테스트할 수 있습니다.

import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let debugElement: DebugElement;
  let incrementDecrementService: IncrementDecrementService;
  let minimumOrMaximumSpy: any;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers: [ IncrementDecrementService ]
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    debugElement = fixture.debugElement;

    incrementDecrementService = debugElement.injector.get(IncrementDecrementService);
    minimumOrMaximumSpy = spyOn(incrementDecrementService, 'minimumOrMaximumReached').and.returnValue(true);
  }));

  it(`should show 'Limit reached' message`, () => {
    fixture.detectChanges();

    const message = debugElement.query(By.css('p.message')).nativeElement.innerText;

    expect(message).toEqual('Limit reached!');
  });
});

결론

이 기사에서는 Angular 프로젝트에서 Jasmine 스파이를 사용하는 방법을 배웠습니다.

Angular에 대해 자세히 알아보려면 연습 및 프로그래밍 프로젝트에 대한 Angular 주제 페이지를 확인하세요.