続いて、React、Angular2それぞれの単体テストの実施方法を比較します。テストコード内で実行するテストケースは、Todoコンポーネントを対象とした下記3つです。
今回はReactとAngular2でテストするに当たって、表2のテスティングフレームワークを利用しています。
テスト対象 | テストフレームワーク | 説明 |
---|---|---|
React | Jest | Facebook社提供のフレームワーク。内部的にJasmineを使っており、テストコードの書き方は基本的にJasmineと同様となる |
Angular2 | Jasmine | テストコードを実行し、テスト対象となるアプリケーションが期待される状態にあるかどうかを検査するための仕組みを提供するためのテストフレームワーク。BDD(Behavior Driven Development)形式を採用しており、RSpecとよく似た記法で記述するのが特徴 |
Karma | さまざまなブラウザでテストを実行し、結果をまとめてレポートするためのテストランナー | |
Reactでは今回、「create-react-app」に内包されているテスティングフレームワーク「Jest」を使用します。
今回はサンプルとして、Todo.jsをテストするためのテストコードを作成します。テストファイルは「テスト対象のファイル名.spec.js」の形で作成します。
import React from 'react'; import TestUtils from 'react-addons-test-utils'; import Todo from '../components/Todo'; // コンポーネントの出力 function setup(propOverrides) { const props = Object.assign({ todo: { id: 1, text: 'テストTODO', isChecked: false, } }, propOverrides); const renderer = TestUtils.createRenderer(); renderer.render(<Todo {...props} />); const output = renderer.getRenderOutput(); return { props: props, output: output, }; } describe('should have li', () => { //コンポーネントの出力テスト it('draw component', () => { // コンポーネントの出力 const { output } = setup(); // liタグの存在チェック expect(output.type).toEqual('li'); }); //コンポーネント内テキストの出力テスト it('should contain prop text', () => { // コンポーネントに渡すpropの定義 const prop = { todo: { id: 2, text: 'Reactの学習', isChecked: false, }, checkTodo: jest.fn(), } // コンポーネントの出力 const { output } = setup(prop); const itemText = output.props.children; // コンポーネントに出力されたテキストのチェック expect(itemText).toEqual('Reactの学習'); }); // コンポーネントのcheck関数呼び出しテスト it('should be call checkTodo method by click event', () => { // ダミーの関数定義 const checkTodo = jest.fn(); const prop = { checkTodo: checkTodo, } // コンポーネントの出力 const { output } = setup(prop); // コンポーネントにセットされたダミー関数の起動 output.props.onClick(); // 関数の呼び出し回数のチェック expect(checkTodo.mock.calls.length).toEqual(1); }); });
それでは、具体的な実装方法について見ていきましょう。
const renderer = TestUtils.createRenderer(); renderer.render(<Todo {...props} />); const output = renderer.getRenderOutput();
Reactが提供するTestUtilsを使用し、コンポーネントを出力します。Render関数を使ってTodoコンポーネントに初期値を渡し、出力結果を受け取ります。
Todoコンポーネントはliタグを出力する作りになっていますので、liタグが出力されていることを確認することでTodoコンポーネントが正しく出力されていることを確認します。
it(' should have li ', () => { // コンポーネントの出力 const { output } = setup(); // liタグの存在チェック expect(output.type).toEqual('li'); });
Todoコンポーネントは受け取ったTodoのテキストを表示する作りになっているので、コンポーネントに渡した「Reactの学習」が出力されていることを確認します。
it('should contain prop text ', () => { // コンポーネントに渡すpropの定義 const prop = { todo: { id: 2, text: 'Reactの学習', isChecked: false, }, checkTodo: jest.fn() } // コンポーネントの出力 const { output } = setup(prop); const itemText = output.props.children; // コンポーネントに出力されたテキストのチェック expect(itemText).toEqual('Reactの学習'); });
受け取ったcheckTodo関数が実行できているか確認します。Jestのfnメソッドを利用し、「mock」と呼ばれるダミーの関数を生成し、Todoコンポーネントに渡します。TodoコンポーネントのonClickにセットされたファンクションを実行し、実行回数が1回カウントされていることをチェックします。
it('should be call checkTodo method by click event(', () => { // ダミーの関数定義 const checkTodo = jest.fn(); const prop = { checkTodo: checkTodo, } // コンポーネントの出力 const { output } = setup(prop); // コンポーネントにセットされたダミー関数の起動 output.props.onClick(); // 関数の呼び出し回数のチェック expect(checkTodo.mock.calls.length).toEqual(1); });
テストの実行は「create-react-app」にスクリプトが組み込まれていますので、下記コマンドで実行できます。
C:\workspace\todo-react>npm run test
テスト結果は下記の形でコンソールに出力されます。
PASS src\test\Todo.spec.js TODO Component √ draw component (3ms) √ draw text (2ms) √ check TODO Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 0.239s, estimated 1s Ran all test suites related to changed files.
コンソールに出力された結果から、全部で3ケース分のテストを実行し、全てにパスしていることが分かります。
Angular2では、Angular1と同様にKarma、Jasmineといった単体テストフレームワークを組み込みで利用できます。
ここでは、前述した3つのテストケースについて、Jasmineをベースにテストコードを記述し、それらをKarmaから実行し、テスト結果を確認しています。
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { TodoComponent } from './todo.component'; import { TodoService } from '../services/todo.service'; describe('TodoComponent', () => { // テスト対象のコンポーネント let component: TodoComponent; let fixture: ComponentFixture<TodoComponent>; //テスト事前準備 beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ TodoComponent ], providers: [ { provide: TodoService } ] }).compileComponents(); })); beforeEach(() => { // コンポーネントインスタンスの生成 fixture = TestBed.createComponent(TodoComponent); component = fixture.componentInstance; }); //コンポーネントの出力テスト it('should have li', () => { expect(fixture.debugElement.nativeElement.querySelectorAll('li').length).toEqual(1); }); //コンポーネント内テキストの出力テスト it('should contain prop text', () => { component.todo = { name: "Angular2の学習" ,enabled: true}; // コンポーネントの変更を検知し DOMを最新化する fixture.detectChanges(); expect(fixture.debugElement.nativeElement.querySelector('li').outerHTML).toContain("Angular2の学習"); }); //コンポーネントのcheck関数呼び出しテスト it('should be call checkTodo method by click event', () => { component.todo = { name: "title", enabled: true}; spyOn(component, "checkTodo"); // コンポーネントの変更を検知し、DOMを最新化する fixture.detectChanges(); let dom = fixture.debugElement.nativeElement.querySelector('li'); dom.dispatchEvent(new Event("click")); expect(component.checkTodo).toHaveBeenCalled(); }); });
次に、具体的な実装方法について見ていきます。
Reactと同様、まずは各テストで共通して実行する処理として、テスト対象のコンポーネントを出力します。
単体テスト対象のモジュールを管理するAPIであるTestBedを利用して、出力するコンポーネントの種類、コンポーネント内で利用するサービスの指定を行っています。
// テスト対象のコンポーネント let component: TodoComponent; let fixture: ComponentFixture<TodoComponent>; let el: HTMLElement; //テスト事前準備 beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ TodoComponent ], providers: [ { provide: TodoService } ] }).compileComponents(); }));
その上で各テストの実施前に、コンポーネントインスタンスの生成を行います。
beforeEach(() => { // コンポーネントインスタンスの生成 fixture = TestBed.createComponent(TodoComponent); component = fixture.componentInstance; el = fixture.debugElement.nativeElement; });
生成されたコンポーネントインスタンス内にliタグが存在していることを確認します。
it('should have li', () => { expect(el.querySelectorAll('li').length).toEqual(1); });
コンポーネントの持つプロパティを変更してテストする場合は、変更後にfixture.detectChanges関数を実行し、テスト利用するDOMを最新化する必要があります。
it('should contain prop text', () => { component.todo = { name: "Angular2の学習" ,enabled: true}; fixture.detectChanges(); expect(el.textContent).toContain("Angular2の学習"); });
関数が実行されているかの確認では、スパイ機能を利用しています。スパイを使うと関数単位で処理を置き換えたり、期待した通りに呼び出されたかを検証できます。テスト内では、コンポーネントを監視対象として設定した後で、toHaveBeenCalled関数を用いて呼び出しされたかの検証を行っています。
it('should be call onClick method by click event', () => { component.todo = { name: "title" ,enabled: true}; spyOn(component, "onClick"); fixture.detectChanges(); let dom = el.querySelector('li'); dom.dispatchEvent(new Event("click")); expect(component.onClick).toHaveBeenCalled(); });
テストの実行は「angular-cli」にスクリプトが組み込まれていますので、下記コマンドで実行できます。
C:\workspace\todo-angular2>ng test
テスト結果は下記の形でコンソールに出力されます。
Chrome 58.0.3029 (Windows 10 0.0.0): Executed 3 of 3 SUCCESS (0.232 secs / 0.223 secs)
全部で3ケース分のテストが実行され、全てのテストが成功していることが分かります。
サンプルアプリを通してReact、Angular2での仕組みや実装を比較してきましたが、特にコンポーネントの作成方法や状態管理の方法に違いがあることが分かりました。
記事 | 観点 | React | Angular2 |
---|---|---|---|
第2回 | 開発言語 | JavaScript+BabelまたはJavaScript+TypeScript(※Babelが主流) | JavaScript+BabelまたはJavaScript+TypeScript(※TypeScriptが主流) |
第2回 | コンポーネントの作成 | JavaScript内にJSX(HTMLライクなReact構文)を記述 | HTML内にAngular2の構文を記述 |
第3回 | 状態管理 | コンポーネントで管理(※状態を集中管理させるためのアーキテクチャ(Flux)が存在) | DIの仕組みを用いて、サービスで管理(※ライブラリを用いて、Fluxアーキテクチャを適用することも可能) |
第3回 | CSSの適用 | css-modulesを組み込むことで、コンポーネント単位で適用が可能 | コンポーネント単位で適用が可能 |
第3回 | 単体テスト | テスティングフレームワークを自由に選択可能 | KarmaやJasmineが標準で組み込まれている |
実装方法に違いはありましたが、どちらもフレームワークとしてさまざまな仕組みを備えており、実現できることはほぼ同じです。
フレームワークとしての大きな違いは、Reactはviewのみを提供するライブラリであるため、他のReact関連ライブラリと組み合わせて使っていく必要があります。反対にAngular2はフルスタックに機能を提供してくれます。本連載では触れませんでしたが、例えばSPAでの画面遷移や通信処理などの実装に当たり、Reactは他のライブラリを組み込む必要があるのに対して、Angular2はフレームワークに機能を内包しています。そのため、Reactでは柔軟に構成を組める半面、個別にライブラリの選定が必要となります。Angular2はフレームワーク単体で一通りのことができますが、ロックインが強くなります。
3回にわたりSPAについて解説をしてきましたが、いかがでしたでしょうか。フロント開発において非常にホットな技術となっているSPAは、さまざまなWebサービスはもちろんのこと、業務アプリケーションの領域でも使われ始めています。
既にSPAに取り組んでいる方や、これから取り組みを検討されている方に、本連載が何らかの形でお役に立ちましたら幸いです。
Copyright © ITmedia, Inc. All Rights Reserved.