【Angular】これからはじめるE2Eテスト（2019）

この記事は [Angular #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/angular-2) 10日目の記事です

Angular 9なかなかこないですね！みなさまのAngularのアップデート状況はいかがなものでしょうか。

いざアップデートしたはいいものの、ブラウザ上で正常に動作するか手作業で確認するのはなかなかツライものがあります。
また、E2Eで自動化してみたいけど何から手をつけたらいいか分からなかったり、昔からの製品なのでIEでも動作するか確認したい・・・
など色々困っていることはありませんでしょうか？

この記事では、マルチブラウザで動作し <font color="Blue">（Chrome、Firefox、Edge、IE11など）</font>
Angularで困ったときのナレッジが割と見つかりやすく
Angular CLIでプロジェクトを作成した際に標準でインストールされている [Protractor](https://www.protractortest.org/#/) を使って
はじめてE2Eを書くための方法を紹介していきたいと思います。:grinning:


　　　　　　![images.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54858/4f5a4574-b1a1-6512-cf43-f927314fbb81.png)


## 対象読者
* 初めてプロジェクトにE2Eテストを取り入れようと考えている方
* マルチブラウザのE2E対応が必要な方
* ProtractorでE2Eの書き方を知りたい方


## 実行環境
・OS: Windows10 Pro 1903
・Angular: 8.2.14
・AngularCLI: 8.3.20
・Node.JS: v12.13.1
・protractor: 5.4.2
・webdriver-manager: 12.1.7

## ログイン画面のE2Eのテストを書く
今回は下のようなログイン画面元に、
値を入力してホーム画面に移動する簡単なE2Eテストを書いて行きたいと思います。

![スクリーンショット 2019-12-09 6.40.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54858/39f31d97-3b1e-eeb0-f82a-ad46300d9db6.png)

また、サンプルコードはこちらにご用意してますので合わせてご確認ください。
https://github.com/hiroyuki-nishi/angular-e2e


### 1. 選択する要素に識別子を振る
まずは、選択する要素を識別するために [data属性](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/data-*)を使って識別子を振っていきます。
今回は、メールアドレスとパスワードに値を入力した後に、ボタンをクリックしてログインしたいので
3つの要素に識別子を振っていきます。

```.html:login.component.html
<h1>login</h1>
<form>
  <div>
    <mat-form-field>
      <input matInput placeholder="email" [attr.data-test]="'email'"> // ←　要素①： data-*属性で一意となる識別子を振る
    </mat-form-field>
  </div>

  <div>
    <mat-form-field>
      <input type="password" matInput placeholder="passoword" [attr.data-test]="'password'"> // ← 要素②
    </mat-form-field>
  </div>

</form>
<div class="p-button-wrapper">
  <button mat-raised-button class="p-button" color="accent" (click)="login()" [attr.data-test]="'login-button'">Login</button>　 // ← 要素③
</div>
```

E2Eテストで要素を識別するために他に使用される属性として、id属性・class属性などがありますが
これらの属性はデザインの変更などで変更される恐れがあるため
その影響によりテストが動かなくなってしまう可能性があります。

そのため、別のE2EフレームワークであるCypressのBest Practicesに記載されているように
data-*属性のようなセレクターを利用したり、デザインにあまり影響を受けないならid属性を利用するなど
プロジェクトにあった適切な方法で識別子を振っていきましょう。

**Cypress Best Practices：Selecting-Elements**
https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements
**Protractor style guide：Locator strategies**
https://www.protractortest.org/#/style-guide


### 2. テストを書く
次にE2Eのテストを書いていきます。
ここでは主に、`＊.e2e-spec.ts`と`*.po.ts`の2つのファイルが登場します。

#### Page Object Patternで設計する
Angular CLIでプロジェクトを作成するとe2e/srcディレクトリ直下に、
`＊.e2e-spec.ts`と`*.po.ts`の2つのファイルが作成されていると思います。

これは、アプリケーションの画面単位で1オブジェクトを定義する[Page Objects](https://github.com/SeleniumHQ/selenium/wiki/PageObjects
) としてE2Eテストが作成されているためです。

<font color="Green">PageObjects </font>とは、アプリケーションの画面を1つのオブジェクトとしてとらえるデザインパターンです。

E2Eテストはデザインの変更を受けやすいという特徴があります。
もし、テストケースに直接セレクタを記述する場合、デザインの変更があった場合、セレクタを記述している全てのテストケースを修正しなければなりません。

そのため、Page Objectsとして、`*.po.ts`に各画面毎の要素選択などの処理を定義することで
`*.e2e-spec.ts`は実際のテストケースを手続き的に書いていけるので、コードの見通しがよくなり
また、1度実装したページは使い回しが効くなどのメリットがあります。
より詳細な説明については、最後にまとめている資料リンク先などを確認していただけたらと思います。

---

#### はじめてのテスト

上記のような設計思想を頭に入れつつ、まずは`login.e2e-spec.ts`にテストケースを書いていきます。
今回用意した[サンプルコード](https://github.com/hiroyuki-nishi/angular-e2e
)は、ログインが成功するとHome画面に遷移されるので、ログイン後URLが変更されているか確認してみます。

#### テストケースを書く

```.ts:login.e2e-spec.ts
import { LoginPage } from './login.po';
import { browser } from 'protractor';


describe('ログインできること', () => {
  const page: LoginPage = new LoginPage();

  it('login', async () => {
    page.login('example@com', 'xxxxx');
    browser.getCurrentUrl().then(url => expect(url).toEqual('http://localhost:4200/#/home'));
  });
});
```

LoginPageオブジェクトを作成し、ログイン操作を行ったあと
Protractor の<font color="Blue">browser.getCurrentUrl()</font>でURLを取得してJasmine のmatcherである<font color="Blue">toEqual() </font>でURLを比較しています。


他にどのようなAPIがあるかは下記ドキュメントに記載されているので色々試してみてください。:thumbsup:
[Protractor APIドキュメント](https://www.protractortest.org/#/api)
[Jasmine matcherのドキュメント](https://jasmine.github.io/api/3.5/matchers.html)


#### Page Objectsの実装を書く
`login.po.ts`では、ページで行える操作をメソッドに定義していきます。
メールの入力→パスワードの入力→ログインボタンのクリックを行いたいので `login()`として定義しています。

```.ts:login.po.ts
import { browser, by, element } from 'protractor';


export class LoginPage {
  private TEST_IDS = {
    EMAIL: 'email',
    PASSWORD: 'password',
    BUTTON: 'login-button',
  };

  private xpathLocator(id: string) {
    return `//*[contains(@data-test, "${id}")]`;
  }

  login(mail: string, password: string): LoginPage {
    browser.get('http://localhost:4200/#/login');
    element(by.xpath(this.xpathLocator(this.TEST_IDS.EMAIL))).sendKeys(mail);
    element(by.xpath(this.xpathLocator(this.TEST_IDS.PASSWORD))).sendKeys(password);
    element(by.xpath(this.xpathLocator(this.TEST_IDS.BUTTON))).click();
    return this;
  }
}
```

Page Objectsをつかってテストケースからセレクタを追い出していく感じです。


#### テストを実行する
最後にテストを実行してみます。

```sh
// Angularを起動しておく
$ ng serve

// protractorを起動しテストを実行する。 別ターミナルなどで起動してください。
$ npx protractor ./e2e/protractor.conf.js --specs=./e2e/src/**/*

// 特定のファイルのテストを実行する場合
$ npx protractor ./e2e/protractor.conf.js --specs=./e2e/src/login.e2e-spec.ts
```

無事テストが成功すると下のような結果が出力されていると思います！

```sh
[09:09:50] I/launcher - Running 1 instances of WebDriver
[09:09:50] I/hosted - Using the selenium server at http://localhost:4444/wd/hub
Jasmine started

  ログインできること
    ✓ login

Executed 1 of 1 spec SUCCESS in 1 sec.
[09:09:53] I/launcher - 0 instance(s) of WebDriver still running
[09:09:53] I/launcher - chrome #01 passed
```

ChromeなどでProtractorを使ってE2Eテストを記述する場合は
ここまでの内容でE2Eテストをガリガリ書いていけると思います！



##マルチブラウザで同時にテストする

ここからは、Protractorを使ってIEなどでテストを実行するための方法について紹介していきます。

![https___qiita-image-store.s3.amazonaws.com_0_3732_85c6a9c5-7810-97f3-0010-b521939faf70.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54858/238c36c3-c05f-5995-3634-ff0a001bfd8f.jpeg)

[Protractorのサイト](https://www.protractortest.org/#/infrastructure)によると、Protractorは、WebDriverJSというSelenium WebDriver APIのラッパーであり
このWebDriverJS を使って Selenium Server に命令を送り、 Selenium Serverはテストからのコマンドを解釈した後
Browser Driver がブラウザを操作するという仕組みで作られています。

<font color="Green">Chrome </font>や <font color="Green">Firefox </font>などのブラウザの場合
`protractor.conf.js`の [directConnect](https://github.com/angular/protractor/blob/master/lib/config.ts) をtrueにすることで、直接Browser Driverを操作することができますが
IEなどのブラウザは、Selenium Server経由でWebDriverを介してブラウザを操作する必要があります。

そのための設定を紹介していきます。


### IE11で起動するための準備：
#### ブラウザオプションの変更
IE11でProtractorを起動するために下記サイトを参考にIE11のオプションを変更していきます。
[Run protractor test in IE [Internet Explorer]](https://qavalidation.com/2017/11/run-protractor-test-ie-internet-explorer.html/)

変更するのは下記の２点です。

* IE11 のセキュリティレベルのすべてのゾーンの保護モードを有効にします
* IE11の拡大率を100%にします

#### Angular側の変更
`browserlist`でIE11での起動が除外されているため、下記をコメントアウトします。

```.sh:browserlist
# not IE 9-11 # For IE 9-11 support, remove 'not'.
```


また、ng serve上でIE11のE2Eテストを実行したい場合
`tsconfig.json`のtargetをes5変更するか別途es５で起動するtsconfig.jsonファイルを作成する必要があります。
[ng serve/test/e2e does not work with Internet Explorer 11 #14455](https://github.com/angular/angular-cli/issues/14455)

```.json:tsconfig.json
{
    "target": "es5",
    ...
}
```

#### Protractor側の変更
Selenium Server経由でWebDriverを介してブラウザを操作させるために 
`protractor.conf.js`のdirectConnectを無効化し、マルチブラウザで同時に起動するように
`capabilities`の代わりに`multiCapabilities `を設定します。

```.js:protracotor.conf.js
  multiCapabilities: [
    { 'browserName': 'chrome'},
    { 'browserName': 'firefox' }, // Firefoxを追加
    { 'browserName': 'internet explorer'}, // IEを追加
    { 'browserName': 'MicrosoftEdge'} // Edgeを追加
  ],
   // directConnect: true, 無効化
```

以上でIE11をProtractorで操作できるようになりました！

#### Edgeで起動するための準備
次は、Edge（chronium版じゃないやつ）の設定です。
まずは、ご自身が使用されているEdgeのバージョンを確認した後、下記サイトからDriverをDLしてきます。

[Microsoft WebDriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/#downloads)

Microsoft Edge version 18 以降の場合、下記コマンドを実行することでDLすることができます。

```
DISM.exe /Online /Add-Capability /CapabilityName:Microsoft.WebDriver~~~~0.0.1.0
```

DLが完了すると、以下のフォルダに`MicrosoftWebDriver.exe`がDLされているので
Angularのディレクトリにコピーしておきます。

```.sh:
C:¥Windows¥System32¥MicrosoftWebDriver.exe
```

以上でProtractorでテストを行える準備は整いました。

### テストを実行する

Selenium Server経由でProtractorを実行する場合、Selenium Serverを起動しておく必要があるため起動しておきます。

```
webdriver-manager start --edge <MicrosoftWebDriver.exeのパス>
```

あとは、同じようにテストを実行してみます。

```
$ npx protractor ./e2e/protractor.conf.js --specs=./e2e/src/**/*
```

![Success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54858/ff7ea029-bb00-697d-7b3f-675d501d49fb.png)

　　　＿人人人人人人人＿
　　　＞　Success！！　＜
　　　￣Y^Y^Y^Y^Y^Y^Y^￣


マルチブラウザで同時にテストが実行されるようになりました。
４ブラウザが起動して自動で動いていくのを見るのはなかなかおもしろいです。
## おわりに

なかなかコストが高く、手を出しづらそうなE2Eテストですが、適切に実装することでプロジェクトの助けになります。
今からでも遅くないので、ぜひ取り入れてみてはいかがでしょうか？

11日目は @kawakami-kazuyoshi さんです。よろしくお願いします!

## 参考
E2Eテストコードのメンテナンス性に立ち向かう
https://qiita.com/tsuemura/items/c6703efc107314b66d7b
idやclassを使ってテストを書くのは、もはやアンチパターンである
https://qiita.com/akameco/items/519f7e4d5442b2a9d2da
こわくない Protractor
https://qiita.com/shuhei/items/6973fe694d29a193f224
PageObjects
https://github.com/SeleniumHQ/selenium/wiki/PageObjects
Cypress:Best Practices
https://docs.cypress.io/guides/references/best-practices.html