エクサウィザーズ Engineer Blog

株式会社エクサウィザーズのエンジニアチームブログ

エクサウィザーズのTLが実践する、開発が遅くならないテストの書き方

この記事について

この記事ではエクサウィザーズの介護記録AIアプリ「CareWiz ハナスト」(以下ハナスト)の開発スピードを維持するために、どのようにテストを書いているかをご紹介します。

内容としては基本的なことかと思うので、ハナスト開発ではどのような基本に則ってテストしているかという感じで読んでいただければ良いかと思います。

書いているのは誰?

この記事はハナスト開発チームのテックリードをしている原(@haracane)が書いています。

ハナストチームでは主にNode.js&TypeScriptでバックエンドAPIを開発していてテストにはJestを使っています。

ちなみにこれまではKotlin&JUnitやRuby on Rails&Rspecなどで開発&テストをしたりしてました。

ハナストについて

ハナストは簡単に言うと「音声入力で介護の記録をするアプリ」です。

以下の動画を見ていただくと、大体どんなアプリかわかるかと思います。

vimeo.com

ハナストの開発プロセス

やや脱線しますが、ハナストの開発プロセスについてはこちらのnote記事によくまとまっています。

note.exawizards.com

とても良いことが書いてあるので是非読んでいただきたいのですが、この記事に関係するところで言うと、ハナストの開発はあくまで「仮説検証のためにやっている」というところがポイントです。

効果的に仮説検証を進めるには開発スピードを維持することが重要になります。

そのためにどのような工夫をしているか、ということをこの記事にはまとめています。

ハナストの構成

続いてハナストの実装について紹介すると、ハナストは大きく分けて

  • バックエンド API
  • 音声認識 AI
  • iOS アプリ

の3つで構成しています。

今回は主にハナストAPIでどのようにテストしているかをご紹介します。

ハナストAPIのテスト

ハナストAPIはGraphQL APIとして提供していて、Clean Architectureで設計しています。

テストは主にGraphQLのリクエスト&レスポンスのテストを書いています。

例えば記録を作成するGraphQL APIのテストだとこんな感じです。

describe('createCard', () => {
  describe('with food input', () => {
    let response

    beforeEach(async () => {
      response = await graphQLRequest(`
        mutation createCard {
          createCard(type: "food", amount: 10) {
            id
            type
            amount
          }
        }
      `)
    })


    it('creates food record & renders food record', async () => {
      const cardId = response.data.createCard.id
      const card = await new CardRepository().findById(cardId)
      expect(card).toMatchObject({
        type: 'food',
        amount: 10,
      })

      expect(response).toEqual({
        data: {
          createCard: {
            id: cardId,
            type: 'food',
            amount: 10,
          }
        }
      })
    })
  })
})

逆にユニットテストは極力書かないようにしています。

上記のような記録作成の例だと、Layer毎に

  • GraphQL Layer
    • MutationResolver#createCard
  • UseCase Layer
    • CardService#create
  • Database Layer
    • CardRepository#create

のようなクラス&メソッドを実装していますが、各メソッドのユニットテストは書いていません。

その理由についてご説明する前に、ハナスト APIのテストの役割についてご説明します。

ハナスト開発におけるテストの役割

ハナスト開発でのテストの役割は

  • API仕様と異なる実装を検出すること
  • 開発スピードを落とさないこと

としています。

この二つに優先順位はなく、品質担保と同様に開発スピードの維持も重要な役割としています。

これはハナスト開発のフェーズが仮説検証を繰り返している段階で、素早く機能を開発する必要があるからです。

必要のない機能は作らない

テストを書く、書かない以前に開発スピードを落とさないためには必要のない機能を作らないということが大事です。

基本的な方針として、ハナスト開発ではAPIの仕様を決める時に

  • 必要のない入力を受け付けない
  • 必要のない出力をしない

ようにしています。

API仕様に必要のない入出力があると、その仕様のテストも必要になりますし、後々変更をする際にもその仕様を守るために余計な工数がかかってきて開発スピードが落ちてしまいます。

また、必要のないテストをしないことも同様に重要です。

同じようなテストがいくつもあると、そのテスト作成の工数だけでなく、テスト変更時の工数も増えてしまいます。

ハナストAPIのリクエストテストを書く理由と、ユニットテストを書かない理由

現在のハナストのように開発が活発な状態だと外部APIおよび内部APIの仕様変更も頻繁に発生します。

リクエストテストは外部APIの実装が壊れないようにするためのもので、これは必要です。

リクエストテストがないと、外部APIが仕様通りに動いていない場合に気付くことができません。

一方ユニットテストについては内部APIの実装が壊れないようにするためのものになります。

ハナストの開発ではリファクタリングによって内部APIの仕様を変えるというケースは頻繁に発生します。

ユニットテストが書いてある場合は、内部APIの仕様を変えた時には関係するユニットテストも合わせて修正する必要があります。

リクエストテストは問題なく通っていて、API全体の動作としては問題ない場合でも、ユニットテストが壊れている場合は直さなくてはいけません。

これは「開発スピードを落とさない」という観点からは許容できません。

そのため、ハナストでは基本的にリクエストテストを書いて、原則としてユニットテストを書かないようにしています。

ユニットテストを書く場合

「原則」ユニットテストは書いていませんが、場合によっては書く場合があります。

一つはユニットテストレベルでTDD(テスト駆動開発)をした方が実装しやすい場合です。

よくあるのは値を変換する汎用ロジックのテストです。

例えばUTCの時刻からJSTの日付を取得するようなメソッドなどが該当します。

このようなメソッドはユニットテストを書いてから実装コードを書くTDDスタイルの方が実装しやすいので、ユニットテストを用意しています。

もう一つは他のテストでスタブしているメソッドのテストです。

リクエストテストでメソッドをスタブしている場合、スタブしているメソッドのユニットテストがないと、そのメソッドが全くテストされません。

ハナストの開発ではテストされないロジックは許容していないので、そのような場合はユニットテストを用意しています。

スタブを使う場合と使わない場合

スタブについても「開発スピードを落とさない」ことを考えて使うかどうかを決めています。

前述したように、スタブを用意した場合はユニットテストが必要になるのでスタブも極力使わないようにしています。

これはS3やSQSなどについても同様で、minioやelasticmqなどのクローンを使ってなるべくスタブはしていません。

ただし、

  • クローンが用意されていない外部リソースを利用する場合
  • 外部リソースのエラー時の挙動をテストする場合
  • 外部リソースの状態変化をテストする場合

といったケースでは外部リソースのスタブを許容しています。

この場合、スタブ対象が十分にテストされていることが望ましいので、なるべくライブラリのクライアントなどを直接スタブするようにしています。

Factoryを積極的に使う

テスト用のデータ(例えばUserデータなど)についてもやはりモックは使いません。

これは、安易にモックしてしまうと矛盾のあるデータが作られてしまいやすく、テストが不安定になるからです。

その代わりにハナストの開発ではテスト用のデータ作成用のFactoryクラスを用意しています。

例えばUserFactoryなら

new UserFactory().create()

という感じで呼び出すと、デフォルトのパラメタでUserデータを作成します。

テストのためにパラメタの指定が必要な場合は

new UserFactory().create({ familyName: 'ハナスト', givenName: '太郎' })

のようにパラメタを指定します。

Factory側では依存するレコードの作成なども含めて、矛盾のないデータを作成するようにしています。

CIは並列実行する

  • 基本的に(ユニットテストではなく)リクエストテストを書く
  • スタブはしない

という方針でテストを書いていると、テストの実行時間は長くなりがちです。

その結果CIの待ち時間が長くなってしまうと、やはり開発スピードが落ちてしまうので、CIに時間がかかるようになってきたら適宜並列化して、全体で5分程度で終わるようにしています。

並列化の方法はシンプルで、例えばテスト用のディレクトリ構成が

  • spec
    • entity
    • usecase
    • request

のような構成になっていたら、entity・usecase・requestディレクトリのテストをそれぞれ並列に実行します。

ハナストのCIはGithub Actionを使っているので、上記のような例ですと3並列のワークフローを定義してCIを実行すればOKです。

まとめ

今回はハナストの開発スピードを落とさないための基本的なテスト戦略についてご紹介しました。

他にもハナストでは音声認識技術や音声入力のインタフェースなどをチームメンバーが各自の役割を発揮して開発していますが、介護の世界でやるべきことはまだまだたくさんあります。

そのような社会的な課題の解決をより進めるために、ハナストチームおよびエクサウィザーズでは一緒に働く人を募集しています。

介護をより良くするハナストの開発、あるいはAIで社会課題を解決するエクサウィザーズに少しでも興味がありましたら、是非ご応募ください!

hrmos.co