Cypressハンズオン

E2Eテストを実施するにあたり、Cypressというテストフレームワークを導入してみます。

[JavaScript End to End Testing Framework | cypress.io](https://www.cypress.io/)

他に[Selenium](https://www.selenium.dev/documentation/ja/)や[TestCafe](https://devexpress.github.io/testcafe/)など候補はありましたが、導入の手軽さとテストの書きやすさの評判がよかったのでCypressに決めました。

## 準備

今回はReactプロジェクト（SPA）にE2Eテストを導入する体で進めてきます。

[Installing Cypress | Cypress Documentation](https://docs.cypress.io/guides/getting-started/installing-cypress.html)

**作業環境**
- macOS: 10.15.4
- npm: 6.14.4

まずは作業ディレクトリを作成します。

```bash
$ npx create-react-app cypress-demo --template typescript
# 色々インストールされる
$ cd cypress-demo
# 一応動作確認もします
$ yarn start
# dev server動けばok
```

#### SPA準備

**・react-router-domインストール**

```bash
$ yarn add react-router-dom @types/react-router-dom
```

`App.tsx`に、[REACT TRANING](https://reacttraining.com/react-router/web/guides/quick-start/2nd-example-nested-routing)のやつをコピペして、Topicsコンポーネントにテスト用のクラス名を付与します。

```App.tsx
import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useRouteMatch,
  useParams
} from 'react-router-dom';

export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to='/'>Home</Link>
          </li>
          <li>
            <Link to='/about'>About</Link>
          </li>
          <li>
            <Link to='/topics'>Topics</Link>
          </li>
        </ul>

        <Switch>
          <Route path='/about'>
            <About />
          </Route>
          <Route path='/topics'>
            <Topics />
          </Route>
          <Route path='/'>
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Topics() {
  let match = useRouteMatch();

  return (
    // テスト用にクラス追加してます
    <div className='topics'>
      <h2>Topics</h2>

      <ul>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${match.url}/props-v-state`}>Props v. State</Link>
        </li>
      </ul>

      {/* The Topics page has its own <Switch> with more routes
          that build on the /topics URL path. You can think of the
          2nd <Route> here as an "index" page for all topics, or
          the page that is shown when no topic is selected */}
      <Switch>
        <Route path={`${match.path}/:topicId`}>
          <Topic />
        </Route>
        <Route path={match.path}>
          <h3>Please select a topic.</h3>
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  let {topicId} = useParams();
  return <h3>Requested topic ID: {topicId}</h3>;
}

```

保存して実行してみます。

```bash
$ yarn start
```

以下のような画面になりました。

[![Image from Gyazo](https://i.gyazo.com/3ff544f592c40b8965b443de5719a1d7.gif)](https://gyazo.com/3ff544f592c40b8965b443de5719a1d7)

これで環境の準備が整ったので、Cypressを導入してきます。

### Cypressインストール

```bash
$ yarn add cypress
```

直接ダウンロードもできますが、非推奨。
依存ライブラリのインストールも自動で行ってくれるnpmの使用を推奨してます。

### Cypress初期化

インストールしたら、次は初期化をおこないます。

```bash
$ yarn run cypress open
```

上記のコマンドを実行すると、自動でいくつかのフォルダとテスト例を作りますけどいいですか？というポップアップが出てくるので、OK, got it!しましょう。

そうすると、テストランナーのGUIが立ち上がります。

[![Image from Gyazo](https://i.gyazo.com/08d40e0b2187671d11997c2bacfec84b.png)](https://gyazo.com/08d40e0b2187671d11997c2bacfec84b)


テストランナーの実行を短縮コマンドでできるよう`package.json`のscriptsに、cypress用のコマンドを追加します。

```package.json
{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "cypress:open": "cypress open"
  }
}
```

これで以下のコマンドで実行できるようになりました。（あまり短くなってないですね）

```bash
# 一度Cmd+Cでテストランナー落として
$ yarn cypress:open
# 再度テストランナー起動したはず
```

### テストを実行する

ためしに一つテストを動かしてみます。
`actions.spec.js`をクリックしてみると、、、

[![Image from Gyazo](https://i.gyazo.com/ef91180e2f0d1450e2fdeba31bc5cc7a.gif)](https://gyazo.com/ef91180e2f0d1450e2fdeba31bc5cc7a)

自動でブラウザが立ち上がって`actions.spec.js`に定義されたテストが始まります。



テストしたいファイルをクリックしたら実行されるみたいですけど、クリックせずともテスト実行したい場合、どうやってやるでしょう？
CIでも使えるそうなので方法はあると思いますが、あとで調べます。

### テストを作る

実行する方法も分かったので、次はテストを自作しましょう。

Cypressの初期化をおこなった時点でルートに`cypress`フォルダと`cypress.json`が作られています[^1]。

`cypress`フォルダ直下の`integration`フォルダ内に、`home_page_spec.js`というファイルを作って、テストコードを書いていきます。

```bash
$ touch ./cypress/integration/home_page_spec.js
```

```home_page_spec.js
describe ('The Home Page', () => {
  it ('successfully loads', () => {
    cy.visit ('localhost:3000');
  });
});
```

テストの書き方は他のテストライブラリと大体同じですね。

それではテストランナーを立ち上げて、動作確認してみます。

```bash
# まずdev server立ち上げて
$ yarn start
# 別タブひらいてプロジェクトルートに移動して、cypress起動
$ cd /your/project-route/cypress-demo
$ yarn cypress:open
```

expamlesフォルダの下に作成した`home_page_spec.js`があるので、クリックしてテスト実行します。

[![Image from Gyazo](https://i.gyazo.com/21f949673606c045c4022a8340b10f72.png)](https://gyazo.com/21f949673606c045c4022a8340b10f72)

無事にSuccessしました。

とくにassertionを書いてませんが、`cy.visit`で引数のURLにアクセスし、200が返ってきたらテストが通ります。
なのでここを`localhost:300`とかにするとテスト失敗します。

また、アクセス対象のURLを毎回httpから書くのは手間なので、`cypress.json`に記述しましょう。

```cypress.json
{
    "baseUrl": "http://localhost:3000"
}
```

こうすると、テストコードでは`/`と指定するだけでアクセスできるようになります。

```home_page_spec.js
describe ('The Home Page', () => {
  it ('successfully loads', () => {
    cy.visit ('/');
  });
});
```

あとは任意のシナリオにそってテストを書いてきます。
以下ではSPAでの遷移できてるか（url変わってるか）、コンポーネントの描画できてるか、などをテストしてます。

```home_page_spec.js

describe ('The Home Page', () => {
  it ('successfully loads', () => {
    cy.visit ('/');
  });
  it ('visit About Page after "About" clicked', () => {
    cy.visit ('/');
    cy.contains ('About').click (); // 「About」文字列を探し、クリックする。　見つけれないorクリックできないとテスト失敗

    cy.url ().should ('include', '/about'); // urlに「/about」が含まれているか
  });
  it ('visit Topics Page after "Topics" clicked', () => {
    cy.visit ('/');
    cy.contains ('Topics').click ();

    cy.url ().should ('include', '/topics');
    cy.get ('.topics li').should ('have.length', 2);　// cy.getのセレクタはjqueryと同じ
  });
});

```

各APIの説明は公式に載ってるので適宜読みましょう。
[Table of Contents | Cypress Documentation](https://docs.cypress.io/api/api/table-of-contents.html)

最後ざっくりですが、これでハンズオン終わりです。

## さいごに

今回作ったレポジトリ：https://github.com/hiroshi-kato/cypress-demo

あとは適切なシナリオの作成、TypeScript化、circleCIでの実行あたりをやっていきたい。

## 参考
- [Table of Contents | Cypress Documentation](https://docs.cypress.io/api/api/table-of-contents.html)
- [Cypress - TypeScript Deep Dive 日本語版](https://typescript-jp.gitbook.io/deep-dive/intro-1/cypress)
- [Cypress をお供にE2E受け入れテスト駆動開発 〜そしてAutifyへ〜｜seya｜note](https://note.com/seyanote/n/n68825bf83138)


[^1]: cypress:openで作成されたフォルダ等の説明は[Cypress - TypeScript Deep Dive 日本語版](https://typescript-jp.gitbook.io/deep-dive/intro-1/cypress#ktonarufairuno)を参照ください。