Page Object Patternを使うな、というCypress公式記事を読んで思ったこと

※この記事は[ただの集団 Advent Calendar 2019](https://adventar.org/calendars/4054)の11日目の記事です。

# はじめに
若干過激なタイトルにしたことを最初にお詫びします。
正確には以下の記事を読んで思ったことです。

[Stop using Page Objects and Start using App Actions](https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/) 
Page Objectsを使うのはやめて、App Actionsを使おう （著者訳）

引用元はCypressの公式ブログです。
全編英語なので、「読むの面倒くさいよぅ」という方には、本記事が内容理解に役立つかもしれません。

# Summary
**1.前提**
**2.Page Object Pattern推奨派の主張**
**3.Page Object Pattern否定派の主張**
**4.記事を読んで思ったこと**
**5.最後に**

説明の都合上、まずPage Object Pattern推奨派の考え方を簡単に説明し、その後にPage Object否定派（Cypressの記事内容）を紹介します。
最後に、記事を読んで思ったこと（私個人の意見）を掲載させていただきたいと思います。

# 前提
##### 対象読者
- end-to-endテストに興味がある人
- Page Object Patternについてなんとなく知っている人
- UIテストの自動化ってなんかうまくいかないよなぁ、と思っている人

##### Cypressについて
- 2017年に公開された、かなりモダンなend-to-end testing frameworkです
- 2004年に公開され、その後長きにわたって多くの開発者を苦しめた（？笑）、Seleniumに代わる自動化ツールとして期待されています（主観あり）
- かなり優秀です（主観あり）

##### Page Object Patternについて
- end-to-endテストを実装する際に使われるデザイン・パターンのことです
- ものすごく簡単に言うと、「UIテストに必要なDOM操作をテストコードに書かないで、Page Objectに集約させてしまおう」という考え方です
- あとでもう少しだけ解説しますが、詳細が知りたいよという方はご自身で調べてみてください

# Page Object Pattern肯定派の主張
最初にPage Object Patternの簡単な解説と、一般的に言及されているメリットをご紹介します。

##### Page Objectの（ものすごく簡単な）解説
もともとは「UIテストに必要なDOM操作を一箇所にまとめよう」という発想から生まれました。
DOM操作のまとめ先がPage Objectということになります。

以下簡単な例を示します。
まずはPage Objectを使わないケースから。

- ログイン操作のテスト（**Page Objectを使わない場合**）

```javascript
it('ログインに成功すること', function() {
    this.driver.open('/login.html');
    this.driver.find('#username').set('scott');
    this.driver.find('#password').set('tiger');
    this.driver.find('form button[type="submit"]').click();

    assert.equals(this.location.pathname, '/dashboard.html');
    assert.equals(this.driver.find('#flashMessage').text(), 'ログインに成功しました');
}
```
ご覧の通り、テストコード内にDOM操作に関する処理が散らばっています。
次にPage Objectを使った例を見てみます。

- ログイン操作のテスト（**Page Objectを使った場合**）
 
```javascript
// test code
it('ログインに成功すること', function() {
    const loginPage = new LoginPage(driver).open();
    loginPage.setUsername('scott');
    loginPage.setPassword('tiger');
    const nextPage = loginPage.submit();

    assert.equals(nextPage.url(), '/dashboard.html');
    assert.equals(nextPage.flashMessageText(), 'ログインに成功しました');
}

// Page Object
public class LoginPage {
    // こんな感じの処理がたくさん書かれます
    get username() { return this.driver.find('#username'); }
    get password() { return this.driver.find('#password'); }

    // DOM操作メソッドなども提供します（多くの場合）
    setUsername(value) { this.username().set(value); }
    setPassword(value) { this.username().set(value); }

    // 以下省略（Page Objectの解説は本記事の主題ではないため）    
}
export default LoginPage;
```
ご覧の通り、テストコードからDOM操作に関する処理が一掃されています。
この調子で、ログインに失敗するテストケースを書く場合も、同じPage Objectを使い回すことが可能です。

##### Page Object Patternを使うメリット
- テストコードからDOM操作が排除され、読みやすくなる（可読性）
- 同じDOM操作コードを何度も書く必要がなくなる（Don't Repeat Yourself）
- class名などDOM構造に変更があった場合に、Page Objectだけを修正すれば事足りる（変更容易性）


# Page Object Pattern否定派の主張
次に、本題の「Cypressの記事=Page Object否定派の主張」を見てみます。
記事では、はっきりと「Page objects problems」と謳って、その問題点を指摘しています。
記事の一部を引用してみます。

##### Page Object Problems
1. Page objects are hard to maintain and take away time from actual application development.<br />
   I have never seen PageObjects documented well enough to actually help one write tests.
2. Page objects introduce additional state into the tests, which is separate from the application’s internal state.<br />
   This makes understanding the tests and failures harder.
3. Page objects try to fit multiple cases into a uniform interface, falling back to conditional logic - a huge anti-pattern in our opinion.
4. Page objects make tests slow because they force the tests to always go through the application user interface.


##### Page Objectの問題（著者訳 <- 意訳あり）
1. Page objectは保守が難しく、実サービスの実装時間を奪う。<br />
   testを書くのが楽になるほど洗練されたPage Objectなんて見たことがない。
2. Page objectはテスト専用の「状態」を管理しなければならない。これはサービスの「状態」とは別物である。このせいでテストの内容やテストが失敗した理由を理解するのに時間がかかる。
3. Page objectは１つのインターフェースに複数のパターン動作を処理させようとするので、コード内に分岐ロジックが発生する。我々の意見では、これはメガ・アンチパターンである。
4. Page objectを使うとテストが遅くなる。なぜならPage objectを使うと、テストが必ずUI経由になってしまうからである。<br />
   ※補足 -- 記事の筆者は「UIテストでもlocal storageデータなんて直接生成してしまえ」という考え方を持っています。

##### 代替案：App Actionsの提案
さらに、DRY（Don't Repeat Yourself）の観点については、「**Application Actions**というSolutionを用意したのでそれを使え」と提案しています。
以下、上のログインテストの例でサンプルコードを紹介します。

```javascript
// test code
it('ログインに成功すること', function() {
    cy.visit('/login.html');
    cy.login('scott', 'tiger');

    cy.url().should('eq', '/dashboard.html');
    cy.get('#flashMessage')should('eq', 'ログインに成功しました');
}

// App Actions
Cypress.Commands.add('login',(username, password) => {
    cy.get('#username').type(username);
    cy.get('#password').type(password);
    cy.get('form button[type="submit"]').submit();
})
```

##### Cypressの考えるテストコードの実装方針
また、（ブログではなく）公式ドキュメントの中でこんなことも言っています。
> Test code serves a different purpose than app code. Understandability and debuggability should be prioritized above all else.
> テストコードとサービスコードでは目的が違う。（テストコードでは）理解しやすさとデバッグのしやすさが何よりも優先される。（著者訳）

※[「Custom Commands | Cypress Documentation - #Best Practice」](https://docs.cypress.io/api/cypress-api/custom-commands.html#Best-Practices)より引用


##### 記事の主張まとめ
- Page Objectはコードの理解に時間がかかるので、実装が大変（開発コスト）
- Page Objectは、Page Object自体のメンテナンスが結構大変である（保守コスト）
- DRY（Don't Repeast Yourself）とかテストコードではあまり気にするな（可読性及びデバッグ容易性）


# 記事を読んで思ったこと
Cypress開発者の意見を聞いて、あなたはどう感じましたか？
Page Objectを使った開発者はカメのように開発がのろまで、しかも保守・運用フェーズでも苦労しているトントンチキに思えたでしょうか？

私個人はというと、やはりCypressの記事を読んで「なるほどな」と思うところはありました。
しかし、それでも（当時の）Page Object支持派の気持ちもわかるのです。
だって、**Seleniumは本当によく落ちたのだから...！笑**

DOMの取得一つとっても、そんなわかりやすく「cy.get('#username')」じゃなかったんです。
「this.driver.find('form.edit-todo input[name="tomorrow"]').prev().prev().siblings()[0]」みたいなセレクタでないと取得できないDOMがあったのです。
そりゃせめてPage Objectの一つでも用意して

```javascript
getTommorrowTodoMsg() {
    return this.driver.find('form.edit-todo input[name="tomorrow"]').prev().prev().siblings()[0];
}
```
としたくなる気持ちにもなるでしょう、と。
（↑ちなみにこのセレクタはdivの階層が一つでも変わると落ちます。そんなDOM取得があちこちに散らばっていたら...という恐怖）
（そして何も悪くないのに時々落ちるというSeleniumの恐怖）
（そもそもdebugしづらいというSeleniumの恐怖）
（せめてDOM取得の処理だけでもPage Objectに集約して、原因の切り分けを事前に行いたいという淡い期待...）

いろいろ書きましたが、最近仕事でCypressを使うようになって、とにかく「Cypress優秀だなあ」と思って暮らしています。
Page Objectの善悪については、まだ判断を保留させていただいていますが、ひとまずCypress精神に従って実装している今日この頃です。


# 最後に
実はこれが一番言いたかったことでもあるのですが、この手の議論が発生した時に一番大切なことは「**自分の頭で考えよう**」ということだと思っています。個人的に。
よく「誰々がこう言っていたから」とか「Googleはこうだから」とか言う意見を聞きますが、じゃあ「あなたのサービスはSpread Sheetなんですか？」と問いたい（時がある笑）。

おそらくですが、その人が携わっているサービスは、Googleのサービスとは何から何まで違います。
サービス要件、言語、インフラ構成、開発人数、QA体制、保守体制、テストにかけられるコスト、オーナーの品質に対する理解...。
そもそも求める品質や投入できるコストも違うのだから、同じ方法論は通用しないだろうと私は思うのです。

もちろんGoogleなりCypressなりの偉い人が言うことを参考にして「やってみる」のはアリだと思います。
しかし、それはあくまで**「仮説」**であって（あくまで**そのサービスで通用する可能性があるという「仮説」**であって）、そこには必ず**「検証」**が必要なはずです。
やってみてうまくいったならOKなのですが、うまくいかなかったのであれば見直しが必要です。

この記事の主題である「Page Objectの善悪」についても、究極を言えば「プロジェクト次第」なのではないかなあと、個人的には思っていたりします。

長くなりましたが、ここまでお読みいただいた方どうもありがとうございました。

tumo.jp