💯

Next.js × Cypressでe2eテストをする

2020/12/11に公開

@wataryooouです。
Next.jsをベースにブログサイトを作って、そのブログサイトのE2EテストをCypressで実装してみました。

対象読者

  • E2Eテストの書き方がわからない
  • Cypressを使ってみたい
  • なんとかログイン導線は死守したいけど、そもそもE2Eテスト環境をまだ導入していない

E2Eテストとは

E2EテストはEnd to End Testの略です。Unitテストのように1つの関数に対してテストをするのではなく、システム全体に対してテストを行います。

例えば、メールアドレスとパスワードを入れてログインするページがあったとします。
Unitテストでは、メールアドレスやパスワードを入れたときにそれぞれの書式が正しいかどうか判定するための関数をチェックします。
E2Eでは、メールアドレスとパスワードを入れてログインボタンを押し、正しくログインしたあとのページに遷移しているかをチェックします。

Cypress

E2EテストをリッチなUIでよしなに実行できます。
実際に使ってみて思ったこととしては、テストがなぜこけたのかがスナップショットからわかることで、修正のスピードが上がりました。また、仕様と違う動作をしているかどうかをすぐ確認できたり、Cypressの標準機能でiPhone8にサイズを変えたり、MacBookのサイズに変えたりしたことでUIが崩れてボタンが押せなくなったなどの人間が見たら一発でわかるような仕様にも気付けました。Real time reloads機能があることでテストを書き直したりコンポーネントをいじったあと、目視ですぐに確認できる体験もよかったです。
何かとテストを書くことが置き去りになってしまいますがデバッグの質が上がるだけでなく、テストを書く体験もよくなるので積極的に導入検討していただけたらと思います。詳しくはこちらの動画をみていただけたらと思います。Cypress Vimeo

Cypressを体験してみる

準備

$ git clone https://github.com/wataryooou/nextjs-cypress.git
$ cd nextjs-cypress
$ yarn
$ yarn dev
http://localhost:3000/にアクセスして、無事に起動したか確認してください。

作ったもの

こんな感じのブログをテストします。こちらを参考に実装しました。コードはここに貼っておきます。

image

Cypressの実行

$ yarn cy:open をすると以下のようなGUIが表示されます。このGUIの index.spec.js をクリックするとそのテストが走ります。右側にある Run 6 integration specs をクリックすることで全てのテストを走らせることも可能です。
image

$ yarn cy:run では全てのテストをCLIで実行できます。
$ yarn cy:run --spec <file path> ではfile pathのテストだけCLIで実行できます。
image

各テストの書き方

特定の画面が表示されているか

画面が表示されていることを満たす条件を考えましょう。今回の場合だと、

  1. 指定したURLにアクセスしている
  2. そのページに必ずあるものが表示されている

この2つを担保するテストが通れば、そのページが表示できているとします。では、その2つのテストを上に貼ってある「Webcome to page!」が表示されたページのテストを書いていきます。

1に関してはcy.location("pathname", { timeout: 10000 }).should("include", "/");を使うことでURLを取得できます。このページのURLはhttp://localhost:3000/ なので、上のままで大丈夫です。

2に関しては「Webcome to page!」が表示されているはずなので、この文言が正しく表示されているか確認します。その場合は2つあって、1つはcy.get("p").contains("Welcome to page!");というように pタグ に文言が入っているか調べるやり方です。
もう1つはhtmlに<p data-cy="welcome">Welcome to page!</p>というように、データタグを付与するやり方です。基本的にはこちらを推奨しています。この場合、cypressのテストコードではcy.get("[data-cy=welcome]").contains("Welcome to page!");と書いてあげると、タグの文言に正しく「Welcome to page!」が入っているかをチェックできます。

後者をおすすめする理由はたくさんありますが、htmlをpタグからspanタグに入れ替えたときにテストコードも変える手間がなくなったり、複数ボタンが存在するときに特定のボタンをクリックできたりとテストの安全性が高まります。

サンプルコード: 特定の画面が表示されているか
index.spec.js
/// <reference types="cypress" />

context("/ - Home", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("アクセスしたURLがあっている", () => {
    cy.location("pathname", { timeout: 10000 }).should("include", "/");
  });

  it("ページが表示されている", () => {
    cy.get("[data-cy=welcome]").contains("Welcome to page!");
  });
});

beforeEachはそれぞれのテスト(it)前に実行するものを書きます。この場合、cy.visit("/");が書いてありますがこれは/ ページにアクセスするという意味です。

このサンプルコードの実行順序は、

  1. / ページにアクセスする
  2. アクセスしたURLがあっている - test
  3. / ページにアクセスする
  4. ページが表示されている - test

となっています。E2EテストはそこまでUnitテストのように分割する必要がないのと、これだと毎回アクセスするためより多くの時間がかかってしまいます。

そのため、次の改良版で書いているように、2つのテストをまとめてしまうことが望ましいです。この変更でどれくらい早くなるかというと、改良前は 1.47sec かかっていたものが改良後には 0.75sec になりました。

改良版サンプルコード: 特定の画面が表示されているか
index.spec.js
/// <reference types="cypress" />

context("/ - Home", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("ページが表示されている", () => {
    cy.location("pathname", { timeout: 10000 }).should("include", "/");
    cy.get("[data-cy=welcome]").contains("Welcome to page!");
  });
});

ユーザの操作に対して正しい挙動をするかチェックする

ユーザが操作することといえば、何かをタイピングしたり、ボタンをクリックしたりすることが挙げられます。
今回はこの2つをテストでチェックしていきたいと思います。

タイピング

cy.get("[data-cy=input]").type("タイプしたい文字列");とすることで、その要素に対してタイピングすることができます。
/sampleページでタイプしたものがそのまま表示されるかテストしているのでそちらを参考にしてみてください。

サンプルコード: タイピングしたら同じものが表示される
index.spec.js
/// <reference types="cypress" />

context("/sample - サンプルページ", () => {
  beforeEach(() => {
    cy.visit("/sample");
  });

  it("タイピングしたら同じものが表示される", () => {
    cy.get("[data-cy=input]").type("123456abcdef");
    cy.get("[data-cy=output]").contains("123456abcdef");
  });

});
要素のクリック

cy.get("[data-cy='About']").click();とすることで、その要素に対してクリックすることができます。
ページ上部に表示されているnavをクリックして各ページに遷移しているかをテストするコードを書いたのでそちらを参考にしてみてください。

サンプルコード: 全てのページに遷移できるか
index.spec.js
/// <reference types="cypress" />

context("/ - Home", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("全てのページに遷移できる", () => {
    cy.get("[data-cy='About']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/about");

    cy.get("[data-cy='Posts']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/posts");

    cy.get("[data-cy='Contact']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/contact");

    cy.get("[data-cy='Sample']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/sample");

    cy.get("[data-cy='Home']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/");
  });
});

テストをスキップさせたいとき

E2Eテストは不安定なところがあり、メンテを怠っているといつの間にかテストがこけるようになってしまいます。どうしてもテストを今すぐ改修できない場合は、xitというようにitの前にxを付与してあげることでそのテストをスキップすることができます。

サンプルコード: テストをスキップする
index.spec.js
/// <reference types="cypress" />

context("/sample - サンプルページ", () => {
  beforeEach(() => {
    cy.visit("/sample");
  });

  it("アクセスしたURLがあっている", () => {
    cy.location("pathname", { timeout: 10000 }).should("include", "/sample");
  });

  xit("このテストは実行されない", () => {
    cy.get("[data-cy=input]").type("123");
    cy.get("[data-cy=output]").contains("123");
  });
});

余談

エラーが起きる場合はこのように表示されます。このケースだと、同じ要素が2つ以上あるので、clickできないと言われています。今回はその要素で一番最初にヒットしたものをクリックするような処理にしていますが、テストを安定させるためにもできるだけユニークな値で設計していくことが望ましいです。また、data-cydiv などから取ってくる2つの手法で実装していますが、後者の場合だとdivの重複などが原因でこのエラーのようなケースが発生するので、前者をおすすめします。
image

最後に

E2Eテストは、DB/ネットワーク/デザインの変更/APIの影響を受けて不安定になってしまうことも実際の開発ではよく起こります。そのような不安定なテストとどう向き合うか、他のテストフレームワークと違って何がよいのかなどは別の記事にまとめていきたいと思います。個人的にはNext.jsとCypressの相性がいいなぁと思っているのでその件についても書きたいと思っています。

Discussion