Angular/KarmaでDirectiveのユニットテストをおこなう


2019/07/09


今回のプロジェクト構造

以下、ファイル構造を示します。(※ node_modules フォルダ以下の深い闇を避けるためにtreeでは除外します)

            
            $ tree -I node_modules -L 5
.
├── README.md
├── angular.json
├── browserslist
├── e2e
│   ├── protractor.conf.js
│   ├── src
│   │   ├── app.e2e-spec.ts
│   │   └── app.po.ts
│   └── tsconfig.json
├── karma.conf.js
├── package.json
├── src
│   ├── app
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── components
│   │   │   └── my-str-tmpl1
│   │   │       ├── my-str-tmpl1.component.scss
│   │   │       ├── my-str-tmpl1.component.spec.ts
│   │   │       └── my-str-tmpl1.component.ts
│   │   └── directives
│   │       └── my-tmpl-def1
│   │           ├── my-tmpl-def1.directive.spec.ts
│   │           └── my-tmpl-def1.directive.ts
│   ├── assets
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   └── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
├── tslint.json
└── yarn.lock
        
今回は、src/app/directives/my-tmpl-def1/のDirectiveにユニットテストを加えます。

作成するDirective

今回はHTMLのp要素の文字を赤くするだけのDirectiveを作成します。

完成品のビューは以下のようになります。

合同会社タコスキングダム|蛸壺の技術ブログ

Directiveの使い方は割愛しますが、
my-str-tmpl1 Component上では以下のようになっております。

            
            import {
    Component
} from '@angular/core';

@Component({
    selector: 'app-my-str-tmpl1',
    template: `
    <div appMyTmplDef1>Hello {{name}}</div>
    `,
    styleUrls: ['./my-str-tmpl1.component.scss']
})
export class MyStrTmpl1Component {
    private name: string;
    constructor() {
        this.name = 'ANGULAR 2+';
    }
}
        
### my-tmpl-def1.directive.ts

以下が今回のDirectiveの実装です。

...要素の文字を赤くしているだけです。

            
            import {
    Directive,
    ElementRef,
    Renderer2,
    OnInit
} from '@angular/core';

@Directive({
    selector: '[appMyTmplDef1]'
})
export class MyTmplDef1Directive implements OnInit {

    constructor(
        private el: ElementRef,
        private renderer: Renderer2
    ) {}

    ngOnInit() {
        this.renderer.setStyle(this.el.nativeElement, 'color', 'red');
    }

}
        
ちなみに、angular cli で Direvtiveを新規作成する場合のショートハンドコマンドには ng g d [Direvtive名]を使うと便利です。

my-tmpl-def1.directive.spec.ts

ではテストコードを作成しましょう。

まずは完成形を以下に示します。

            
            import { Component } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { MyTmplDef1Directive } from './my-tmpl-def1.directive';

// Simple mocking component
@Component({
    template: '<p appMyTmplDef1>Cool Unit Test for Directives</p>'
})
class MockingComponent {
    constructor() { }
}

describe('MyTmplDef1Directive', () => {
    let component: MockingComponent;
    let fixture: ComponentFixture<MockingComponent>;

    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [
                MockingComponent,
                MyTmplDef1Directive
            ]
        });
        fixture = TestBed.createComponent(MockingComponent);
        component = fixture.componentInstance;
    });

    it('should create a component', () => {
        expect(component).toBeDefined();
    });

    it('should capitalize style against p element', () => {
        const elem: HTMLElement = fixture.debugElement.nativeElement;
        const p: HTMLElement = elem.querySelector('p');

        // Apply style at the directive to the testing component
        fixture.detectChanges(); // !important

        // 'color' property should be set red
        expect(p.style.color).toBe('red');
    });
});
        
それでは、このコードについて以下に解説をいれましょう。

Mocking Component

通常、Directiveは単体では機能せずComponentに付随して働きます。

単体テストではDirectiveの実行できるような模擬のコンポーネントを準備する必要があります。

上記のコード内では、

            
            ...
// Simple mocking component
@Component({
    template: '<p appMyTmplDef1>Cool Unit Test for Directives</p>'
})
class MockingComponent {
    constructor() { }
}
...
        
としている箇所に該当いています。

ComponentFixture

Directiveの単体テストに欠かせないのが、ビルドインのユーティリティであるComponentFixtureです。

名前の通り、Componentに装着させて補助使うようです。

以下、テスト内(describeメソッドの中身)だけ見て行きますと、

            
            let component: MockingComponent;
    let fixture: ComponentFixture<MockingComponent>;

    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [
                MockingComponent,
                MyTmplDef1Directive
            ]
        });
        fixture = TestBed.createComponent(MockingComponent);
        component = fixture.componentInstance;
    });
    ...
        
の部分で、テストに入る前の下ごしらえをbeforeEach内で行なっています。

順序として、まずはComponentFixtureを定義し、そこから被験コンポーネントの実体を変数
componentに着ける手順のようです。

で、肝心のDirectiveが正常に機能しているか(今回は文字を赤くしているか)のテストが、以下の部分です。

            
            ...
    const elem: HTMLElement = fixture.debugElement.nativeElement;
    const p: HTMLElement = elem.querySelector('p');

    // Apply style of the directive to test component
    fixture.detectChanges(); // !important

    // 'color' should be set red
    expect(p.style.color).toBe('red');
...
        
ComponentFixureのインスタンスから、まずdebugElement.nativeElementで該当HTMLテンプレートから要素を引き出すことが出来るようです。

そのテンプレートからp要素を
querySelectorメソッドで引き出します。

このp要素を引き出した時点では、
appMyTmplDef1 Directiveは実行されていません。

よってDirectiveをローカルで手動実行する為に、今回もっとも重要な手続きは、

            
            fixture.detectChanges();
        
を適用させなければならないということです。

これはDirectiveにイベントリスナーを組み込んでテストする際には、イベント関数を発火させる度にその都度
detectChangesを呼出すことにも留意しなければいけません。

以上、最近のAngularに置いて Directive を使う機会は少ないかもしれませんが、ぜひDirectiveの単体テストをお手元でお試しください。


参考

Testing Angular Directives

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。