SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

テストコードのあるきかた 〜歩きはじめ方〜

こんにちは!スマートキャンプ21卒エンジニアの関口です。私は9月にBALES CLOUDというSaaSを開発するチームに移動しました。

突然ですが皆さんはテストを書いていますか? 私は今まであまり真摯にテストを書いてきませんでした。しかし直近で開発チームを移動した際にテストについて学ぶ機会があり、心機一転しテストと向き合うようになりました。 今回の記事では私がテストに向き合う中で学んだことをまとめていきます。

テストを書くことを意識した経緯

前述したとおり、今まで私は真摯にテストと向き合ってきませんでした。なぜなら、テストを書くよりも開発に工数を充てる方が効率的なのではないかという疑念があったからです。また、個人的にテストには苦手意識がありました。それは、テストコードの作成に時間を割かれ、タスクの工数見積もりを超過してしまうのではないかという不安からです。しかし、移動先の開発チームは、新しい機能を追加する際には、基本的にテストを書く方針で動いていました。 私はこれは良い機会だと思い、「テストよりも機能開発を優先するべきではないか」「テストに苦手意識があり見積もりの工数をオーバーしてしまうかもしれない」という個人的な思いをチームの先輩に相談してみました。

私の相談に対して、先輩は以下の資料を共有しながらテストを書くことの重要性について教えてくれました。

質とスピード

共有してもらった資料に書かれていた事項のうち、特に印象深かった内容は以下です。

  • 品質とスピードはトレードオフではなく、品質が高まることによってプロダクトの開発スピードが上がること
  • スピードとトレードオフなものは、メンバーの学習時間や成長機会であること

また、併せて先輩からは以下のことを教えていただきました。

  • テストにかかる時間も考慮してタスクの見積もりをおこなっていること
  • 自分とプロダクトの成長のためにも最初に時間はかかってもテストは書いてほしいということ

和田さんのスライドを見ながらテストの目的、重要性について説明していただき、テストを書くことに時間がかかる不安をチーム内で解消できたため、テストを書くことに対して前向きになれました。

テストの必要性

まずテストの必要性について考えます。

テストの必要性について考える際に以下の記事が参考になりました。

testing-vs-checking

この記事の中で以下の記述がありました。

  • 確認とは、既存の信念を確認したいという動機で行なうものです。
  • テストというのは、新しい情報を見つけたいという動機で行なうものです。
  • テストとは、探求、発見、調査、そして学習のプロセスです。
  • テストは製品を評価する目的で、あるいは想定していなかった問題を認識する目的で、製品を構成し、操作し、観察することです。

これらの記述からテストは自分たちがすでに想定している問題を確認するためだけに行なう作業ではなく、自分たちが想定していなかった問題を発見するために必要な作業という認識に変わりました。

自動テストについて

ソフトウェア開発におけるテストの実施手法には大きく手動テストと自動テストがあります。手動テストとは人間が自らの手でシステムを動作させながら、画面の出力やデータベースの値の変更などの確認を行なうテスト手法のことです。もう一方の自動テストとは手動テストで行なっていた作業をシステムで自動化したテスト手法のことを指します。本記事では特に「自動テスト」について考えていきます。

*今回の記事のこの章以降で表す「テスト」は「自動テスト」のことを意味します。

自動テストの種類

自動テストを書き始めるにあたってその種類について調べました。

自動テストには単体テスト、統合テスト、システムテストが存在します。

  • 単体テスト

    • 原子的な単位での機能の振る舞いを確認するテスト
    • テスト対象の単位の範囲はチームによって異なるが、基本的にはクラス、メソッドを指すことが多い
    • テスト対象の仕様を理解できるため、ドキュメントとしての働きももつ
  • 統合テスト

    • 複数のモジュールを組み合わせ、データの受け渡しなどのモジュール間の連携を確認するテスト
  • システムテスト

    • すべてのモジュールの動作が仕様どおり機能しているかを確認するためのテスト
    • 開発者が行なうテストの最終段階に当たるため、本番と同などの環境を用いてテストを行なうことが望ましい

自動テストのメリット・デメリット

メリット

続いて自動テストのメリットについて考えていきます。テストを自動化することによって受けられる恩恵はたくさんありますが、中でも自分がメリットに感じることは以下の2つです。

  • 手動テストに比べて作業時間が掛からない
  • リファクタリングがしやすくなる

手動テストに比べて作業時間が掛からない

2つのテストの作業時間を比較する場合、以下の計算式が考えられます。

自動テストにかかる時間 = 自動テストの実装時間 + 仕様変更などに伴いコード修正する作業時間(管理コスト)
手動テストにかかる時間 = 手動テストの1回あたりの作業時間 × 実行回数

実行回数が多くなればなるほど、手動テストにかかる時間が増えますので、 確認する頻度が高い機能ほどテストを自動化するメリットがあると言えます。

リファクタリングがしやすくなる

自動テストがあることによって機能のリファクタリングがしやすくなります。プロダクトコードを変更した際にテストがあることで、コードが正常に機能しているかどうかを即座に確かめることができます。 また手動テストでは確認が漏れてしまうような、リファクタリングによる他機能への影響を自動テストがあることで確認できます。さらにテストコードを通じてリファクタリング対象のメソッドやクラスを使用することで、その対象が利用しやすくなっているかを確認できます。

デメリット

メリットがたくさんあるように見える自動テストですが、デメリットもあります。 私がデメリットに感じることは以下のことです。

  • プロダクトコードの仕様変更に伴い、テストの修正、追加など一定の管理コストがかかる

プロダクトコードがずっと同じ仕様ということは稀です。 プロダクトコードが変更した際にはテストも修正する必要があり、その修正のための作業時間は生まれます。プロダクトコードの変更可能性が高い箇所のテストには相応の管理コストがかかります。 管理コストをかけてでも自動化するべき箇所を見極めてテストを自動化することが大切なのではないかと考えます。

実際にテスト書いてみる

理論の説明は以上にして、実際の開発タスクのテストを書く中で学んだことをサンプルコードを用いて紹介していきます。Railsのモデルのユニットテストを事例にします。お題の機能は、「リード」というオブジェクトに1人の担当者を追加or修正する機能です。リードとは、インサイドセールスにおいて、架電やメール送信などを行なう対象を表すオブジェクトです。

テーブル定義・プロダクトコード

モデル間の関係性は以下の画像のようになります。

1つのリードには1人の担当者が紐づく仕様です。 リードに担当者を追加するメソッドを以下のようにモデルに定義します。

class LeadUser

  def self.assign_user!(user_id, lead_id)
    user = User.find(user_id)
    lead = Lead.find(lead_id)

    lead_user = LeadUser.find_by(lead_id: lead.id)
    lead_user&.destroy

    lead_user = lead.build_lead_user(user_id: user.id)
    lead_user.save!
  end

end

テスト

テストケースを作成する際は実際の処理から、何を保証するべきなのかを考え、それをテストに落とし込むことが大切です。

メソッドの処理の流れは以下になります。

  • 担当者を追加するリードと担当者のデータを検索

  • すでに登録されている担当者を削除

    • 担当者が登録されていない可能性もあり
  • 登録したい担当者をリードに紐づける

この処理の結果から保証するべきものを列挙します。

このメソッドはリードに紐づく担当者の作成を担保します。1つのリードに紐づく担当者は必ず1人になります。

  • 指定したリードに紐づく担当者が1人存在すること

  • 指定した担当者が指定したリードに紐付いていること

上記の条件を保証するテストを作成します。 また今回はリードに担当者がすでに登録されている場合と登録されていない場合があるので、保証すべきものは同じですが、その条件を分岐させます。 テストは以下のようになります。

RSpec.describe LeadUser, type: :model do
  describe '#assign_user' do
    
    let(:user) do
      create(:user)
    end

    let(:lead) do
      create(:lead)
    end

    subject do
      LeadUser.assign_user!(
        user.id,
        lead.id
      )
    end

    context '既に担当者が存在する場合' do
      let(:current_user_id) do
        create(:user).id
      end

      let(:lead_user) do
        create(:lead_user, user_id: current_user_id, lead_id: lead.id)
      end

      it 'リードに紐づく担当者の数が1件' do
        subject
        expect(LeadUser.where(lead_id: lead.id, user_id: user.id).length).to eq(1)
      end

      it 'リクエストで送られたユーザーが担当者として登録される' do
        subject
        expect(lead.user).to eq(user)
      end
    end

    context '担当者が存在しない場合' do
      it 'リードに紐づく担当者の数が1件' do
        subject
        expect(LeadUser.where(lead_id: lead.id, user_id: user.id).length).to eq(1)
      end

      it 'リクエストで送られたユーザーが担当者として登録される' do
        subject
        expect(lead.user).to eq(user)
      end
    end
  end
end

テストコードの説明をします。 前述した”保証したいこと”はそれぞれ以下のようにテストコード中で保証しています。

  • 指定したリードに紐づく担当者が1人存在すること

    • 指定したリードと担当者のレコードがLeadUserテーブルに1つだけ存在することを確認することで保証されます。
  • 指定した担当者が指定したリードに紐付いていること

    • 指定したリードに紐付けられた担当者が作成された担当者と同じことを確認することで保証されます。

テストを書き始めるにあたって一番苦戦したことはメソッドの処理から保証するべきものを列挙する作業でした。 どんな条件を満たせばメソッドの処理が保証されるのか、漏れや被りが無い条件を探すことが難しいと感じましたが、パズルを解いているような感覚で難しさの中に楽しさもありました。

まとめ

今までテストは書いたほうが良いものと思いつつ書く習慣をつけることができませんでした。しかし、開発チームを移動したことがきっかけでテストを書くべき意味、目的、楽しさを学び、テストを書く習慣をつけることができました。 これからの開発では素早く、抜け漏れが無いテストをかけるように精進していきます。