Cypress + TypeScript + Reactを導入してみた

# 概要
業務でCypressを触る機会があったので、
CypressテストをTypeScriptで書いてみて、簡単なReactアプリの上で実行してみます。

# Cypressとは
- E2Eテストのツールでブラウザベース(Chrome,FireFox,Edge)でのテストを実行可能。
- HTMLエレメントを直感的に指定してコードを書くことが可能なツール。
- Node.js環境で速やかに構築・実行が可能。

# 導入
## インストール
- [Typescript側のドキュメント](https://typescript-jp.gitbook.io/deep-dive/intro-1/cypress)
- [Cypress側のドキュメント](https://docs.cypress.io/guides/tooling/typescript-support)

TypeScript側の方がわかりやすかったのでそちらを参考に進めます

### 環境
OSはmacOS Monterey `12.2.1`
```bash
% node -v
v18.0.0
```
### 手順
```bash
% mkdir e2e
% cd e2e
# 初期化。package.jsonの作成
% npm init -y 
# TypeScript、Cypressのインストール
% npm install cypress typescript
added 166 packages, and audited 167 packages in 29s

27 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

# tsconfig.jsonの作成
% npx tsc --init --types cypress --lib dom,es6
Created a new tsconfig.json with:
  target: es2016
  module: commonjs
  lib: dom,es6
  strict: true
  types: cypress
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig

# cypress.json(cypress設定ファイル)の作成
% echo {} > cypress.json

```
### 作成されたファイル
```json:package.json
{
  "name": "e2e",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cypress": "^9.7.0",
    "typescript": "^4.7.2"
  }
}
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "lib": ["dom", "es6"],
    "module": "commonjs",
    "types": ["cypress"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
```

```json:cypress.json
{}
```
## テスト作成
e2e/cypress/integration以下にテストを作成。
```typescript:e2e/cypress/integration/HelloWorld.ts
it("is serching hello world", () => {
  cy.visit("https://google.com");
  cy.get('[name="q"]').type("Hello, World!").type("{enter}");
});
```
- テストは`it`で始めます。構文は`it("説明",()=>{テスト})`です。
- テストでcypressを利用する際は`cy.〇〇`でコマンドを書きます。
cy.visit(URL)でURLへアクセス
cy.get()でHTML要素の取得などです。
- コマンドのリファレンスは[こちら](https://docs.cypress.io/api/table-of-contents)

## テスト実行
テストは２通りの方法で実行可能。
- GUIベースの実行。コマンドは`npm run cypress open`
- CLIベースの実行。コマンドは`npm run cypress run`

package.jsonに記述しておきます。
```diff:package.json
{
  "name": "e2e",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "cy:gui": "cypress open",
+    "cy:cli": "cypress run"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cypress": "^9.7.0",
    "typescript": "^4.7.2"
  }
}
```

#### GUIテスト実行
```bash
% npm run cy:gui
```
初回は`It looks like this is your first time using Cypress: 9.7.0`と表示された後、以下のような画面が立ち上がる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636746/31ebffe1-5b3a-7e2f-6aba-80239b0444d5.png)
`HelloWorld.ts`をクリックするとテストが開始される。
デフォルトではChrome上で開始されるが、`--browser ***`としてブラウザを指定すると任意のブラウザで実行できる。
https://docs.cypress.io/guides/guides/launching-browsers#Browsers
# Reactアプリのテスト
Google検索で「Hello,World!」が検索できることをテストしました。
だが、実際はGoogleやAmazonのWebサイトをテストするのではなく、自分のアプリをテストするのに使うはず。
なので、実際に(かなり初歩的な)アプリを作ってcypressでE2Eテストを実装していくこととします。
## Reactアプリ作成
Reactをインストールして、`https://google.com`へのリンクを作成しました。
```typescript:index.ts
import React from "react";
import ReactDOM from "react-dom/client";
import GoogleLink from "./GoogleLink";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <div>
      <GoogleLink />
    </div>
  </React.StrictMode>
);
```
```typescript:GoogleLinx.tsx
function GoogleLink() {
  return (
    <>
      <a href="https://google.com">Google</a>
    </>
  );
}

export default GoogleLink;
```
## Reactアプリテスト
Cypressの方も少し修正します。
```diff:e2e/cypress.json
- {}
+ { "baseUrl": "http://localhost:3000/", "chromeWebSecurity": false }
```
```diff:e2e/cypress/integration/HelloWorld.ts
it("is serching hello world", () => {
-  cy.visit("https://google.com");
+  cy.visit("/");
+  cy.contains("Google").click();
-  cy.get('[name="q"]').type("Hello, World!").type("{enter}");
+  cy.url().should("contain", "google.com");
});
```

テストとしてはReactアプリ上のリンクをクリックして`google.com`を含むURLにリンクできるかのテストを作成しました。
※今回はこのようなテストを作成しますが、cypressは外部リンクにアクセスするようなE2Eテストを推奨していません。
https://docs.cypress.io/guides/references/error-messages#Cypress-detected-a-cross-origin-error-happened-on-page-load

## Best Practice
CypressのBest PracticeではHTML要素の取得のしかたが記述されています。
https://docs.cypress.io/guides/references/best-practices#Selecting-Elements
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636746/66beff4c-7d13-bca3-de4b-c34133b87c82.png)

今回の例のように`cy.contains()`を利用して要素を取得するのは「Best」ではないです。
というのも、似たような文言が羅列されているページでは`contains`つまりページ上の文字では判別がつかないことが多いからです。

Reactを以下のように修正してみました。
```typescript:DummyLink.tsx
function DummyGoogleLink() {
  return (
    <>
      <a href="">Dummy Google</a>
    </>
  );
}

export default DummyGoogleLink;
```
```diff:index.ts
import React from "react";
import ReactDOM from "react-dom/client";
import DummyGoogleLink from "./DummyLink";
import GoogleLink from "./GoogleLink";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
+    <div>
+      <DummyGoogleLink />
+    </div>
    <div>
      <GoogleLink />
    </div>
  </React.StrictMode>
);
```
これで同じテストを実行してみると、
`Dummy Google`リンクをcypressがクリックして失敗してしまいます。
「Google」という文言がcontainしているからです。

これを避けるためにBest Practiceである。data属性を追加してみます。
```diff:GoogleLinx.tsx
function GoogleLink() {
  return (
    <>
-      <a href="https://google.com">Google</a>
+      <a data-cy="correct" href="https://google.com">Google</a>
    </>
  );
}

export default GoogleLink;
```
```diff:DummyLink.tsx
function DummyGoogleLink() {
  return (
    <>
-      <a href="">Dummy Google</a>
+      <a data-cy="dummy" href="">Dummy Google</a>
    </>
  );
}

export default DummyGoogleLink;
```
```diff:HelloWorld.ts
t("is serching hello world", () => {
  cy.visit("/");
-  cy.contains("Google").click();
+  cy.get("[data-cy=correct]").click();
  cy.url().should("contain", "google.com");
});
```

こちらでテストすると想定通り、Googleへリンクし成功します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636746/79a9bc44-e321-bb7c-1449-5a4a9dac79c5.png)


# まとめ
- CyoressはNode.js環境で速やかに構築・実行ができるE2Eテストツール。
- デプロイ後でもローカルでもテストが可能。
- 要素取得の際にはあらかじめ付与したdata属性を用いてテストを記述するのがBest Practice。