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.ts
のconstructor()
で使われています。
(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.ts
とhero-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ファイルの重複をなんとかしたかった。)