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

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

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 要素の属性にしかないものもあるのでその場合は「属性バインディング」を使うことになりそう

参考

Angular の「構造ディレクティブを書く」を読む

構造ディレクティブとは

angular.jp

ドキュメントを見ると以下のように定義されている。

構造ディレクティブは HTML のレイアウトを担当します。 それらは、通常は要素を追加、削除、または操作することによって、 DOM の 構造 を構築または再構成します。

「構造ディレクティブを書く」を読みながら理解する

構造ディレクティブを書く では NgIf の逆をおこなう UnlessDirective の作成をする。

<p *appUnless="condition">Show this sentence unless the condition is true.</p>

上記では conditionfalse のときに p 要素の中のメッセージが表示される。

アスタリスク接頭辞 を読むと *ng-template を使用した記述のシンタックスシュガーであることがわかる。 つまり上記のコードは以下と同等になる。

<ng-template [appUnless]="condition">
  <p>Show this sentence unless the condition is true.</p>
</ng-template>

<ng-template> にもある通り <ng-template> は HTML をレンダリングするための要素であるが構造ディレクティブなしでは実施に表示はされない。

そのため構造ディレクティブがどのように <ng-template> を表示しているか見ていく。

TemplateRef と ViewContainerRef

構造ディレクティブを自作するためには TempalteRef と ViewContainerRef が必須になる。

TemplateRef は <ng-template> の内容を取得することができる。

取得した内容は ViewContainerRef を通して埋め込むことができる。(埋め込んだ view を clear することも可能)

読了 : エラスティックリーダーシップ

読んだ本

https://www.amazon.co.jp/dp/4873118026

感想

チームには 3 つのフェーズがあると書いてありそれぞれ「サバイバルフェーズ」「学習フェーズ」「自己組織化フェーズ」と呼んでいた。 そもそもチームのフェーズについて考えたことがなかったので目からウロコであった。

フェーズによってチームの状況が異なるので、当然リーダーに求められる役割も変わる。つまりリーダに求めること、リーダーが求められることを考える前にチームのフェーズを正しく認識するべき、と改めて思った。

メモ

各フェーズに求められるリーダーのタイプ

  • サバイバルフェーズ / 指揮統制型
  • 学習フェーズ / コーチ型
  • 自己組織化フェーズ / ファシリテーター

既存の Angular アプリに Scully を入れてみた

既存の Angular アプリに Scully を入れてみたのでそのメモ

github.com

Scully とは

  • Angular 用の静的サイトジェネレーター(SSG)
  • 各ページを pre rendering する
  • Node.js v10.x 以上、Angular v9.x 以上が必要(今だと v9 RC を使う必要がある)

導入

インストール

$ yarn ng add @scullyio/init
yarn run v1.21.1
$ ng add @scullyio/init
Installing packages for tooling via yarn.
Installed packages for tooling via yarn.
    ✅️ Added dependency
    ✅️ Import HttpClientModule into root module
UPDATE package.json (1992 bytes)
UPDATE src/app/app.module.ts (827 bytes)
UPDATE src/polyfills.ts (3039 bytes)
UPDATE src/app/app.component.ts (301 bytes)
✔ Packages installed successfully.
    ✅️ Update package.json
CREATE scully.config.js (65 bytes)
UPDATE package.json (2052 bytes)
✨  Done in 174.11s.
  • インストールすると ↑ こんなログが出る

ビルド

  • Scully のビルドには Angular アプリがビルドされている必要がある
$ yarn ng build
$ yarn scully
  • ビルドすると dist/static/ 配下に成果物ができる
    • Scully 用の routes の scully-routes.json も一緒に生成される
  • production 用にする場合は $ yarn ng build --prod でビルドする
  • ローカルで動かすときは $ yarn scully:serve を使うと 1864 ポートで起動する

デプロイ

  • もともと Angularfire で Firebase hosting にデプロイしていたので firebase.json の public を dist/static に変更

結果

  • HTML がレンダリングされた状態でレスポンスが返ってくるようになった
Scully 導入前 Scully 導入後
Scully 導入前
Scully 導入後
  • ほとんどロジックがないアプリケーションだったこともあると思うが、想定していたよりも resources のサイズや load time の変化はなさそう
  • SSG 化するまでのステップは簡単だった