お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

Angular でテストを書く(サービス編)

Mac に慣れません。身体が Windows でできているので Mac を受け付けないのかもしれません。 最近、五等分の花嫁にハマっておりスマホの壁紙やキーボードの見た目を五等分の花嫁仕様にして楽しんでいます。Kindle で買った後に物理的に欲しくなったので全巻買いに行ったら8巻がありませんでした(そのとき9巻まだ出てなかった)。

f:id:pickles-ochazuke:20190421223158j:plain

全員可愛くて好きですがあえて一番を上げるなら三玖です。そろそろ本題に入ります(9巻の表紙ヤバいですね!)。

はじめ

Angular は標準でテストを行う機能があるので、簡単に実行することができます。

テストは、 ng test で行うことができます。

プロジェクトの作成

ng new angular-test で作成しました。

router はなし、CSSは標準のCSSを選択しました。今回はどっちも関係ないのでたぶんどっちでもいいです。

ng test でテストが可能なことを確認します。

とりあえず、放り投げた値を計算して画面に表示するようにします。

ng serve --open

html を以下のように変更します。

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<div>
  5 + 4 = 9
</div>

これから、このコンポーネントで足し算をする画面を作成して、そのテストを書くことを目指します。

テストの作成(コンポーネント

実は初めからテストコードが用意されています。 app.component.spec.ts がそれです。テストファイルは、.spec.ts という拡張子(?)です。

中身を見ると以下のようなコードが書かれています。

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'angular-test'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('angular-test');
  });

  it('should render title in a h1 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-test!');
  });
});

初めて目にすると「う゛っ……」ってなるかもしれません。自分はなりました。

今回の話では見る必要はないので、読み飛ばしても大丈夫ですが、気になる人は、以下を一読すると参考になるかも。

少し目を慣らすために、まずは大きく分類していきます。 自分は、このテストコードは、大きな項目とその中にある小さな項目の集まりだと思っています。 大きな項目は、describe で始まる関数で、小さな項目は、it で始まる関数です。 どちらも一つ目の引数に文字列が渡され、二つ目の引数に無名関数が渡されています。

文字列は、テストを実行した時に、実行者がなんのテストを行って、どのテストが失敗したか、成功したのかを簡単に判別するための文字です。要は何をテストしているのかを説明するような文章を書くといいでしょう。当然日本語が書けるし、書くべきです。周りの母国語が英語なら英語にするべきだろうけど。

describe は小さなテストを集めた親みたいなもので、テスト自体はその中にある it がテスト内容になります。 it の中では、大抵以下の順番でテストを書きます。

  1. テストの前準備
  2. テスト対象の処理を実行
  3. 処理結果が期待する結果になっているか判定

といった感じです。 ここで、テストを書いていると各 it で共通の初期化処理があることに気づいたりします。そういった共通処理は、beforeEach に書きます。beforeEach は、it を実行する時に最初に勝手に呼ばれます。

期待値の比較は、いろいろメソッドが存在しますが、基本は、expect(比較対象).比較方法(期待値) のように書きます。 上のコードを見れば何となくわかるかもしれないですが。

初めから用意されているテストコードは、Angular 固有のコンポーネントクラス(呼び方あってる?)に対してテストを行うテストになっています。実は、コンポーネントのテストは面倒くさいし難しいです(設計に問題があったのだろうか……)。まずはサービスで慣れていくのがいいと思います。

サービスとそのテストの作成

サービスでテストコードを書く前に、画面に変更を加えて渡した値を足し算してくれるようにしたいと思います。

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">

<div>
  {{right_operand}} + {{left_operand}} = {{result}}
</div>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'angular-test';
  right_operand = 10;
  left_operand = 20;
  result = this.right_operand + this.left_operand;
}

コンポーネントに存在する right_operand, left_operand, result をビューにバインドして表示しているだけです。 これぐらいなら既存のテストコードに追加するだけでいいんですが、厄介なのが画面のために追加した機能がある場合。 例えば、ダイアログを追加したり、他のコンポーネントを追加したりすると、テストの準備だけで大変になったりします。

ロジック部分だけテストしたいのに、一切使わないものを作らないといけないのは煩わしいので、足し算のようなロジックや、保持しておくデータは積極的にサービスに追いやります。

ng generate service app でサービスを作成します。

以下のようなコードが作成されます。

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

@Injectable({
  providedIn: 'root'
})
export class AppService {

  constructor() { }
}

テストも作られているので、そちらも見てみます。

import { TestBed } from '@angular/core/testing';

import { AppService } from './app.service';

describe('AppService', () => {
  beforeEach(() => TestBed.configureTestingModule({}));

  it('should be created', () => {
    const service: AppService = TestBed.get(AppService);
    expect(service).toBeTruthy();
  });
});

短くてまだ理解しやすそうです。コンポーネントのテストが面倒くさいというのが少し理解できましたでしょうか。 以下のようなテストを追加します。

  it('1 + 1 の結果は 2 であるべき', () => {
    const service: AppService = TestBed.get(AppService);
    expect(1+1).toEqual(5000);
  });

追加されて、成功しましたか? いまいち成功したかわからなければ、失敗を書いてみましょう。 例えば、最後をexpect(1+1).toEqual(5000); のようにします。 失敗すると、赤いバーが表示され、5 specs, 1 failure と表示されます。

動作確認ができたので、実際にサービスに組み込んでテストをします。 まずはテストを変更しましょう。サービスには、足し算する対象を二つ保持し、それらを足し合わせた結果を持ちたいサービスだとします。以下のようなテストを考えました。

  it('right_operand に値を渡すと渡した値を持つ', () => {
    const service: AppService = TestBed.get(AppService);

    service.right_operand = 10;
    
    expect(service.right_operand).toEqual(10);
  });

残念ながら、テストは失敗しません(コンパイルエラーは起こりますが)。エラーを起こさないようにサービス側に変数を追加します。変数は private にしましょう。

export class AppService {

  private rightOperand: number = 0;

  constructor() { }
}

まだエラーが出ます。private なので アクセサ(setter) を作ってやる必要があります。以下のようになりました。

export class AppService {
  private rightOperand: number = 0;

  constructor() { }

  public set rightOperand(v : number) {
    this._rightOperand = v;
  }
}

まだエラーが出ます。Expected undefined to equal 10. どうやら戻り値が未定義になっていて、undefined10 を比較しているようです。 今度は、getter を作ってやりましょう。

export class AppService {
  private _rightOperand: number = 0;

  constructor() { }

  public set rightOperand(v : number) {
    this._rightOperand = v;
  }
  
  public get rightOperand() : number {
    return this._rightOperand;
  }
}

テストが成功しました! leftOperand にも同じ実装を施して、最後に足し算をテストします。 以下のようなテストを作りました。

  it('3 + 4 = 7 であるべき', () => {
    const service: AppService = TestBed.get(AppService);

    service.leftOperand = 3;
    service.rightOperand = 4;

    expect(service.addOperand()).toEqual(7);
  });

とても簡単なのでサービス側の実装は、載せません。

最後に、コンポーネント側のオペランドたちをサービスに置き換えます。

export class AppComponent {
  title = 'angular-test';

  constructor(private appService: AppService) {
    appService.rightOperand = 30;
    appService.leftOperand = 20;
  }
}
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>

<div>
  {{appService.rightOperand}} + {{appService.leftOperand}} = {{appService.addOperand()}}
</div>

これで Angular のテストを行える知恵を身につけることができました!

参考

angular.jp