Mainly Devel Notes

Twitter, GitHub, StackOverflow: @ovrmrw (short hand of "overmorrow" that means the day after tomorrow)

Angular2でDI(依存性注入)してテスト(Jasmine)を書いてみた。

Angular2, Dependency Injection, テスト(Jasmine), TypeScript

【注】この記事ではAngular2 alpha.47を前提としています。それ以降のバージョンだと色々細かいところで違いがあるので注意してください。

Angular2 の Dependency Injection とテストについて、 以下の公式サイトで学んだことを少しまとめてみました。

5 MIN QUICKSTART
TUTORIAL: TOUR OF HEROES
TESTING GUIDES

上記を一通り通読すると大体のことは分かるようになっていますので、英語ですがなるべく読むことをおすすめします。
またこの記事はAngular2 for TypeScriptの公式チュートリアルを少しアレンジして遊んでみた。の続きです。前回と重複する部分は説明を端折っているかもしれません。

(2015/11/21 この記事の続きを書きました。→Angular2でDIしてテストを書いたけどhtmlファイルの重複をなんとかしたかった。)

まずは準備

npm init -y
npm install angular2@2.0.0-alpha.47 --save --save-exact 
npm install systemjs jquery --save
npm install live-server typescript jasmine-core --save-dev
tsc --init
tsd install jquery --save

フォルダを用意します

mkdir src
cd src
mkdir app

Angular2のチュートリアルに倣って、srcフォルダを追加し、さらにその下にappフォルダを追加します。

package.json の scripts を書き換えます

"scripts": {
  "tsc": "./node_modules/.bin/tsc -p . -w",
  "test": "live-server --open=src/index.test.html"
},

tsconfig.json を編集します

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES5",
    "noImplicitAny": false,
    "sourceMap": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules"
  ]
}

(root)/src/system.config.js を追加します

System.config({
  baseURL: '.',
  packages: {
    'app': { defaultExtension: 'js' }
  }
});

System.config()に関する細かい内容は以前書きましたので宜しければ御参照ください。
TypeScript + System.js の構成における System.config() の基本パターン。そしてモダンWeb開発の環境をマッハで作る。
設定が思った通りに反映されないときはデバッガーツールwindow.Systemオブジェクトを観察すると色々わかると思います。

(root)/src/index.test.html を追加します。ほぼ公式チュートリアルのパクり

<html>
  <head>
    <title>Angular 2 Test</title>
    <link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
    <script src="../node_modules/systemjs/dist/system.src.js"></script>             
    <script src="../node_modules/angular2/bundles/angular2.dev.js"></script>    
    <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
    <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
    <script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
    <script src="system.config.js"></script>      
  </head>
  <body>    
    <script>
      //System.import('app/app');
      System.import('app/app.spec').then(window.onload);
    </script>
    <my-app>loading...</my-app>
    <script src="../node_modules/jquery/dist/jquery.min.js"></script>
  </body>
</html>

(root)/src/app/app.ts を追加します。ほぼ公式チュートリアルのパクり

import {bootstrap, Component} from 'angular2/angular2';
import {HeroService} from './hero-service';

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <li *ng-for="#hero of heroes" id="hero{{hero.id}}">{{hero.name | uppercase}}</li>
    </ul>
    <div id="count">{{countStr + heroesCount | uppercase}}</div>
  `
})
export class AppComponent {
  heroes: Hero[];
  countStr = 'count:';
  constructor(heroService: HeroService){
    this.heroes = heroService.getHeroes();
  }
  get heroesCount(): number {
    return this.heroes.length;
  }
}
bootstrap(AppComponent, [HeroService]);

このconstructor()bootstrap()の書き方がDIの基本(らしい)
HeroServiceについては次の次に出てきます。

(root)/src/app/my.d.ts を追加して interface Hero と IHeroService を定義します

declare interface Hero {
  id: number,
  name: string
}

declare interface IHeroService {
  getHeroes: () => Hero[]
}

(root)/src/app/hero-service.ts を追加します。DIのために必要です。

export class HeroService implements IHeroService {
  heroes: Hero[];
  constructor() {
    this.heroes = HEROES;
  }
  getHeroes() {
    return this.heroes;
  }
}

var HEROES: Hero[] = [
  { "id": 11, "name": "Mr. Nice" },
  { "id": 12, "name": "Narco" },
  { "id": 13, "name": "Bombasto" },
  { "id": 14, "name": "Celeritas" },
  { "id": 15, "name": "Magneta" },
  { "id": 16, "name": "RubberMan" },
  { "id": 17, "name": "Dynama" },
  { "id": 18, "name": "Dr IQ" },
  { "id": 19, "name": "Magma" },
  { "id": 20, "name": "Tornado" }
];

getHeroes()が大事。app.tsconstructor()で使われています。

(root)/src/app/app.spec.ts を追加します。【今回のエントリーの要です】

import {bootstrap, Component, provide} from 'angular2/angular2';
import {HeroService} from './hero-service';
import {AppComponent} from './app';
//import 'jquery'; // 実行時エラーの原因になるため不採用
declare var $: JQueryStatic;

describe('Mock Test', () => {
  let ac: AppComponent;
  let mock: IHeroService = {
    getHeroes: () => [{ id: 1, name: 'mock1' }, { id: 2, name: 'mock2' }]
  }

  beforeEach(done => {
    bootstrap(AppComponent, [provide(HeroService, { useValue: mock })])
      .then(result => result.instance)
      .then(instance => {
        ac = instance;
        done();
      });
  });

  it("Test for Heroes' Name", () => {
    expect(ac.heroes[0].name).toEqual('mock1');
    //expect(document.querySelector('#hero' + ac.heroes[0].id).textContent).toEqual('MOCK1');
    expect($(`#hero${ac.heroes[0].id}`).text()).toEqual('MOCK1');
  });

  it("Test for Heroes' Count", () => {
    expect(ac.heroesCount).toEqual(2);
    //expect(document.querySelector('#count').textContent).toEqual('COUNT:2');
    expect($('#count').text()).toEqual('COUNT:2');
  });
});

app.tshero-service.tsを両方importして、bootstrap()のときに モックを流し込んでいます。
これにより本来app.tsで注入されるはずだった10人のヒーローは2人のモックヒーロー で上書きされます。(bootstrap()の第二引数がその処理です)
その後bootstrap()の戻り値からAppComponentインスタンスを取り出し、 テストにかけます。
このテスト結果は全て成功するので、MVVMでいうところのViewとViewModel は想定通りに動いたと考えられます。
また、TypeScriptで書いているので変数acはインテリセンスとコンパイラエラーチェックが働きます。これはとても大事なことです。
もう一度言います。特にメンテナンスする上でとても大事なことです。

タスクを走らせて確認します

npm run tsc
npm test

はい、
個人的にあまりテストを書いたことがないので、こんな感じでいいのかどうかも わからないのですが、備忘録をかねてまとめてみました。


以上です、ありがとうございました。

(2015/11/21 この記事の続きを書きました。→Angular2でDIしてテストを書いたけどhtmlファイルの重複をなんとかしたかった。)