今まで実装してきたコードにユニットテストを追加していきます。本記事では、基本的なテストを書いて動くまでを体験することを目的にしますので、Jasmineのそもそもについては触れませんのでご了承ください。
各種バージョンは以下のとおりです。
- Angular CLI: 11.2.8
- Node: 15.10.0
- Angular Material: 11.2.8
TOC
今回の目標とソースコードについて
今回の目標です。
- 下記のユニットテストを追加する
- メインコンポーネントの初期時にesppDataがないこと
- 購入時価格から購入価格へ値がコピーされること
- ユーザが購入価格をタッチした場合には、購入時価格から購入価格へ値がコピーされないこと
save()
を呼び出すと、esppData
にデータが追加されることsave()
内の利益の計算が正しいこと
機能が増えてくると、今まで想定していたロジックが動かなくなることもよくありますので、テストを書いていくことは重要です。今回は、これまでに実装した関数が正しく動くことを確認するテストを書きます。
ちなみに、テストが動いてたときのイメージは以下のとおりです。
参考 今回のソースコードJasmine テストを起動する
以下のコマンドで現状のコードでテストを実施してみましょう。ちなみに現時点でのテストコードは以下のとおりです。
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に必要なモジュールがインポートされていないことを示します。そのため、FormsModule
とReactiveFormsModule
をテストのコードにもインポートすることで解決できます。テストコードにはできるだけ依存しないようにするため、必要なモジュールがあれば、都度追加する必要があります。変更箇所は以下のimports
の部分です。
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MainComponent ],
imports: [FormsModule,ReactiveFormsModule]
})
.compileComponents();
});
この変更により、エラーが解消されてshould create
というテストが成功したと思います。
データの初期値を確認するテスト
最初のテストを作成します。各テストのテンプレートは以下のようになります。
it('テスト名', () => {
// 準備
// テストしたい処理
// 確認
expect(true).toBeTrue();
});
まずは、コンポーネントのesppDataList
にデータがないことを確認するテストを作成します。
この場合の準備、テストしたい処理、確認は以下のとおりです。
- 準備
- メインコンポーネント作成
- テストしたい処理
- なし
- 確認
esppDataList
にデータがないこと
では、テストコードを見てみましよう。
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();
});
そのため、最終的に作成するテストコードは以下のとおりです。
it('should have no data', () => {
// 準備
// テストしたい処理
// 確認
expect(component.esppDataList.length).toEqual(0);
});
MainComponent配下にshould have no data
が作成され、緑色で表示されていればテストがパスしていることを示しています。
コンポーネントで独自に定義した関数のテスト
コンポーネント内で独自に作成した関数のテストを行います。例としてcopyToPurchasePrice()
を詳しく解説します。
テストのシナリオは以下の通りです。
- 準備
- メインコンポーネント作成(beforeEachで実施済み)
- フォームの
marketPrice
に120という値をセット
- テストしたい処理
copyToPurchasePrice()
呼び出し
- 確認
- フォームの
purchasePrice
に120という値がセットされていること
- フォームの
このまま、愚直にコードにしていけば問題ありません。先程と同様にコンポーネントの作成は省きます。
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
のは、最終的に以下の通りになります。
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コンテナとしてデプロイまで