visible true

技術的なメモを書く

開発時の動作確認ツールとしてCypressのE2Eテストを導入した話

ユビーAI問診は、Ubieが提供する医療機関向けのプロダクトです。患者さんに対して問診を実施し、医師向けのカルテを作成します。現在は大きく分けて、タブレットスマートフォンの2つの利用方法があります。

f:id:sys1yagi:20220109133721p:plain
タブレット用、スマートフォン用の画面

これらはどちらもWebアプリケーションとして実装していて、フロントエンドはReact/TypeScriptで書いています。

問診のプロセスは画面遷移が多い

ユビーAI問診は紙の問診票で書くような定型的な質問だけでなく、来院した目的に合わせて様々な質問を行います

例えば「頭が痛い」といった症状を入力した場合、発症時期や部位、痛みの程度、持続時間、経過、頻度などを掘り下げて、更にそれらの回答内容から疑われる疾患に関連する質問を重ねていきます。あるいは「足をひねった」など外傷に関する場合は、スポーツをしていたかや事故かといった状況を聴取したりします。問診の長さは入力内容によって様々ですが、短くて10数回、長いと4、50回ほど画面遷移を行います

質問の種類は10数種程度なので、質問の表示の動作確認はStorybookなどを用いれば十分行えます。 しかし問診の回答結果に基づいて作成するカルテは、膨大なパターンの質問と回答の組み合わせがあるため、動作確認にはかなりの労力を必要とします。

f:id:sys1yagi:20220109150227j:plain
最初の数質問の分岐。今はもっと複雑に

CypressによるE2Eテストを開発時の動作確認用に導入する

Ubieでは基本的にひとつのフィーチャーをひとりのエンジニアが担当します*1。バックエンドとフロントエンドの両方を設計・実装したあと、全体の動作確認を行うわけですが、一回の問診は数分かかるのでトライアンドエラーが発生すると非常に時間がかかります。

そこでE2EテストフレームワークであるCypressを、開発時の動作確認用に導入することにしました。特定の問診フローを実行するテストを追加していくことで、開発時の動作確認を気軽に行えるようにしようという目論見です。

Cypressを選んだ深い理由は特になかったのですが、

  • 導入が簡単な点
  • 拡張が容易な点(テスト中にNode.jsで任意の処理を実行できる)
  • Cypress StudioというGUIアプリケーションで、テストの実行や管理が容易な点

などが気に入っています。

Cypressを導入する

Cypressの導入は非常にかんたんです。

yarn add -D cypress # あるいは npm install cypress --save-dev

次のコマンドでCypress Studioを起動できます。

yarn run cypress open # あるいは npx cypress open

初回の起動時にテストのための各種ファイルが生成されます。

cypress
├── fixtures
│   └── example.json
├── integration # テストをここに置く
│   ├── 1-getting-started
│   └── 2-advanced-examples
├── plugins
│   └── index.js
└── support
    ├── commands.js
    └── index.js

f:id:sys1yagi:20220109210900p:plain
Cypress Studio。integration配下のテストが一覧される

デフォルトはjsなので、TypeScriptにするための設定がちょこちょこ必要になります。 https://docs.cypress.io/guides/tooling/typescript-support

とりあえず動かす

Googleで'Cypress E2E'というキーワードで検索し、https://www.cypress.ioのページを開くテストをするとします。実装は次のとおりです。

describe('CypressをGoogleで検索する', () => {
  it('Cypress E2Eで検索するとヒットする', () => {
    // Googleを開く
    cy.visit('https://google.com');

    // input要素にキーワードを入力する
    cy.get('input')
        .first()
        .clear()
        .type('Cypress E2E{enter}');

    // Cypressのページタイトルを探して、クリック
    cy.get('h3')
      .contains('JavaScript End to End Testing Framework')
      .click();
  });
});

cyという特殊なオブジェクト以外は概ねJestのような書き口です

f:id:sys1yagi:20220109162555g:plain
上記のコードが動作する様子

開発環境でCypressを利用する

基本的には最初にvisitするページをlocalhostにすれば自分の環境で立ち上げたアプリケーションにアクセスするテストが書けます。

f:id:sys1yagi:20220109164059g:plain
開発環境でのログインのテストの様子

画面の操作をする他にアサーションも書けます。

// 画面上に'診察券がある'という文言のボタンが存在することを要求する
cy.get('button').contains('診察券がある').should('exist');

要素の状態のアサーション以外にも、Cookieの値やURL、API Callの内容の検証などについても行えます。

Introduction to Cypress | Cypress Documentation

プラグインを使ってテスト実行前にDB設定を整える

localhostに向けてテストを書くだけでは不十分です。医療機関にはいろいろな種類や設定があり、それぞれ動作が異なります。これらの設定を行うにはデータベースをセットアップしなければなりません

CypressのテストコードはChromeFirefoxなどの環境で動作するので、そこからデータベースを直接操作するといったことは基本的にはできません。バックエンド側に開発用のAPIを生やすことも考えられますが、そうすると複数の環境にテストのためのコードが散逸してしまうためできれば避けたいところです。

Cypressではプラグインを追加できます。プラグインはNode.jsで動作します。 デフォルトでtaskというプラグインがあり、ここでNode.jsで動作する任意の処理を追加できます。 https://docs.cypress.io/api/commands/task

plugins/index.tsでtaskイベントに対する処理を記述することで、

(module).exports = (on) => {
  on('task', {
    hello(message: string): string {
      // Node.jsで動作する
      const value = `hello ${message}!`;
      console.log(value);
      return value;
    }
  });
}

テストコードから呼び出せるようになります。

// ブラウザではなく、コマンドラインのほうに'hello world!'とログが出る
cy.task('hello', 'world'); 

ここでNode.js向けのデータベースクライアントを導入し、データをセットアップするtaskを追加することで、テスト実行時に必要な環境を整えられるようになります。

ユビーAI問診では各テーブル毎にCRUD操作をする関数を生やして、任意のデータを用意できるようにしています。TypeOrm を使い、typeorm-model-generatorで既存のテーブルからEntityを生成したので、比較的簡単に準備用のコード群ができました*2

このスタイルの場合、データベースを直接操作し、バックエンドAPIに対して実際にAPI Callをすることになるので、CIなどでのテスト実行は困難になります。少し迷いましたが、あくまで開発時に使うということで割り切ることにしました。

導入してよかった点

思ったよりメンテしやすい

動作確認を楽にするためとはいえ、メンテナンスが難しいと結局コストとしてどうなんだっけ?ということになりますが、Cypressのテストコードはかなりメンテナンスがしやすい印象です。Best Practices | Cypress Documentationを参考に再利用可能な関数を整頓していくと様々なバリエーションのテストを素早く増やしていけます。TypeScriptが使える点もありがたいです

experimentalですがCypress Studio上で操作を記録し、テストコードを生成する機能もあります。 Cypress Studio | Cypress Documentation この辺りは要素のセレクタなどを適切に準備する必要があるので気軽には使えないですが、テスタブルなコードを書く動機にもなって良いなと思います。

大胆な変更も安心できる

一連のシナリオを実行するテストを書けば、何度でも使えるので、そのシナリオ中に関連するコードを変更する際にリグレッションテストとして機能します。 問診のフローはかなり複雑に関連しあっているので、これまでは変更にかなり慎重に取り組まなければなりませんでしたが、テストが増えていくにつれて大胆な変更が可能になりました。

ドキュメントとしての価値もあった

実はCypressを導入したあとに気づいたのですが、各種テストをきちんと構造化するとドキュメントとしての価値もでてきました。現在カバーできている範囲は全体の数%にも満たないですが、それでも新たな実装に対してCypressのテストを追加することで、他の人に引き継いだり、時間が経過した後のキャッチアップなどが容易に行えます。 データのセットアップもコードで表現しているので、前提条件なども把握できるようになっており、CIでの実行を犠牲にした価値は十分あったかなと思います。

心残りな点

とはいえやはり、CIなどの三者による定期的な実行は諦めきれない要素です。どうしても人間が任意のタイミングで実行するだけでは漏れが生じるからです。この間も圧倒的に壊れていました。この辺りは別途対策を考えています。各リポジトリのdevelopブランチをデプロイする環境を持っておいて、変更が入るとデプロイ後にCypress Testをキックするなど、自前で用意することになりますが実現自体は可能なのではないかと思っています。

まとめ

E2Eテストというと結構重たいというか、大変そうなイメージがありましたが、開発時の動作確認ツールとして割り切ることで、かなり便利に使えることがわかりました。

ユビーAI問診は開発が始まってから時間が経っていて、ドメインの深さやコードの複雑さが新メンバーの負担になったり、あるいは古い人が離れられないといった問題がありました。CypressのE2Eテストがすべてを解決するわけではありませんが、今後もこうした取り組みを重ねて、壊れにくく、キャッチアップしやすく、手離れしやすい環境を作っていきたいと思っています。

そんなUbieでは新たなソフトウェアエンジニアを募集しています。最近はフロントエンド、バックエンドに特化したポジションが増えたりしていますので、昔見たな〜という方もぜひまた見てみてください。 recruit.ubie.life

*1:もちろん規模が大きい場合手分けする場合もあります

*2:prismaも検討しましたが、複数のDB接続を簡単にはできなそうだったので諦めました。https://github.com/prisma/prisma/issues/2443