🦉

CypressでFirebase Authenticationのe2eテスト書いてみる

2021/03/24に公開

概要

前々からスプレッドシートにテストケースかいて
何度もおててでテストするの辛いんじゃ。。。
と思いながらも中々e2eテストに手を出せてなかったので
ここいらで前から気になってたCypressに手を突っ込んでみる

導入

思ってた何倍か簡単だった🥸
packageをinstallしてopenすると...

yarn add -D cypress
yarn cypress open

cypressウィンドウが現れて
examples下のテスト用jsファイルを適当にクリックすると
Chromeブラウザが起動しテストが走り出す!全然設定とかいらない

テスト書いてみる

なんか最初から入ってたexampleのテストをやってみたので
自分で作った何かしらをテストしてみる!

対象のページ

とりあえずザクっと作ったこんな感じのページ

未認証でアクセスすると/に飛ばされる

Google認証すると「このサイトについて」リンクが現れる

「このサイトについて」リンクを押下すると/aboutページに遷移し
ログインユーザーのemailが表示される

書くテストケース

とりあえずこの辺書いてみる

  1. /ページ
    1. 認証済の動作
      1. 「このサイトについて」リンクが表示される
      2. 「このサイトについて」リンク押すと/aboutに遷移できる
      3. ログインユーザーのemail表示される

/ページ

Google認証

「Googleでサインインボタン押してログインしてぇ...」みたいなことを考えたくなるけど

CypressではGoogle認証みたいにドメインの異なるサードパーティー系のページに
UIでアクセスするテストはやめときましょうねって書いてある(気がする
自分がコントロールできる範囲内のテストにとどめましょう!とのこと
https://docs.cypress.io/guides/references/best-practices#Visiting-external-sites

認証状態を作りたいならStubかCy.request()とかでAPIアクセスで認証状態作るのが良さそう

とはいえ、自分のローカルではFirebase AuthenticationのEmulatorが動いてて
APIで操作できる範囲は全然なさそうなので、SDKで頑張って実現してみる

やったこと

ざっくりこんな流れ

  1. .envの環境変数をCypressでも読めるようにする
  2. Cypressでfirebaseの初期化
  3. テスト作成

そもそもアプリで利用してる.envファイルがこんな感じであるので
それをCypress側でもそのまま使いたい

frontend/.env
FIREBASE_API_KEY=xxx
FIREBASE_AUTH_DOMAIN=xxx
FIREBASE_PROJECT_ID=xxx
FIREBASE_STORAGE_BUCKET=xxx
FIREBASE_MESSEGING_SENDER_ID=xxx
FIREBASE_APP_ID=xxx
FIREBASE_MEASUREMENT_ID=xxx
FIREBASE_AUTH_EMULATOR_URL=xxx

.envの環境変数をCypressでも読めるようにする

frontend/cypress/plugins/index.js
require('dotenv').config();

/**
 * @type {Cypress.PluginConfig}
 */
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config
  config.env = process.env
  return config
}

Cypressでfirebaseを初期化する
(後のテストで認証データ作るために2メソッド用意してます

frontend/cypress/support/firebase.js
import firebase from "firebase/app";
import "firebase/auth";

const config = {
  apiKey: Cypress.env("FIREBASE_API_KEY"),
};

if (!firebase.apps.length) {
  firebase.initializeApp(config);
}

if (process.env.NODE_ENV !== "production") {
  const auth = firebase.auth();
  auth.useEmulator(Cypress.env("FIREBASE_AUTH_EMULATOR_URL"));
}

export const signInWithEmailAndPassword = async (email, password) => {
  return await firebase.auth().signInWithEmailAndPassword(email, password);
};

export const createUserWithEmailAndPassword = async (email, password) => {
  return await firebase.auth().createUserWithEmailAndPassword(email, password);
};

テストケースを作成していく!
毎回ドメイン書かなくていいようにconfigに書いておく

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

beforeEachで認証データを作って
上部で書いたケースを手順に書いてく

frontend/cypress/integration/index/auth.spec.js
import {
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
} from "../../support/firebase";

describe("Authentication test in index page", () => {
  const email = "test@example.com";
  const password = "123456";

  beforeEach(() => {
    signInWithEmailAndPassword(email, password)
      .then(async (user) => {
        await user.user.delete();
        await createUserWithEmailAndPassword(email, password);
      })
      .catch(async (e) => {
        await createUserWithEmailAndPassword(email, password);
      });
  });

  it("Login by firebase authentication", () => {
    cy.visit("/");
    cy.get("[data-cy=link-to-about]").click();
    cy.url().should("include", "/about");

    cy.get("[data-cy=user-email]").should("contain", email);
  });
});

Best Practiceにあるようにdata-cy属性を指定して、それをcy.get()で取得するようにしてる

frontend/pages/index.tsx
      <main className={styles.main}>
        <h1 className={styles.title}>{t("title")}</h1>
        {currentUser ? (
          <Link href="/about">
            <a data-cy="link-to-about" >{t("buttons.about")}</a>
          </Link>
        ) : (
          <GoogleSignInButton onClick={signIn} />
        )}
      </main>

実際にテストしてみる

作成したテストをクリックするとテスト動き出す!

うまいことテストできた!!

テスト結果の画像と動画が見れるという噂

Runsタブが怪しいところ

適当なプロジェクトを作成して選択!
(Cypressにサインアップしてサインインしとく必要はありそう

あとは言われた通りのコマンドをローカルで叩いてみると・・・

テスト走り出した!!

$ yarn cypress run --record --key xxx
yarn run v1.22.10
$ /Users/su/ghq/github.com/shintaro-uchiyama/base-app/frontend/node_modules/.bin/cypress run --record --key xxx
Missing baseUrl in compilerOptions. tsconfig-paths will be skipped

====================================================================================================

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:    6.8.0                                                                              │
  │ Browser:    Electron 87 (headless)                                                             │
  ...

さっきのRunsに実行してる感じのクルクルが出てくるのでクリックするとブラウザに飛ぶ
テスト実行時のスナップショットやビデオが見れる!素敵!

なんかローカルにもscreenshotsとかvideosディレクトリができてファイルいっぱいできてる🤓
Dashboard使わなくてもローカルでscreenshot, videoは記録できそう

$ git status
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	cypress/fixtures/profile.json
	cypress/fixtures/users.json
	cypress/screenshots/
	cypress/videos/

CIで実行してみる

Github ActionsでCypress走らせてみる!

GitHub Actions

とりあえず結果から書くとこんな感じ
githubのsecretsにFIREBASE_API_KEYCYPRESS_RECORD_KEYをセットしといてくだされ

.github/workflows/cypress.yml
name: Cypress Tests with Install Job and UI Chrome Job x 3

on:
  pull_request:
    branches:
      - main

jobs:
  install:
    runs-on: ubuntu-20.04
    container: cypress/browsers:node14.16.0-chrome89-ff86
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - uses: actions/cache@v2
        id: yarn-and-build-cache
        with:
          path: |
            ~/.cache/Cypress
            frontend/build
            frontend/node_modules
          key: ${{ runner.os }}-node_modules-build-${{ hashFiles('frontend/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-node_modules-build-

      - name: Cypress install
        uses: cypress-io/github-action@v2
        with:
          # Disable running of tests within install job
          runTests: false
          install-command: yarn --frozen-lockfile --silent
          working-directory: frontend

  ui-chrome-tests:
    runs-on: ubuntu-20.04
    container: cypress/browsers:node14.16.0-chrome89-ff86
    needs: install
    strategy:
      fail-fast: false
      matrix:
        # run copies of the current job in parallel
        containers: [1, 2, 3]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - uses: actions/cache@v2
        id: yarn-and-build-cache
        with:
          path: |
            ~/.cache/Cypress
            frontend/build
            frontend/node_modules
          key: ${{ runner.os }}-node_modules-build-${{ hashFiles('frontend/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-node_modules-build-

      - name: 'UI Tests - Chrome'
        uses: cypress-io/github-action@v2
        with:
          # we have already installed all dependencies above
          install: false
          start: |
            yarn firebase emulators:start --import=./firebase/data --export-on-exit --project ucwork --only auth,ui
            yarn dev
          wait-on: 'http://localhost:3000, http://localhost:4000'
          wait-on-timeout: 120
          browser: chrome
          record: true
          parallel: true
          group: 'UI - Chrome'
          spec: cypress/integration/**/*
          working-directory: frontend
        env:
          FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
          FIREBASE_AUTH_EMULATOR_URL: 'http://localhost:9099'
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          # Recommended: pass the GitHub token lets this action correctly
          # determine the unique run id necessary to re-run the checks
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

やったことのポイントは以下

  1. main向けにPR作ったらGitHub Actions走らせてみる
  2. yarnでpackageをインストール。そしてキャッシュ
    1. package(yarn.lock)に変更がない場合、次のCIからキャッシュが効いて早くなる!
  3. cypressのテスト用に色々環境起動
    1. firebase emulator起動
      1. これが結構大変で詰まった
    2. 3000ポートで動くnuxtアプリの起動(yarn dev)
  4. cypressを並行で走らせる

ちょっとブログ書くの疲れてきたので細かい説明は諦める

検証

実際に動いてるとここんな感じ

並列でテスト実行できるのかなぁとか気になって実は以下テストを追加して検証してる
試しにこんな感じでもう一つaboutページ向けのテストファイルを作って

frontend/cypress/integration/about/unauthenticated.spec.js
describe("Unauthenticated test in about page", () => {
  it("Access to about page w/o authenticated", () => {
    cy.visit("/about");
    cy.url().should("include", "/");
  });
});

parallel: trueを指定して
3つのcontainerで3並列実行してみる!
(2ファイルしかないのに3並列したらどうなるかちょっとみたかった🤓

    strategy:
      fail-fast: false
      matrix:
        # run copies of the current job in parallel
        containers: [1, 2, 3]

今回で言うと
1つコンテナで/ページ用のテストが実行されて
2つコンテナで/aboutページ用のテストが実行されて
3つコンテナではどのテストも行われてなかった!!

この辺の並列度合いはCypressのダッシュボードに聞いて
どう言う並列するのが最適かよろしくやってくれるらしい!素敵!
(テスト実行数500超えて有料になっちゃったらどうなるんだろ・・・🧐

ブラウザ単位で並列化とか自分で恣意的に色々やることもできるみたい

まとめ

思ったよりも量が多くなってきて後半疲れました。すみません🙈

やる前は結構気合入れないと導入できないのかな・・・
とか思ってらけどやってみると案外スッとできた。

これからe2eテスト未導入プロジェクト見つけたら突っ込んでいこう!

Discussion