LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

Cypressを使ったフロントエンドのテスト駆動開発:LINEドクターの事例

はじめに

こんにちは。プロダクト開発本部のYoungjin Jangです。

皆さんはフロントエンド開発をするときに、テストコードを書いていますか? フロントエンドは分野の特性上、開発スケジュールの後半に成果物(スペック、デザイン、API)を総合して最終的にプロダクトを作る立場になるため、時間に追われてテストコードを書きにくいこともあるかと思います。逆にスケジュールの前半では他の職種より比較的時間が余ることもあるでしょう。この時間を活用してテストを書いてみるのはいかがでしょうか?

この記事では、テスト駆動開発の基本的な概念とCypressを利用したテスト駆動開発方法、そして実際にLINEドクターのフロントエンドを開発するときに、どのようにテスト駆動開発を行っているかを紹介します。

LINEドクターとは

LINEドクターはオンライン診療サービスで、ユーザーはLINEを通じて診察を受け、処方箋や薬を自宅で受け取れるサービスです。LINEドクターは信頼性が非常に重要であるため、フロントエンド開発をするときにはテスト駆動開発を採用し、品質を保証しています。

テスト駆動開発の概要

テスト駆動開発は開発の初期段階で先にテストを書き、そのテストケースが通過するコードを書くプロセスです。

失敗するテストコードを先に書いた後、機能を実装してテストを通過させ、コードをリファクタリングします。このサイクルを繰り返すことにより、質の高いコードベースを構築します。

  1. テストコードの作成:機能に対するテストケースを先に書く
  2. 実装:テストが通るまで機能を実装
  3. リファクタリング:実装したコードを整理

Cypressによるテスト駆動開発の実践

Cypressは実際のブラウザでテスト実行が可能なフロントエンドテストツールです。Cypressは簡単にインストールでき、テストコードの作成が容易で、リアルタイムでテスト結果を確認できます。また、Cypressのテストコードは実際のブラウザで動作するため、デバッグが容易です。これらのCypressの特長がフロントエンドのテスト駆動開発を可能にします。

ここでは、Cypressをインストールし、テスト駆動開発をどのように行うかを確認します。まず、npmを利用してCypressをインストールし、Cypressを起動します。

npm install cypress --save-dev
npx cypress open

Cypressを起動すると、下記のようにCypressプログラムが実行されます。Cypressを利用したテスト駆動開発はE2Eテストを基準とします。”E2E Testing”をクリックし、Google Chromeでテストを開始します。

最初に失敗するテストコードを書きます。たとえば、タスク管理サービスで、ユーザーがタスクリストページにアクセスしたときに、タスクリストが表示されるべきだという仕様を実装すると仮定します。まず、cypress/e2e/todo.cy.js ファイルを作成し、下記のようにテストコードを作成します。

// cypress/e2e/todo.cy.js

describe('タスク管理', () => {
  it('タスクリストが表示される', () => {
    // ページにアクセスする
    cy.visit('http://localhost:3000/');

    // ページの中で、data-testidのattributeに"todo-list"が指定されているelementが存在するか確認
    cy.get('[data-testid="todo-list"]').should('exist'); // まだ実装前なので、このテストは失敗します。
  });
});

このテストコードは、ユーザーがページにアクセスしたときに、「タスクリスト」が画面に表示されることを確認するものです。

テストを実行すると、以下のようにテストの失敗を確認できます。

テストを通過させるために、タスクリストを表示するUIを実装します。

<!-- index.html -->

<div data-testid="todo-list">
  <!-- タスクリストが表示される場所 -->
</div>

実装後、Cypressを確認すると、下記のようにテストが通過したことを確認できます。

次はリファクタリングの段階ですが、リファクタリングが必要なほどのコードがないので、ここでは省略します。

タスク管理サービスをさらに開発していく場合には、タスクを追加したり、タスクを完了させるなどの新たな要件が必要になるかと思います。その際も、下記のようにまずテストコードを追加し、その後で実装を進める方法でサイクルを繰り返し、テスト駆動開発を進められます。

// cypress/integration/todo_spec.js

describe('タスク管理', () => {

  // ... 以前のテストコード

  it('タスクを追加する', () => {
    // ページにアクセス
    cy.visit('http://localhost:3000/');

    // タスクを入力した後、エンターキーを押す
    cy.get('[data-testid="new-todo"]').type('新しいタスク{enter}'); 

    // タスクリストに新しいタスクが追加されたことを確認
    cy.get('[data-testid="todo-list"]').should('contain', '新しいタスク'); 
  });
});

LINEドクターでの事例

LINEドクターには、オンライン診療後にユーザーが希望する薬局で薬を受け取る機能があります。選択した薬局がユーザーの住所から15km以上離れている場合、実際に薬を受け取りに行くには距離的に無理があるため、本当にその薬局で薬を受け取るかを確認するポップアップを表示する機能があります。この機能について、実際のテスト駆動開発をどのように進めたかを紹介します。

1. テストコード作成

まず、テストコードを作成するために必要な情報を決めていきます。ユーザーが選択した薬局がユーザーの住所から15km以上離れているかどうかの確認は、フロントエンドではできないため、バックエンド開発者とAPIについて議論を行った後、下記のようにAPIインターフェースを決定しました。このAPIを使用して、フロントエンドはユーザーの住所と選択した薬局のIDをバックエンドに送信し、15km以上離れているかどうかを確認できます。

// このAPIは実際に使われているAPIではなく、加工したAPIです。

// request
GET /v1/is-pharmacy-nearby
{
  "userAddress": "ユーザーの住所"
  "pharmacyId": "薬を受け取る薬局のID"
}

// response
true || false

そして、仕様をテストコードに置き換えて作成します。テストコードを書くときは、まず機能の基本となるケースを書き、そのケースから派生するケースを追加していきます。フロントエンドでコントロールできない部分、たとえばAPIの背後で起こる動作は、APIにわたすパラメータや適切なタイミングでAPIをリクエストしているかに焦点を当てて、テストコードを書きます。テストコードを書く際は、ステークホルダーと議論した内容がすべてテストコードに反映されることを目指しています。テストコードはonly機能を利用して、すべてのテストケースを実行しないようにし、テスト実行速度による生産性の低下を避けています。

describe.only('選択した薬局とユーザーの住所の距離確認', () => {
  // 基本になるテストケース
  it('薬局がユーザーの住所から15km以上の場合、ポップアップを表示する', () => {
    // バックエンドエンジニアと議論したAPI Interfaceをモックする
    // 薬局がユーザーの住所から15km以上の場合なので、APIはfalseを返す
    cy.intercept('GET', `/v1/is-pharmacy-nearby**`, 'false').as(
      'fetchIsPharmacyNearby'
    );

    // ポップアップを表示するページにアクセスする
    cy.visit(`/treatment/review`);

    // 予約を完了させる
    cy.get('[data-testid=confirm_reservation]').click();

    // ポップアップが表示されることを確認する
    cy.get('[data-testid="common_modal"]').should('exist');
    cy.get('[data-testid="common_modal_description"]').should(
      'contain',
      'お薬を受け取る薬局の住所がご自宅住所から離れています。このまま予約を完了しますか?'
    );

    // APIを呼ぶときに正しいパラメータをわたしたか確認する
    cy.wait('@fetchIsPharmacyNearby').then(({ request }) => {
      const searchParams = new URL(request.url).searchParams;
      expect(searchParams.get('userAddress')).to.equal('xxx');
      expect(searchParams.get('pharmacyId')).to.equal(`999`);
    });
  });

  // 派生したケース
  it('薬局がユーザーの住所から15km未満の場合、ポップアップを表示しない', () => {
    /// 薬局がユーザーの住所から15km未満の場合なので、APIはtrueを返す
    cy.intercept('GET', `/v1/is-pharmacy-nearby**`, 'true');

    // ...省略

    // ポップアップが表示されないことを確認する
    cy.get('[data-testid="common_modal"]').should('not.exist');
  });
});

基本となるケースでは、まずcy.interceptを使用して、APIの結果が常にユーザーの住所が薬局から15km以上であると判断されるようにします。そして、cy.getを使用して予約完了ボタンを探し、cy.clickを使用して予約完了を行います。このとき、期待される結果である確認ポップアップが画面に表示されたかどうかをcy.shouldで検証します。

派生したケースでは、APIの結果が常に15km未満になるように設定し、予約完了を押したときに、確認ポップアップが表示されないかを検証します。

テストコードを追加してCypressを実行すると、ポップアップを表示する機能がまだ実装されていないため、ポップアップが表示されなかったという理由でテストが失敗することを確認できます。

2. 実装

では、テストを通過させるために、実際の実装コードを作成します。ユーザーが予約ボタンを押したときに、ユーザーが選択した薬局がユーザーの住所から15km以上離れているかどうかをAPIを通じて確認します。APIの結果がfalseの場合、ポップアップを表示します。

// LINEドクターのフロントエンドはVueを利用しているので、サンプルコードはVueのコードになります。


<template>
  <main>
    <!-- ... -->

    <button data-testid="confirm_reservation" @click="handleClickConfirm">
      予約する
    </button>

    <!-- ... -->
  </main>
</template>

<script>
export default {
  // ...
  methods: {
    async handleClickConfirm() {
      // ...

      const response = await this.$axios.get(
        '/v1/is-pharmacy-nearby',
        {
          params: {
            userAddress: this.userAddress,
            pharmacyId: this.pharmacyId,
          },
        }
      );
      if (!response.data) {
        await this.$modal.open({
          text: 'お薬を受け取る薬局の住所がご自宅住所から離れています。このまま予約を完了しますか?',
        });
      }

      // ...
    },
  },
};
</script>

実装コードを追加した後、Cypressテストを再度実行すると、テストが成功したことを確認できます。

3. リファクタリング

次はリファクタリングの段階です。実装時に予約完了ボタンのクリックハンドラーにコードを書きましたが、予約完了ボタンのハンドラーには複数の処理ロジックが含まれているため、可読性が多少低下しました。そのため、該当ロジックを別の関数に分離し、そのボタンのクリックハンドラーから呼び出すようにリファクタリングを行いました。

<template>
  <main>
    <!-- ... -->

    <button data-testid="confirm_reservation" @click="handleClickConfirm">
      予約する
    </button>

    <!-- ... -->
  </main>
</template>

<script>
export default {
  // ...
  methods: {
    async handleClickConfirm() {
      // ...

      if (!await this.isPharmacyNearby()) {
        await this.$modal.open({
          text: 'お薬を受け取る薬局の住所がご自宅住所から離れています。このまま予約を完了しますか?',
        });
      }

      // ...
    },
    async isPharmacyNearby() {
      const response = await this.$axios.get(
        '/v1/is-pharmacy-nearby',
        {
          params: {
            userId: this.userId,
            pharmacyId: this.pharmacyId,
          },
        }
      );

      return response.data;
    }
  },
};
</script>

このようにLINEドクターのUI開発では、まずテストコードを書き、テストコードが通過する実装コードを書き、その後リファクタリングを行う方法で進めています。

まとめ

この記事では、テスト駆動開発の基本、Cypressを利用したテスト駆動開発の実践方法、そしてLINEドクターでの具体的な開発事例を通じて、テスト駆動開発の実践的な適用方法を紹介しました。テスト駆動開発は、質の高いソフトウェア開発を支援する強力な手法です。Cypressを活用することで、フロントエンドのテスト駆動開発によりアクセスしやすくなります。

皆さんも、テスト駆動開発を実践することで、より信頼性の高いフロントエンド開発を行ってみてはいかがでしょうか?

ここまで読んでいただき、ありがとうございました。