Angular Jasmine でテストをかいてみる

今まで実装してきたコードにユニットテストを追加していきます。本記事では、基本的なテストを書いて動くまでを体験することを目的にしますので、Jasmineのそもそもについては触れませんのでご了承ください。

注意
本連載は、基本的に前回までの作業が終わっていることを前提としています。最初から試したい方は以前の記事も参考にしてください。また、今回から作成するWebページはESPPなどで必要な情報を入れると確定申告に必要な情報をだしてくれるものを目指しますが、確定申告に必要な情報を保証するようなものではないので、了承の上使用ください。
Angular Material Table データの追加

各種バージョンは以下のとおりです。

  • Angular CLI: 11.2.8
  • Node: 15.10.0
  • Angular Material: 11.2.8

今回の目標とソースコードについて

今回の目標です。

  • 下記のユニットテストを追加する
    • メインコンポーネントの初期時にesppDataがないこと
    • 購入時価格から購入価格へ値がコピーされること
    • ユーザが購入価格をタッチした場合には、購入時価格から購入価格へ値がコピーされないこと
    • save()を呼び出すと、esppDataにデータが追加されること
    • save()内の利益の計算が正しいこと

機能が増えてくると、今まで想定していたロジックが動かなくなることもよくありますので、テストを書いていくことは重要です。今回は、これまでに実装した関数が正しく動くことを確認するテストを書きます。

ちなみに、テストが動いてたときのイメージは以下のとおりです。

参考 今回のソースコード

Jasmine テストを起動する

以下のコマンドで現状のコードでテストを実施してみましょう。ちなみに現時点でのテストコードは以下のとおりです。

main.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { MainComponent } from './main.component';

describe('MainComponent', () => {
  let component: MainComponent;
  let fixture: ComponentFixture<MainComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ MainComponent ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MainComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

以下のコマンドで、テスト起動してください。

npm test

お使いの環境に依存しますが、ブラウザが開いて下記のようなページが出てきたかと思います。

MainComopnent作成時に作られた初期のテストが失敗していることがわかります。では、変更を加えていきます。

NullInjectorError: No provider for FormBuilder! の解消

NullInjectorError: No provider for FormBuilder! このエラーメッセージは、FormBuilderに必要なモジュールがインポートされていないことを示します。そのため、FormsModuleReactiveFormsModuleをテストのコードにもインポートすることで解決できます。テストコードにはできるだけ依存しないようにするため、必要なモジュールがあれば、都度追加する必要があります。変更箇所は以下のimportsの部分です。

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ MainComponent ],
      imports: [FormsModule,ReactiveFormsModule]
    })
    .compileComponents();
  });

この変更により、エラーが解消されてshould createというテストが成功したと思います。

データの初期値を確認するテスト

最初のテストを作成します。各テストのテンプレートは以下のようになります。

  it('テスト名', () => {
    // 準備

    // テストしたい処理

    // 確認
    expect(true).toBeTrue();
  });

まずは、コンポーネントのesppDataListにデータがないことを確認するテストを作成します。
この場合の準備、テストしたい処理、確認は以下のとおりです。

  • 準備
    • メインコンポーネント作成
  • テストしたい処理
    • なし
  • 確認
    • esppDataListにデータがないこと

では、テストコードを見てみましよう。

main.component.spec.ts
  it('should have no data', () => {
    // 準備
    fixture = TestBed.createComponent(MainComponent);

    // テストしたい処理

    // 確認
    expect(fixture.componentInstance.esppDataList.length).toEqual(0);
  });

fixture = TestBed.createComponent(MainComponent);でコンポーネントを作成し、expect(fixture.componentInstance.esppDataList.length).toEqual(0);esppDataListに一つもデータがないことを確認しています。今回コンポーネントの作成をitのテスト関数内で行っておりますが、beforeEachという各テスト実施前に呼ばれる関数ですでに行っているため、コンポーネント作成処理は不要です。
ちなみに、該当のbeforeEachは以下の部分です。

  beforeEach(() => {
    fixture = TestBed.createComponent(MainComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

そのため、最終的に作成するテストコードは以下のとおりです。

main.component.spec.ts
  it('should have no data', () => {
    // 準備

    // テストしたい処理

    // 確認
    expect(component.esppDataList.length).toEqual(0);
  });

MainComponent配下にshould have no dataが作成され、緑色で表示されていればテストがパスしていることを示しています。

コンポーネントで独自に定義した関数のテスト

コンポーネント内で独自に作成した関数のテストを行います。例としてcopyToPurchasePrice()を詳しく解説します。
テストのシナリオは以下の通りです。

  • 準備
    • メインコンポーネント作成(beforeEachで実施済み)
    • フォームのmarketPriceに120という値をセット
  • テストしたい処理
    • copyToPurchasePrice()呼び出し
  • 確認
    • フォームのpurchasePriceに120という値がセットされていること

このまま、愚直にコードにしていけば問題ありません。先程と同様にコンポーネントの作成は省きます。

main.component.spec.ts
  it('should copy marketPrice to purchasePrice', () => {
    // 準備
    component.esppForm.get("marketPrice")?.setValue(120);
    
    // テストしたい処理
    component.copyToPurchasePrice();

    // 確認
    expect(component.esppForm.get("purchasePrice")?.value).toEqual(120);    
  });

各処理は簡単なもので、見ていただければ理解できるかと思います。テストコードにパスするとブラウザ内で、下記のように新しいテストが成功したことを示すページが表示されます。

ちなみに、故意に失敗させると以下のとおりです。0ではなく120という値が格納されているとエラーを表示しながら、テストに失敗したことを教えてくれます。

では、他に予定していた下記のテストも同様に実装します。

  • ユーザが購入価格をタッチした場合には、購入時価格から購入価格へ値がコピーされないこと
  • save()を呼び出すと、esppDataにデータが追加されること
  • save()内の利益の計算が正しいこと

それらのコードは以下のとおりになります。難しいことは行っていないので、それぞれ見ていただくことでご理解いただけるかなと思います。

  // ユーザが購入価格をタッチした場合には、購入時価格から購入価格へ値がコピーされないこと
  it("should not copy marketPrice to purchasePrice if user change purchasePrice value", () => {
    // 準備
    fixture.componentInstance.esppForm.get("marketPrice")?.setValue(120);
    fixture.componentInstance.esppForm.get("purchasePrice")?.markAsTouched();

    // テストしたい処理
    fixture.componentInstance.copyToPurchasePrice();

    // 確認
    expect(fixture.componentInstance.esppForm.get("purchasePrice")?.value).toEqual(0);
  });

  // save()を呼び出すと、esppDataにデータが追加されること
  it("should save a new espp data", () => {
    // 準備
    fixture.componentInstance.esppForm.get("name")?.setValue("ABC");
    fixture.componentInstance.esppForm.get("purchaseDate")?.setValue(0);
    fixture.componentInstance.esppForm.get("quantity")?.setValue(10);
    fixture.componentInstance.esppForm.get("marketPrice")?.setValue(120);
    fixture.componentInstance.esppForm.get("purchasePrice")?.setValue(100);

    // テストしたい処理
    fixture.componentInstance.save();

    // 確認
    expect(fixture.componentInstance.esppDataList.length).toEqual(1);
  });

  // save()内の利益の計算が正しいこと
  it("should calculate profit", () => {
    // 準備
    fixture.componentInstance.esppForm.get("name")?.setValue("ABC");
    fixture.componentInstance.esppForm.get("purchaseDate")?.setValue(0);
    fixture.componentInstance.esppForm.get("quantity")?.setValue(10);
    fixture.componentInstance.esppForm.get("marketPrice")?.setValue(120);
    fixture.componentInstance.esppForm.get("purchasePrice")?.setValue(100);

    // テストしたい処理
    fixture.componentInstance.save();

    // 確認
    expect(fixture.componentInstance.esppDataList[0].profit).toEqual(200);
  });

最後に、テストが期待通り動くと、下記のようなページが表示されます。

変更点とまとめ

今回は、メインコンポーネントのテストを追加しました。main.component.spec.tsのは、最終的に以下の通りになります。

main.component.spec.ts
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";

import { MainComponent } from "./main.component";

describe("MainComponent", () => {
  let component: MainComponent;
  let fixture: ComponentFixture<MainComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [MainComponent],
      imports: [FormsModule, ReactiveFormsModule],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MainComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it("should create", () => {
    expect(component).toBeTruthy();
  });

  it("should have no data", () => {
    // 準備

    // テストしたい処理

    // 確認
    expect(component.esppDataList.length).toEqual(0);
  });

  it("should copy marketPrice to purchasePrice", () => {
    // 準備
    component.esppForm.get("marketPrice")?.setValue(120);

    // テストしたい処理
    component.copyToPurchasePrice();

    // 確認
    expect(component.esppForm.get("purchasePrice")?.value).toEqual(120);
  });

  it("should not copy marketPrice to purchasePrice if user change purchasePrice value", () => {
    // 準備
    fixture.componentInstance.esppForm.get("marketPrice")?.setValue(120);
    fixture.componentInstance.esppForm.get("purchasePrice")?.markAsTouched();

    // テストしたい処理
    fixture.componentInstance.copyToPurchasePrice();

    // 確認
    expect(fixture.componentInstance.esppForm.get("purchasePrice")?.value).toEqual(0);
  });

  it("should save a new espp data", () => {
    // 準備
    fixture.componentInstance.esppForm.get("name")?.setValue("ABC");
    fixture.componentInstance.esppForm.get("purchaseDate")?.setValue(0);
    fixture.componentInstance.esppForm.get("quantity")?.setValue(10);
    fixture.componentInstance.esppForm.get("marketPrice")?.setValue(120);
    fixture.componentInstance.esppForm.get("purchasePrice")?.setValue(100);

    // テストしたい処理
    fixture.componentInstance.save();

    // 確認
    expect(fixture.componentInstance.esppDataList.length).toEqual(1);
  });

  it("should calculate profit", () => {
    // 準備
    fixture.componentInstance.esppForm.get("name")?.setValue("ABC");
    fixture.componentInstance.esppForm.get("purchaseDate")?.setValue(0);
    fixture.componentInstance.esppForm.get("quantity")?.setValue(10);
    fixture.componentInstance.esppForm.get("marketPrice")?.setValue(120);
    fixture.componentInstance.esppForm.get("purchasePrice")?.setValue(100);

    // テストしたい処理
    fixture.componentInstance.save();

    // 確認
    expect(fixture.componentInstance.esppDataList[0].profit).toEqual(200);
  });
});
参考 今回のソースコード

本番のアプリケーションでは、このような簡単なテストだけというわけにはいかないと思いますが、書き方の基本は一緒です。準備->テストの対象となる処理->確認の処理の流れと、テストに必要な関数やツールの使い方を覚えていけば様々なテストを書くことができるようになります。

次回以降で、実際にデプロイする方法について触れていきます。

Angular ビルドしてDockerコンテナとしてデプロイまで

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA