はじめに
こんにちは!Techouseのhab_akiです。私は大学で脳科学・神経科学を専攻しており、現在大学3年生です。Techouseには実務未経験で2024年6月にインターンとして入社しました。優秀な先輩インターン生や社員さんに囲まれて置いていかれまいと焦る毎日です。
先日初めてテストを書く機会があったのですが、私は書き方を習得するまでに非常に苦労しました。そこで今回は初心者の目線で、テストを書くときに苦労した話を書こうと思います!私と同じような未経験エンジニアの方々は共感してくれるのではないかと思います。エンジニア歴が長い先輩の方々も未経験のエンジニアのオンボーディングをする時にも役に立つかもしれません。
初めてのテスト
私たちTechouseのインターンは研修の学習資料としてRuby on Rails チュートリアルを使用します。こちらにはテストについての記述があり、そこでテストに関する知識は身につけましたが、その重要性には気づけていない状態でした。数ヶ月テストを書いてきましたが、まだ私の知らないテストの大切さや便利さ、奥深さがあるのだろうと思います。
さて、まずは今回自分がテストを書いたときのタスクについてお話しします。
今回変更を加えるファイルには、応募者情報のcsvをスクレイピングし、そのデータをEntry
というモデルに突合する処理があります。
そこに、取得した csv のうち、応募日時カラムが 3 ヶ月以内であるデータにのみに対してフォーマットと突合を行い、3ヶ月以内でなければreturn
する処理を実装することが私のタスクでした。そこでまず、私が追加したコードが以下のものです。
: @csv.map { |row| application_date = Date.parse(row['応募日時']).to_date three_months_ago = Date.today.prev_month(3) return if application_date <= three_months_ago : # データをフォーマットし、突合する処理 }
row['応募日時']
にはString
型の日付情報が代入されており、これにDate.parse
をすることでDate
型に変換しています。そしてDate
型同士を比較し3ヶ月前の日付と比較しています。
私は今までテストを全く書いたことがないわけではありません。ただ、他の方が書いたコードをそのまま書き換えて乗り越えていたので自力で0からテストを書いたことはありませんでした。しかし、テストは自分で書ける自信があり、それらしいテストが書けたので他の方に質問せずプルリクを投げました。今ではテストを書く前に先輩方に書き方などを聞いておくべきだったと反省しています。
current_date = Date.parse('2024-08-22') three_months_ago = current_date.prev_month(3) entries.each do |entry| assert entry.entered_at.to_date > three_months_ago end
私が携わっているプロダクトではテストフレームワークとしてRSpecではなく、minitestを使用しています。このテストに対し、レビュワーからは以下のようなコメントをいただきました。
レビュワーからは「テスト内のロジックをテストしてしまっている」とご指摘をいただきました。テストファイル内の処理をテストしているだけで実際のファイルのテストができていないとのことです。加えて、「メソッドを切り出してそのメソッドの単体テストも追加しよう」というコメントもいただきました。
メソッドを切り出して単体テスト
さて、3ヶ月以内か否かを判定する処理をwithin_three_months?
メソッドとして切り出し、そのメソッドの単体テストを書くことになりました。この処理では3ヶ月以内であればtrue
を返し、そうでなければfalse
を返します。以下がメソッドのコードです。
def within_three_months?(entered_at) application_date = Date.parse(entered_at).to_date three_months_ago = Date.today.prev_month(3) application_date >= three_months_ago end
このメソッドはFormatter
というクラスのインスタンスメソッドとして定義してます。そして、このメソッドのテストとして追加したのが以下のものです。
test 'test_within_three_months?' do one_month_ago = Date.today.prev_month(1) three_months_minus_one_day_ago = Date.today.prev_month(3) - 1 three_months_ago = Date.today.prev_month(3) one_year_ago = Date.today.prev_month(12) assert_equal(@formatter.within_three_months?(one_month_ago), true) assert_equal(@formatter.within_three_months?(three_months_ago), false) assert_equal(@formatter.within_three_months?(four_months_ago), false) assert_equal(@formatter.within_three_months?(one_year_ago), false) end
加えて、境界値分析と同値分割法によって、テストケースを4つに整理しました。
境界値分析と同値分割法は、効率的にテストケースを設計するための基本的な手法です。
同値分割法は、入力や状態を「同じ結果をもたらす値のグループ」に分ける方法で、これらのグループを同値クラスと呼びます。テストケースを散漫に用意するのではなく、それぞれの同値クラスから代表値を選んでテストを行うことで、テストケースを最小限に保ちつつ、その網羅性を担保します。
次に、境界値分析は、異なる同値クラス間の境界にあたる値を用いてテストする手法です。同値クラスの定義から、境界値はプログラムの実行結果の変化に寄与する値であり、それ故周辺でバグが生じやすい場所とされています。
したがって、境界値をテストすることで、バグの発生を検知しやすいテストケースを作ることが出来ます。 今回のケースでは、メソッドの返り値が「3ヶ月以内」であるか「3ヶ月以上前」であるかによって変化することがわかります。よって、同値クラスを「3ヶ月以内」と「3ヶ月以上前」の2つに分類することが出来ます。
このことから、「3ヶ月以内」の代表値として「1ヶ月前」、「3ヶ月以上前」の代表値として「1年後」、2つの同値クラスの境界値として「3ヶ月前」、「3ヶ月 - 1日前」の4つのテストケースを作成することで、必要十分なテストケースを作成することが出来ました。
ちなみに、このテスト技法は先日開催された「テスト技法勉強会」で学びました。Techouseはこのテスト技法勉強会の他にも、週に一度開催される勉強会なども開催されており、インターン生の成長の環境が整っていると感じています。
こちらのテストには自信を持ってプルリクエストを投げましたが、レビュワーからは次のようなご指摘がありました。
以上のようなご指摘をいただきました。確かにその通りでした。
メソッドや関数の値を固定することをスタブするといいます。Date.today
の値は実行するタイミングによって返す日付が異なりますが、スタブすることでいつ実行しても特定の日時を返すことができます。スタブはテスト作成において非常に便利であり、これからも頻繁に使用するツールです。
また、コメントの中にハードコーディングという用語が出てきました。ハードコーディングとはテストの期待値に変数を用いず、リテラルを直接入力することを指します。今回は日付の値を2024/07/20
のようにハードコーディングしました。
ハードコーディングと対照的に、外部のリソースから取得した値のことをソフトコーディングというそうです。先ほどのDate.today.prev_month(1)
のようにprev_month
メソッドから取得して代入している場合などがソフトコーディングに当たります。
ハードコーディングによりテストしたい部分の以外の処理が入り込むことを防ぎ、予測値がブレてしまうのを防ぐことができます。このアドバイスを受け、日付をハードコーディングしました。
さらに先輩から教えていただいたAAAというテストの書き方を意識し改行を加えました。AAAとはarrange, act, assertionの順番にテストを書き、それぞれの間には改行を入れるというものです。今回、actとassertは1行にまとまっていたので、arrangeとact & assertの間に改行を入れることにしました。
リベンジ
以下のテストを作成し、プルリクエストを投げました。
test 'test_within_three_months?' do Date.stubs(:today).returns(Date.new(2024, 8, 20)) one_month_ago = '2024/07/20' three_months_minus_one_day_ago = '2024/08/21' three_months_ago = '2024/08/20' one_year_ago = '2023/08/20' assert_equal(@formatter.within_three_months?(one_month_ago), true) assert_equal(@formatter.within_three_months?(three_months_minus_one_day_ago), false) assert_equal(@formatter.within_three_months?(three_months_ago), false) assert_equal(@formatter.within_three_months?(one_year_ago), false) end
Date.today
の値はmochaというライブラリを使用してスタブしました。
今回のレビューでついに、approveをいただくことができました!
終わりに
先輩方はテストをスラスラと書いており、中にはテストを書かないと気持ち悪いと言う方もいます。私も経験を積み早くテストを書くことに慣れてその域に達したいです。
テストは大学のプログラミングの授業では通常扱わないため、テストを始めて書くことが難しく感じる未経験エンジニアも多いのではないでしょうか。
テストを書くことでバグを減らすことができ、自分が書いたコードが正しく処理されるかを確実に確かめることができます。自分が書いたコードを手作業調べるのに対して、テストを書けばテスト実行するだけですぐに確認することができます。テストを書けるようになり、使いこなせるようになれば開発スピードは間違いなく早くなるだろうと周りのエンジニアを見て気づきました。
まだ勉強しないといけないことがたくさんあるようです。未経験の皆さん一緒に頑張りましょう!
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。