序章

Jasmine spies は、関数またはメソッドを追跡またはスタブ化するために使用されます。 スパイは、関数が呼び出されたかどうかを確認したり、カスタムの戻り値を提供したりする方法です。 スパイを使用して、サービスに依存するコンポーネントをテストし、実際にサービスのメソッドを呼び出して値を取得することを回避できます。 これにより、ユニットテストを、依存関係ではなく、コンポーネント自体の内部のテストに集中させることができます。

この記事では、AngularプロジェクトでJasmineスパイを使用する方法を学習します。

前提条件

このチュートリアルを完了するには、次のものが必要です。

このチュートリアルは、ノードv16.2.0、npm v7.15.1、および@angular/corev12.0.4で検証されました。

ステップ1—プロジェクトの設定

Angularの単体テストの概要で使用したものと非常によく似た例を使用してみましょう。

まず、@angular/cliを使用して新しいプロジェクトを作成します。

  1. ng new angular-test-spies-example

次に、新しく作成されたプロジェクトディレクトリに移動します。

  1. cd angular-test-spies-example

以前は、アプリケーションは2つのボタンを使用して、0〜15の値をインクリメントおよびデクリメントしていました。

このチュートリアルでは、ロジックをサービスに移動します。 これにより、複数のコンポーネントが同じ中心値にアクセスできるようになります。

  1. ng generate service increment-decrement

次に、コードエディタでincrement-decrement.service.tsを開き、コンテンツを次のコードに置き換えます。

src / app / 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を開き、コンテンツを次のコードに置き換えます。

src / app / 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を開き、コンテンツを次のコードに置き換えます。

src / app / 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を開き、次のコード行を変更します。

src / app / 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関数を使用してサービスメソッドを呼び出し、呼び出されたことをテストする方法は次のとおりです。

src / app / 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;
  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は、クラスインスタンス(この場合はサービスインスタンス)と、スパイするメソッドまたは関数の名前を持つ文字列値の2つの引数を取ります。

ここでは、スパイに.and.callThrough()もチェーンされているため、実際のメソッドは引き続き呼び出されます。 この場合のスパイは、メソッドが実際に呼び出されたかどうかを判断し、引数をスパイするためにのみ使用されます。

メソッドが2回呼び出されたことを表明する例を次に示します。

expect(incrementSpy).toHaveBeenCalledTimes(2);

引数'error'を使用してメソッドが呼び出されなかったことを表明する例を次に示します。

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

サービスのメソッドを実際に呼び出さないようにする場合は、スパイで.and.returnValueを使用できます。

私たちのサンプルメソッドは、何も返さず、代わりに内部プロパティを変更するため、これには適していません。

実際に値を返す新しいメソッドをサービスに追加してみましょう。

src / app / increment-decrement.service.ts
minimumOrMaximumReached() {
  return !!(this.message && this.message.length);
}

注:式の前に!!を使用すると、値がブール値に強制されます。

また、テンプレートが値を取得するために使用する新しいメソッドをコンポーネントに追加します。

src / app / app.component.ts
limitReached() {
  return this.incrementDecrement.minimumOrMaximumReached();
}

これで制限に達した場合、テンプレートにメッセージが表示されます。

src / app / app.component.html
<p class="message" *ngIf="limitReached()">
  Limit reached!
</p>

次に、サービスのメソッドを実際に呼び出すことなく、テンプレートメッセージが制限に達したかどうかを示すことをテストできます。

src / app / 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;
  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トピックページで演習とプログラミングプロジェクトを確認してください。