フューチャー技術ブログ

保守・拡張をしやすいカプセル化したCypress

一行まとめ

壊れやすい上に読みにくくなりがちなE2Eテストは、cypressのCustom Commandsなどでカプセル化すると読みやすくなおしやすい。

CypressでE2Eテスト

Future Tech Night #8 というイベントで、E2EテストをCypressで快適に行う方法を紹介しました。文字起こし兼補足として投稿します。

同イベントの他の発表も記事として投稿されてますので、こちらもご覧いただければ。

E2Eテストは壊れやすい

まずはE2Eテストとユニットテストを比較して、それぞれの特長をみてみましょう。

ユニットテストは基本的に開発するときに部品単位でつくられて、リポジトリにpushする前にサクッと動かしてテスト通るか確認する、という使い方をします。

対して、E2Eテストは画面単位でつくられて、ユーザの視点から画面を叩いて動くかどうかを検証するという使い方をします。つまり、ユーザの視点から見て、あからさまにおかしいなと感じるバグが見つかりやすい。結果としてクレームや問い合わせが減りやすくなります。

E2E、統合、ユニットテストのピラミッド図

しかしながら、E2Eテストは壊れやすいのも特徴。

ユニットテストと比べてカバーする範囲が大きくなるので、どこかに変更があるとすぐ動かなくなってしまいます。例えば日本語の説明文に変更があったとかinputのnameが変わったとか、ちょっとしたことですぐ動かなくなってしまいます。

そして、画面の要素をidとかclassとかのセレクタで指定するため、どのセレクタがどのボタンを押してるのか追っていく必要があり、後から見たときに直しにくいのも難点。そのため、E2Eテストをつくるときは壊れることを前提に作っていくことが大事になってきます。特アジャイル的な開発をしているなら、機能追加の度にどこか動かなくなるという気持ちでいきましょう。

壊れたときに直しやすいように可読性をあげる

頻繁に壊れるということはコードを読み返すことも多くなるということ。壊れても直しやすいようにテストコードの可読性を上げていくことがメンテナンスを続けていくために大事になっていきます。

具体的な例としてToDoアプリを考えてみましょう。
Webアプリのチュートリアルによく出てくる、ToDoの追加と削除ができるページに対して Cypressでテストを行ってみます。
ToDoの追加と削除ができるページ

このToDoアプリに対して追加と削除が正常に動作しているか確認するE2Eテストを書いていきましょう。「Todo1」「Todo2」「Todo3」を追加して、2つ目を削除、残ってるTodoを確認する、というテストをCypressで実現すると以下のようになります。

it('add 3 todo and delete middle todo', () => {
// todo1を追加
cy.get('#new-todo')
.type('todo1').should('have.value', 'todo1')
.type('{enter}', { delay: 100 });
cy.get('#new-todo').should('have.value', '')
cy.get('.todo-item').contains('todo1');

// todo2を追加
cy.get('#new-todo')
.type('todo2').should('have.value', 'todo2')
.type('{enter}', { delay: 100 });
cy.get('#new-todo').should('have.value', '')
cy.get('.todo-item').contains('todo2');

// todo3を追加
cy.get('#new-todo')
.type('todo3').should('have.value', 'todo3')
.type('{enter}', { delay: 100 });
cy.get('#new-todo').should('have.value', '')
cy.get('.todo-item').contains('todo3');

// 2つ目を削除
cy.get('.todo-item:nth(1)').contains('DEL').click();

// 残アイテムの確認
cy.get('.todo-item').contains('todo1');
cy.get('.todo-item').contains('todo2').should('not.exist');
cy.get('.todo-item').contains('todo3');
})

Cypressを使ったことない、という方でも上のコードが何をしているかがなんとなくはわかってもらえるかなと。それくたいCypressの学習コストは低め。jQueryやCSSなどをかじっていてDOM要素を指定する知識があれば、Webアプリのベテランでなくてもすぐに書けるかなと思っています。

しかしながら、DOM要素を指定するためにセレクタを書いていくと、段々と読みにくいコードになっていきます。

例えば、動的に追加された要素やインポートした外部ライブラリのDOM要素を指定する際に、nth> などで掘っていって指定する複雑なセレクタが書かれがちになります。そうすると後で見返したときに、どこをどう直せばテストが通るようになるのか判断するために画面のDOMと見比べて追っていく必要が出てきます。

後で見返したときにわかりやすいように、テストコードの可読性をあげていきたい。

もちろん、コメントで 「ToDoを追加する」「ToDoを削除する」と書いて 分割してまとめておくのも見やすくする一つの案なのですが、Cypressの場合、Custom Commandsを使うと、分割して見やすくしたコードをカプセル化し、より読みやすいコードに仕立て上げることができます。

Custom Commands

Custom Commands、名前の通り自分でコマンドをつくれるという機能です。
https://docs.cypress.io/api/cypress-api/custom-commands

自分の欲しいコマンドを cy.containsやcy.getといったCypressに用意されているコマンドと同じように作ることができます。

Cypress.Commands.add(name, callbackFn)

このCustom Commandsで上記のTodoアプリのテストを整理してみましょう。

it('add 3 todo and delete middle todo', () => {
// todo1を追加
cy.addTodo('todo1');
// todo2を追加
cy.addTodo('todo2');
// todo3を追加
cy.addTodo('todo3');

// 2つ目を削除
cy.deleteTodo(1);

// 残アイテムの確認
cy.get('.todo-item').contains('todo1');
cy.get('.todo-item').contains('todo2').should('not.exist');
cy.get('.todo-item').contains('todo3');
})

「ToDoを追加する」「ToDoを削除する」とコメントした箇所をまとめてカスタムコマンドにしました。
後から見返しやすいコードになりましたね。

cy.addTodoとcy.deleteTodoの実態は以下。

// ./cypress/support/commands.js

// TODOの追加 cy.addTodo('todo1');
Cypress.Commands.add('addTodo', (value) => {
cy.get('#new-todo')
.type(value).should('have.value', value)
.type('{enter}', { delay: 100 });
cy.get('#new-todo').should('have.value', '')
cy.get('.todo-item').contains(value);
});

// TODOの削除 cy.deleteTodo(0);
Cypress.Commands.add('deleteTodo', (nth) => {
cy.get(`.todo-item:nth(${nth})`).contains('DEL').click();
});

addTodoは共通部分をまとめたのですっきり書けるようになったパターン、deleteTodoは todo-item:nth という画面のDOMを追っていかないと何してるか理解しにくい部分にCustom Commandsとして名前をつけてあげることで後から読み返しやすくなるというパターンです。

ひとつCustom Commandsをつくるといろんな場所で似たような操作を簡単に書けるようになります。

お気づきの通り、このCustom CommandsはReactやVueといったコンポーネント指向のライブラリと相性抜群。

書くのも簡単になり、読むときも理解しやすい。同じ処理が別のspecファイルにあるというときもコピペせずに済む。そして、コンポーネントに変更があった場合もCustom Commandsだけ修正すればOK、という場面が増えます。

デメリット

もちろんデメリットもあります。

テストケースの書き方が不味く「ToDoを追加する」という段階でinputがdisabledでTodoが追加できなかった、といったエラーが出た場合、specファイルの中ではなく、カスタムコマンドを定義しているファイルの該当部分を出します。そのため、どのaddTodoでエラーが起きたのかわかりづらい、呼び出し元がわかりにくいということがときたまあります。

そういった場合は引数をユニークなものにしておくと一旦の解決策になります。おなじTodoを3つ作成するのではなく、「todo1」・「todo2」・「todo3」としておくと、どのtodoを作成するタイミングでエラーが起きたのかが把握しやすくなります。

また、Custom Commandsの数が増えてきた際に名前の衝突が起きる可能性が高まります。こちらはある程度の命名規則があれば回避できるかなと思っています。以前登録したCustom Commandsが見つけやすいように cypress\support\commands.js を分割するのも手です。

まとめ

E2Eテストは壊れやすい上に読みにくくなりがちなので、CypressのCustom Commandsを上手く利用して、書きやすく読みやすく直しやすいテストコードにしていきましょう。

とはいえ、Cypressをまだ触ったことないよという方は、ここまで考えずまずは使ってみてください。containsとclickだけ覚えればそこそこのテストが書けます。

Cypressをしばらく使ってテストコード見返すのが辛くなり始めたら、Custom Commandsで一連の流れを固めてカプセル化するなどして、後から見返しやすいコードにできないか検討してみてください。

続いて、 Cypress - 書きやすいテストの秘密と独自コマンドの実装 記事を参照ください。