とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

Angular アプリケーションのユニットテスト環境をゼロから作る

いつも ng new 一発でテスト環境まで準備済みだが、ふとユニットテスト環境を作るのにどういうパッケージとどういう設定が必要かきになったので試してみた記録

テスト環境がない状態で Angular アプリケーションを用意

  • ng new のオプションにめちゃくちゃ都合のいい minimal があったのでこれを使ってアプリケーションを用意
$ npx @angular/cli new ng-sample --minimal

? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
CREATE ng-sample/README.md (1038 bytes)
CREATE ng-sample/.gitignore (631 bytes)
CREATE ng-sample/angular.json (3215 bytes)
CREATE ng-sample/package.json (880 bytes)
CREATE ng-sample/tsconfig.json (489 bytes)
CREATE ng-sample/browserslist (429 bytes)
CREATE ng-sample/tsconfig.app.json (210 bytes)
CREATE ng-sample/src/favicon.ico (948 bytes)
CREATE ng-sample/src/index.html (307 bytes)
CREATE ng-sample/src/main.ts (372 bytes)
CREATE ng-sample/src/polyfills.ts (2835 bytes)
CREATE ng-sample/src/styles.scss (80 bytes)
CREATE ng-sample/src/assets/.gitkeep (0 bytes)
CREATE ng-sample/src/environments/environment.prod.ts (51 bytes)
CREATE ng-sample/src/environments/environment.ts (662 bytes)
CREATE ng-sample/src/app/app.module.ts (314 bytes)
CREATE ng-sample/src/app/app.component.ts (1470 bytes)

テストに必要なパッケージのインストール

  • 通常 Angular アプリケーションのユニットテストには Jasmine と Karma が使われているので同じ環境を揃えにいく
  • Karma の Installation を参考にしてみると最低限必要なパッケージがわかる
$ yarn add jasmine-core karma karma-jasmine karma-chrome-launcher -D
  • テストの実行すると起動することがわかる
$ yarn karma start

yarn run v1.21.1
$ /Users/path/to/ng-sample/node_modules/.bin/karma start
12 06 2020 09:34:34.912:WARN [karma]: No captured browser, open http://localhost:9876/
12 06 2020 09:34:34.978:INFO [karma-server]: Karma v5.1.0 server started at http://0.0.0.0:9876/

設定ファイルを書く

  • Karm の init コマンドで conf ファイルを作る(すべて default で作成)
yarn karma init

yarn run v1.21.1
$ /Users/path/to/ng-sample/node_modules/.bin/karma init

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
> 

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> 

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
> 

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes


Config file generated at "/Users/path/to/ng-sample/karma.conf.js".
✨  Done in 33.58s.
  • 作成された karma.conf.js は以下のようになっている
// Karma configuration
// Generated on Wed Jun 10 2020 22:37:18 GMT+0900 (GMT+09:00)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',


    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],


    // list of files / patterns to load in the browser
    files: [
    ],


    // list of files / patterns to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },


    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],


    // web server port
    port: 9876,


    // enable / disable colors in the output (reporters and logs)
    colors: true,


    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,


    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity
  })
}
  • ここでもう一度 start をすると Chrome が立ち上がりテストが実行される

Angular CLI 経由でテストを動かす

  • angular.json に test architect を追加する
    • builder には @angular-devkit/build-angular:karma を指定
    • options は angular_devkit/build_angular/src/karma/schema.json を見ながら省略できないパラメータを指定
      • 必須項目は angular_devkit/build_angular/src/karma/schema.json の required を見ると確認できる
      • 現時点では main, tsConfig, karmaConfig の 3 つ
diff --git a/angular.json b/angular.json
index 05240c6..d5132e9 100644
--- a/angular.json
+++ b/angular.json
@@ -93,6 +93,14 @@
             }
           }
         },
+        "test": {
+          "builder": "@angular-devkit/build-angular:karma",
+          "options": {
+            "main": "src/test.ts",
+            "tsConfig": "",
+            "karmaConfig": "karma.conf.js"
+          }
+        },
         "extract-i18n": {
           "builder": "@angular-devkit/build-angular:extract-i18n",
           "options": {
  • tsConfig は最低限 key だけあれば良い
  • karmaConfig にはすでに作成済みの karma.conf.js を指定
  • main(entry-point) として指定する src/test.ts を準備
    • 参考になる資料が見つからなかったので minimal でなければ生成されるファイルを用意
// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

declare const require: {
  context(path: string, deep?: boolean, filter?: RegExp): {
    keys(): string[];
    <T>(id: string): T;
  };
};

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
  • angular-devkit/build-angular を使用することを karma.conf.js に明記
diff --git a/karma.conf.js b/karma.conf.js
index 27a1565..238b42e 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -8,7 +8,12 @@ module.exports = function (config) {
 
     // frameworks to use
     // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
-    frameworks: ['jasmine'],
+    frameworks: ['jasmine', '@angular-devkit/build-angular'],
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-chrome-launcher'),
+      require('@angular-devkit/build-angular/plugins/karma'),
+    ],
 
     // list of files / patterns to load in the browser
     files: [],
  • Angular アプリケーションのテストの実行に Zone が必要なので polyfill を読み込む
diff --git a/angular.json b/angular.json
index 6f4687e..e39fec0 100644
--- a/angular.json
+++ b/angular.json
@@ -98,7 +98,8 @@
           "options": {
             "main": "src/test.ts",
             "tsConfig": "",
-            "karmaConfig": "karma.conf.js"
+            "karmaConfig": "karma.conf.js",
+            "polyfills": "src/polyfills.ts"
           }
         },
         "extract-i18n": {
  • Jasmine 用の型定義が必要なので @types/jasmine をインストール
$ yarn add @types/jasmine -D

サンプルテストコードをおいて実行してみる

  • app.component.spec.ts を用意
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });
});
  • テストの実行
$ yarn test --watch false

yarn run v1.21.1
$ ng test --watch false
10% building 1/1 modules 0 active12 06 2020 13:00:14.871:INFO [karma-server]: Karma v5.1.0 server started at http://0.0.0.0:9876/
12 06 2020 13:00:14.873:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
12 06 2020 13:00:14.878:INFO [launcher]: Starting browser Chrome
12 06 2020 13:00:27.166:INFO [Chrome 83.0.4103.97 (Mac OS 10.15.5)]: Connected on socket lsZPFDaFH_rTC8F2AAAA with id 33650755
Chrome 83.0.4103.97 (Mac OS 10.15.5): Executed 1 of 1 SUCCESS (0.248 secs / 0.171 secs)
TOTAL: 1 SUCCESS
✨  Done in 33.15s.

テストが動いた👏

テスト用のパッケージを確認

  • minimal 指定しない場合にインストールされるテスト用のパッケージを確認

上でインストール済みのパッケージ

jasmine-core

karma

karma-jasmine

karma-chrome-launcher

上でインストールしていないパッケージ

jasmine-spec-reporter

karma-jasmine-html-reporter

karma-coverage-istanbul-reporter

protractor

感想

test.ts の役割がモヤッとしている…が、基本的には Karma の設定をするだけで構築できることがわかった。インストールされているパッケージの役割もわかったのがよい。

Hello Deno

Deno 1.0.0 が出たのでお試し

deno.land

インストール

  • 日頃から asdf を使っているので plugin を追加してインストール
$ asdf plugin-add deno https://github.com/asdf-community/asdf-deno.git
$ asdf install deno 1.0.0
$ asdf global deno 1.0.0
$ deno --version 
deno 1.0.0
v8 8.4.300
typescript 3.9.2

Shell autocomplete

  • fish を使っているので以下のコマンドを実行
$ deno completions fish > /usr/local/etc/fish/deno.fish
$ source /usr/local/etc/fish/deno.fish

Hello World

$ deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕
  • もちろん *.js ファイルも実行できる
$ cat app.js 
console.log('Hello world');

$ deno run app.js
Hello world

その他

  • TypeScript がそのまま実行できるのがよい
  • 「Secure by default」と謳っているのでファイルを読み込んだり、HTTP リクエストをしたりするときは --allow-xxx のオプションが必要
  • Standard Library に example があるので色々試せそう
  • Code FormatterTest runner も built in されている
  • npm packages は使えない

参考

@ngrx/component を使って zone-less Angular を試す

@ngrx/component が試せるようになっていたので使ってみた

サンプルコード

github.com

@ngrx/component とは

  • ドキュメントを見ると zone less を実現するためのヘルパーで Angular アプリケーションをより Reactive に書けるようになると書いてある
  • 試せる APILet DirectivePush Pipe の 2 つ

事前準備

  • 以下のようなサンプルアプリケーションを用意した(スタイル省略)
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <main>
      <h2>click event</h2>
      <section>
        <div>Message: {{ message$ | async }}</div>
        <button (click)="showMessage()">Click</button>
      </section>

      <h2>Http request</h2>
      <section>
        <ng-container *ngIf="user$ | async as user">
          <div>{{ user.name }}</div>
        </ng-container>
      </section>
    </main>
  `,
})
export class AppComponent implements OnInit {
  constructor(private http: HttpClient) {}

  messageSubject = new BehaviorSubject<string>('');
  user$: Observable<any | null> = of(null);

  get message$() {
    return this.messageSubject.asObservable();
  }

  ngOnInit() {
    this.fetchUser();
  }

  // NOTE: click event
  showMessage() {
    this.messageSubject.next('Hello world');
  }

  // NOTE: Http request
  fetchUser() {
    this.user$ = this.http.get('https://jsonplaceholder.typicode.com/users/1');
  }
}
  • サンプルアプリケーションには 2 つのシナリオを用意している
    1. ボタンクリックのイベントリスナー
    2. HTTP リクエストを介したデータの取得
  • Angular は通常 NgZone によってこれらを監視し、変更検知をトリガーしている
    • NgZone が変更検知を行っているわけではない
    • NgZone については公式ドキュメント を見るのがよい

zone-less

  • パフォーマンス向上のために NgZone を無効にすることもできる
  • しかし、NoopZone にすると変更検知のトリガーがされなくなり view の更新が行われなくなる
  • いよいよ @ngrx/component を使っていく

Let Directive

  • zone-full と zone-less のどちらのモードでも同じように動作する
  • *ngIf が falsy のときに描画されない問題も解消できる
    • observable な値を view にバインドするのに *ngIf が必要だったがそこの置き換えができる
  <section>
-   <ng-container *ngIf="user$ | async as user">
+   <ng-container *ngrxLet="user$ as user">
      <div>{{ user.name }}</div>
    </ng-container>
  </section>

Push Pipe

  • ngrxPush は AsyncPipe の代替であり zone-full と zone-less のどちらのモードでも同じように動作する
  • 以下のように置き換えると zone-less でも動作する
   <h2>click event</h2>
   <section>
-    <div>Message: {{ message$ | async }}</div>
+    <div>Message: {{ message$ | ngrxPush }}</div>
     <button (click)="showMessage()">Click</button>
   </section>

※ NOTE

  • 今回のサンプルコードだと async を ngrxPush に置き換えただけだと動作せず、下記のエラーが発生した
ERROR RangeError: Maximum call stack size exceeded
    at TapSubscriber._next (tap.js:43)
    at TapSubscriber.next (Subscriber.js:49)
    at MapSubscriber._next (map.js:35)
    at MapSubscriber.next (Subscriber.js:49)
    at DistinctUntilChangedSubscriber._next (distinctUntilChanged.js:50)
    at DistinctUntilChangedSubscriber.next (Subscriber.js:49)
    at Subject.next (Subject.js:39)
    at Object.next (component.js:263)
    at PushPipe.transform (component.js:389)
    at Module.ɵɵpipeBind1 (core.js:36650)
  • issue/2488 にもあったが asObservable() を使っているとテンプレートチェックのたびに新しい参照を返すために発生するとのこと
  • 今回は下記の変更も加えて回避した
   messageSubject = new BehaviorSubject<string>('');
+  _message$ = this.messageSubject.asObservable();
   user$: Observable<any | null> = of(null);
 
   get message$() {
-    return this.messageSubject.asObservable();
+    return this._message$;
   }
 
   ngOnInit() {

感想

ドキュメントにもまだ「実験的」な機能である旨の一文が書いてあるが、zone-less Angular を体験してみるにはとてもよかった。

実際のアプリケーションで zone を外すことを考えたことはまだないが、パフォーマンス懸念やデバッグの難しさから zone を外すことを検討しないといけなくなった場合に代替案があるのはいいと感じた。

参考

Angular Library を Secondary Entry Points 構成にする

Angular Library を Secondary Entry Points 構成にするための手順をメモ

試したリポジトリ GitHub - kasaharu/try-angular-lib-secondary-entrypoints

新規 application を作成

  • Angular CLI を使ってアプリケーションを作成
    • Angular CLI : v9.1.0
$ npx @angular/cli new try-angular-lib-secondary-entrypoints

Library project を作成

$ yarn ng generate library my-lib
  • projects/my-lib/ が作成される

my-lib の service を app.component で使う

Library side

  • my-lib に作られた my-lib.service.ts に debug 用のメソッド実装
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MyLibService {
  constructor() {}

  debug() {
    console.log('MyLibService - debug');
  }
}
  • my-lib Library を build
$ yarn build my-lib

Application side

  • app.module.ts で MyLibModule を import
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { MyLibModule } from 'my-lib';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, MyLibModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
  • app.component.ts で MyLibService の debug() を実行
import { Component, OnInit } from '@angular/core';
import { MyLibService } from 'my-lib';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(private myLibService: MyLibService) {}
  title = 'try-angular-lib-secondary-entrypoints';

  ngOnInit() {
    this.myLibService.debug();
  }
}

f:id:kasaharu:20200327234015p:plain

  • ログが出ることを確認

Secondary Entry Points の作成

作成するもの

  • projects/my-lib/ 配下に以下のファイルを用意する
projects/my-lib/testing/
├── package.json
└── src
    ├── lib
    │   ├── my-lib-testing.module.ts
    │   └── my-lib-testing.service.ts
    └── public_api.ts

package.json

{
  "ngPackage": {}
}

public_api.ts

  • [※ 注意] ng generate で作られる projects/my-lib/src/ 配下にあるのは public-api.ts だが、ここで作る必要があるファイル名は public_api.ts にしなければならないので注意
export * from './lib/my-lib-testing.service';
export * from './lib/my-lib-testing.module';

my-lib-testing.module.ts

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

@NgModule({
  declarations: [],
  imports: [],
  exports: []
})
export class MyLibTestingModule {}

my-lib-testing.service.ts

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

@Injectable({
  providedIn: 'root'
})
export class MyLibTestingService {
  constructor() {}

  debug() {
    console.log('MyLibTestingService - debug');
  }
}
  • 作成する必要があるファイルはこれで全てなので、再度 Library を build する

application で使うために path を登録

  • application で使うためには import path を tsconfig.json に登録する必要があるので、以下の diff の用に変更する
  • ※ 注意 my-lib の登録は Angular CLI が自動的にセットしているが、手動で用意した Secondary Entry Points は path の設定も手動でする必要があるので注意
       "my-lib": [
         "dist/my-lib/my-lib",
         "dist/my-lib"
+      ],
+      "my-lib/testing": [
+        "dist/my-lib/testing"
       ]
     }
   },

my-lib/testing の service を app.component で使う

  • my-lib の時と同じように app.module.ts と app.component.ts に変更を加える

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { MyLibModule } from 'my-lib';
import { MyLibTestingModule } from 'my-lib/testing';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, MyLibModule, MyLibTestingModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

app.component.ts

import { Component, OnInit } from '@angular/core';
import { MyLibService } from 'my-lib';
import { MyLibTestingService } from 'my-lib/testing';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(
    private myLibService: MyLibService,
    private myLibTestingService: MyLibTestingService
  ) {}
  title = 'try-angular-lib-secondary-entrypoints';

  ngOnInit() {
    this.myLibService.debug();
    this.myLibTestingService.debug();
  }
}

f:id:kasaharu:20200327234050p:plain

  • my-lib/testing の方のログも期待通り出力されることを確認

参考

ComponentHarness を使って Angular アプリケーションの単体テストを書く

ComponentHarness を使って Angular アプリケーションの単体テストを書いてみた

試したコード

github.com

ComponentHarness とは

  • angular/components v9.0.0 に追加された testing tool
  • テストコードを書く時の役割が ComponentHarness を作る人と ComponentHarness を使ってテストコードを書く人に分けることが可能
    • 人を分けるわけではなく考えるレイヤーを分ける
  • ComponentHarness を使うと ComponentHarness の層で DOM 構造などを隠蔽できるようになるため実際のテストコードでは DOM の変更に強くなるなどの利点がある

パッケージインストール

  • angular/cdk/testing が提供しているので cdk の v9 以上が必要
$ yarn ng add @angular/cdk

テスト対象のアプリケーション

  • HeaderComponent を用意して AppComponent に配置しただけのアプリケーション

app.component.ts

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

@Component({
  selector: 'app-root',
  template: `
    <app-header></app-header>
  `,
})
export class AppComponent {}

header.component.ts

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

@Component({
  selector: 'app-header',
  template: `
    <header class="header">
      <h1 class="title">Header Title</h1>
    </header>
  `,
})
export class HeaderComponent {}

Custom Harness を作る

  • angular/components が提供する component は ComponentHarness も提供されているが自作の component は Custom Harness を作る必要がある

header-harness.ts

import { ComponentHarness } from '@angular/cdk/testing';

export class HeaderHarness extends ComponentHarness {
  static hostSelector = 'app-header';

  protected _getTitleElement = this.locatorFor('h1');

  async getTitleText() {
    const titleElement = await this._getTitleElement();
    return titleElement.text();
  }
}
  • Harness は angular/cdk/testing が提供する ComponentHarness を継承して class を作る
  • Custom Harness は static な hostSelector を持つ必要がある
    • 基本的にこの hostSelector は対象 component の selector と同じにしておけば問題ない
  • ComponentHarness は DOM 要素を見つけるための API を持っていて、今回は HeaderComponent の h1 要素を見つけるために locatorFor() を使って _getTitleElement を定義している
    • 古い要素のキャッシュを防ぐために locatorFor() は関数を返す仕組みになっている。そのため _getTitleElement() を呼び出すための wrapper method を実装する必要がある
    • ComponentHarness が提供する locator method は locatorFor() の他にも locatorForOptional()locatorForAll() がある

Harness を使った単体テストを書く

app.component.spec.ts

  • HeaderComponent は AppComponent に配置されているので app.component.spec.ts で HeaderHarness を使いテストする
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { HeaderHarness } from './header/testing/header-harness';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let loader: HarnessLoader;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent, HeaderComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
  }));

  it(`should have the HeaderComponent`, async () => {
    const headerHarness = await loader.getHarness(HeaderHarness);

    expect(headerHarness).toBeTruthy();
  });
});
  • HarnessLoader インスタンスを取得するために TestbedHarnessEnvironment.loader() を使用する
    • fixture 内にある Harness を取得するために必要なので今回は HeaderComponent を持つ AppComponent を fixture として指定
  • HarnessLoader インスタンスgetHarness() で対象の Harness(今回は HeaderHarness) を指定する
  • fixture で指定した AppComponent 内に HeaderComponent を表示していれば getHarness(HeaderHarness) で値を取得できるので、それが truthy であれば HeaderComponent が表示できていることになる

header.component.spec.ts

  • HeaderComponent 自身で HeaderHarness を使いテストする
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { HeaderHarness } from './testing/header-harness';

describe('HeaderComponent', () => {
  let fixture: ComponentFixture<HeaderComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [HeaderComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(HeaderComponent);
  }));

  it('Heading text が Header Title であること', async () => {
    const headerHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, HeaderHarness);
    const titleText = await headerHarness.getTitleText();

    expect(titleText).toBe('Header Title');
  });
});
  • 対象の fixture 自身の Harness を用意するときは TestbedHarnessEnvironment.harnessForFixture() を使用する
  • HeaderHarness のインスタンスで用意したメソッド getTitleText() を呼び出すと titleText を取り出せ、そこの文字列の一致をテストすることができる

感想

最低限動かすだけでもそれなりに覚えることはある印象。。

が、ComponentHarness を作るレイヤーと ComponentHarness を使ってテストコードを書くレイヤーを分けられるのは確かにメリットだと思う。

今回の例でも header のタイトルを表示している要素を h1 から h2 に変えても Harness の変更は必要だが、それ使うテストコードの変更は不要だとわかる。

拾いきれなかった API もいっぱいあるからもう少し素振りは必要そうだなぁ

参考

Bonfire Frontend #5 に行ってきた

yj-meetup.connpass.com

今日のテーマは「テストと自動化」

f:id:kasaharu:20200205234243j:plain

タイムテーブルはこんな感じだった

  • 中期プロジェクトでe2eテストを導入してみて感じたこと
  • Vueのテスト手法とVRTのすすめ
  • Yahoo! JAPAN トップページ リニューアルとテストについて

中期プロジェクトでe2eテストを導入してみて感じたこと

メモ

  • E2E を始めるのは開発が落ち着いてから
  • 狙い
    • 工数削減 → クリティカルで人力では大変なケースを自動化したい
    • 品質向上 → 本当に人力が必要な探索的テストなどをするために単純な作業は自動化しておく
  • 導入までは簡単 → 何年くらいメンテしているんだろう?
  • E2E テストのためのセレクタの選定が難しそう → data 属性とかで工夫する
  • Visual Regression Test も導入したい
  • Google Testing Blog というものがあるらしい

Vueのテスト手法とVRTのすすめ

メモ

  • 軽めの案件にはテストはいらない → LP 作成に E2E テストいらないのは、そう
  • どういうテストが何に適しているか
  • Vue ではブラックボックステストが推奨されている
    • Component が一つの責務で作るのが推奨されているから
  • E2E テスト → 開発終盤に QA が人力でやったりする
  • Visual Regression Test → 画像の比較、そこそこ重い
  • reg-suit

Yahoo! JAPAN トップページ リニューアルとテストについて

メモ

  • テストピラミッドは守る → 最下層のユニットテストを手厚くする
  • reg-suit 人気
  • deploy 時に E2E テストが実行される
  • テストも自動化も継続が大事

懇親会

f:id:kasaharu:20200205234634j:plain いつものありました

感想

ユニットテスト手厚くは個人的に結構意識できていると思うからやっぱ E2E の導入フェーズだなーと改めて思えてよかった。

ただ、E2E テストの環境もない状態からいきなりクロスブラウザのことまで考えると失敗しそうだからまず Chrome に絞って導入してみるのがよさそう。 「とりあえず Chrome でやってみる」はそんなに時間かからない印象だから必要なのは始める気持ちな気がする。ちゃんと素振りしよう。

フォーム周りの人力でやっても結構めんどくさそうな操作から自動化がするのがコスパ良さそう。 Visual Regression Test も気にはなるが、今は QA 観点で効率化したいと思ってるから E2E テストからかなーと思えた。

本編と関係ないけど勉強会ちゃんと机あるのいいなー。

属性バインディングとプロパティバインディングの違い

Angular のデータバインディングの種類に「属性バインディング」と「プロパティバインディング」があるが明確に違いがわかっていなかったのでメモ

属性バインディングとプロパティバインディングの違い

  • そもそも HTML 属性か DOM プロパティかの違いがある

  • HTML 属性の場合 button に disabled があるだけでその button は無効になる

<button disabled>Button</button>
  • disabled 属性をロジックによって着脱したい場合は以下のように書く
<button [attr.disabled]="condition ? 'disabled' : null">Button</button>
  • disabled プロパティを使う場合は以下のように書く
<button [disabled]="condition ? true : false">Button</button>

違い

  • attr. を付けると属性バインディングになる
  • プロパティバインディングは値に boolean を渡せばよいが、属性バインディングには null か no-null が必要
  • 対象の HTML 要素に同じ名前のプロパティと属性がある場合は「プロパティバインディング」を使ったほうが、条件式は簡潔になる(boolean を返す条件式にすればよいので)
  • しかし、場合によっては HTML 要素の属性にしかないものもあるのでその場合は「属性バインディング」を使うことになりそう

参考