ReactとAngular2の使い方やコードの違いを状態管理、CSS適用、単体テストで比較するモダンなフロントエンド開発者になるためのSPA超入門(終)(3/3 ページ)

» 2017年08月07日 05時00分 公開
[千葉達仁, 日野達也三菱総研DCS]
前のページへ 1|2|3       

コンポーネントの単体テスト

 続いて、React、Angular2それぞれの単体テストの実施方法を比較します。テストコード内で実行するテストケースは、Todoコンポーネントを対象とした下記3つです。

  • Todoコンポーネントがliタグを1つ持つこと
  • 登録したTodoのタイトルが、liタグに表示されていること
  • Todoコンポーネントをクリックした際に、指定の関数が実行されること。

 今回はReactとAngular2でテストするに当たって、表2のテスティングフレームワークを利用しています。

表2 利用したテストフレームワーク
テスト対象 テストフレームワーク 説明
React Jest Facebook社提供のフレームワーク。内部的にJasmineを使っており、テストコードの書き方は基本的にJasmineと同様となる
Angular2 Jasmine テストコードを実行し、テスト対象となるアプリケーションが期待される状態にあるかどうかを検査するための仕組みを提供するためのテストフレームワーク。BDD(Behavior Driven Development)形式を採用しており、RSpecとよく似た記法で記述するのが特徴
Karma さまざまなブラウザでテストを実行し、結果をまとめてレポートするためのテストランナー

Jestを使ったReactの単体テスト

 Reactでは今回、「create-react-app」に内包されているテスティングフレームワーク「Jest」を使用します。

図6 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);
  });
});
Todo.spec.js

 それでは、具体的な実装方法について見ていきましょう。

  • コンポーネントの出力
  const renderer = TestUtils.createRenderer();
  renderer.render(<Todo {...props} />);
  const output = renderer.getRenderOutput();
Todo.spec.js

 Reactが提供するTestUtilsを使用し、コンポーネントを出力します。Render関数を使ってTodoコンポーネントに初期値を渡し、出力結果を受け取ります。

  • タグの表示確認

 Todoコンポーネントはliタグを出力する作りになっていますので、liタグが出力されていることを確認することでTodoコンポーネントが正しく出力されていることを確認します。

  it(' should have li ', () => {
    // コンポーネントの出力
    const { output } = setup();
    // liタグの存在チェック
    expect(output.type).toEqual('li');
  });
Todo.spec.js
  • Todoテキストの表示

 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の学習');
  });
Todo.spec.js
  • checkTodo関数の確認

 受け取った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);
  });
Todo.spec.js
  • テストの実行

 テストの実行は「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ケース分のテストを実行し、全てにパスしていることが分かります。

JasmineとKarmaを使ったAngular2の単体テスト

 Angular2では、Angular1と同様にKarmaJasmineといった単体テストフレームワークを組み込みで利用できます。

 ここでは、前述した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();
  });
});
todo.component.spec.ts

 次に、具体的な実装方法について見ていきます。

  • コンポーネントの出力

 Reactと同様、まずは各テストで共通して実行する処理として、テスト対象のコンポーネントを出力します。

 単体テスト対象のモジュールを管理するAPIであるTestBedを利用して、出力するコンポーネントの種類、コンポーネント内で利用するサービスの指定を行っています。

  // テスト対象のコンポーネント
  let component: TodoComponent;
  let fixture: ComponentFixture<TodoComponent>;
  let el:      HTMLElement;
  //テスト事前準備
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ TodoComponent ],
      providers: [
       { provide: TodoService }
      ]
    }).compileComponents();
  }));
todo.component.spec.ts

 その上で各テストの実施前に、コンポーネントインスタンスの生成を行います。

  beforeEach(() => {
    // コンポーネントインスタンスの生成
    fixture = TestBed.createComponent(TodoComponent);
    component = fixture.componentInstance;
    el = fixture.debugElement.nativeElement;
  });
todo.component.spec.ts
  • タグの表示確認

 生成されたコンポーネントインスタンス内にliタグが存在していることを確認します。

  it('should have li', () => {
    expect(el.querySelectorAll('li').length).toEqual(1);
  });
todo.component.spec.ts
  • Todoテキストの表示

 コンポーネントの持つプロパティを変更してテストする場合は、変更後にfixture.detectChanges関数を実行し、テスト利用するDOMを最新化する必要があります。

  it('should contain prop text', () => {
    component.todo = { name: "Angular2の学習" ,enabled: true};
    fixture.detectChanges();
    expect(el.textContent).toContain("Angular2の学習");
  });
todo.component.spec.ts
  • checkTodo関数の確認

 関数が実行されているかの確認では、スパイ機能を利用しています。スパイを使うと関数単位で処理を置き換えたり、期待した通りに呼び出されたかを検証できます。テスト内では、コンポーネントを監視対象として設定した後で、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();
  });
todo.component.spec.ts
  • テストの実行

 テストの実行は「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での仕組みや実装を比較してきましたが、特にコンポーネントの作成方法や状態管理の方法に違いがあることが分かりました。

表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に取り組んでいる方や、これから取り組みを検討されている方に、本連載が何らかの形でお役に立ちましたら幸いです。

前のページへ 1|2|3       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。