こんにちは。システム部のマツムラです。
CTO室というチームで開発等をやっています。
GMOリサーチに入社して早10年たとうとしています。
入社当時は、infoqの運用・開発を担当していました。
https://infoq.jp/
色々なチームを渡り歩き、最近ではPythonを使った開発をしています。
さて、今回はユニットテストについてお話ししてみようと思うのですが、ユニットテストを書くのって意外と難しいと思いませんか?
また、他の人が書いたユニットテストをパッと見てすぐに理解するのも難しいなあと感じます。
・・・ということで今日は、私がGMOリサーチで色々な言語に取り組み、色々な人のユニットテストを見たり、自分で書いてみたりしてきた上で、学んだことや教えてもらったことをまとめてみました。
ユニットテストとは
ネットなどで検索すると出てくるように、ユニットテストとは、単体テストのことで、プログラムの個々の機能が正しく動いているか確認するテストのことです。
メソッド単位でテストができるため、プログラム開発途中で、機能の確認や、パターンが網羅されているかチェックすることができます。
その反面、ユニットテストを書くことに追われてしまったり、開発者の負担が大きくなって不完全なテストになってしまったり、評価したい値が評価されていない(テストそのものが機能していない)場合がある、などのデメリットもあります。
GMORでのルール
ずばり、カバレッジ100%(を目指そう)です!
つまり、全てのソースコードをテストしているということです。
GMOリサーチでは、ユニットテストのカバレッジが100%じゃないとリリースできないことになっています(実際の運用では、100%じゃなくてもOKだったりもしますが。。。)。
設計→開発(UT)→テストと進めていくのですが、全体テストを開始する前にチェックゲートがあり、ここでプロジェクトで利用しているプログラムのユニットテストのカバレッジが100%である事をチェックします。
100%になっていない場合は確認、修正します。
どうして100%?と思う方もいらっしゃるでしょう。
実施率100%は中々大変なことですし、意味ないと思う方もいらっしゃるかもしれません。
賛否両論だと思います。
ただ、以前ユニットテストについてGMORで勉強会があったときに教えてもらいました。
・カバレッジ100%にしないとユニットテストに対する意識が低くなってしまう可能性がある
これは、個人の判断でユニットテストを書かなくてよいと判断したり、通すべき処理のユニットテストができていなかったり、と人によってユニットテストに対する意識が変わってしまうからです。
・カバレッジ100%だとカバレッジが下がったことにすぐに気が付ける
100%だったユニットテストが99%に落ちるのはすぐに気がつくことができますが、80%(そもそも元の%基準もよくわからない状態)で、下がってもなかなか気が付く事が難しいと考えています。
これらの理由から、100%ルールがあります。
とはいえ、ここのメソッドのユニットテストをどうしても100%にできない・・・どうやったらいいの・・・と悩むこともあります。
その場合は、上長相談の上ignore(※カバレッジから除外すること)できるか確認することになっています。
これまで学んだ事や注意点
次に、ユニットテストについてこれまで学んできたことをまとめてみました。
これまでに学んだこと
- ignoreは書かない(勝手に判断しない)
- テストコードは人のために書くこと。一年後もわかるように。
- テストケース名はわかりやすく。処理をつらつら書かない(これ結構難しい)また、現象じゃなくて仕様を書く。(これも難しい・・)ユニットテストみたら仕様がわかるように書く。
- テストが書きづらい、書けない、のは実装がおかしい(よく言われた・・・)
- ユニットテスト内での検証時に自分で作ったメソッドは利用しない。そのメソッドに修正が入ると直接関係のないユニットテストに影響が出るため。
- ユニットテストをまとめて後で書かない。まとめて書くと大変です。一個のメソッドまたは、一個のクラスを作成したらそれに対するUTを書いて確認します。
- 一つのメソッド内に複数の意味がある検証を行わない。
いくつか、失敗例を挙げてみます。
<失敗例1:わかりづらいテストケース名>
①
public void test_設定したパラメータでgetCustomeメソッド実行されgetCustomeメソッドの結果が返却されること(){ ・ ・ ・ ・ } |
①は処理の内容であって仕様ではありません。パッと見てもよくわからない。。
指定したパラメータで実行されているかどうかは、ユニットテスト内で評価するので、テストケース名につける必要はありません。
ということで、ここで適切だと思われるテストケース名が以下の②になります。
②
public void test_顧客リストが返却されること(){ new MockUp<getCustomeService>() { @Mock(invocations = 1) public List getCustome(String departmentId) { assertEquals(“1”, departmentId); ←ここでパラメータの評価をする } } } |
テストケース名も中々難しいですよね。
「正常系テスト」とかたまに見かけますが、さっぱり仕様が分からない!と思ってしまいます。
へー正常系かー・・・みたいな感じで。
<失敗例2:ユニットテスト内で複数の検証をしている>
テストもとになるソースコードがこちらです。
このコードは、振込依頼の日程をその月の固定20日にセットする関数です。
20日が日曜日と土曜日の場合は金曜日にセットします。
このメソッドは、銀行振り込み指定日を決めるために作成されたメソッドです。(※祝日対応できてませんが汗)
public static String getTransferDate(Calendar cl) { // 日付に20日をセット cl.set(Calendar.DATE, 20); switch (cl.get(Calendar.DAY_OF_WEEK)) { case Calendar.SUNDAY: // 日曜日なら金曜に変更するため2日前にする cl.add(Calendar.DATE, -2); break; case Calendar.SATURDAY: // 土曜日なら金曜に変更するため1日前にする cl.add(Calendar.DATE, -1); break; default: // それ以外はなにもしない break; } SimpleDateFormat dateformat = new SimpleDateFormat(“yyyyMMdd”); return dateformat.format(cl.getTime()); } |
①ユニットテストコード
ユニットテスト内で複数の検証をしている場合。
public void test_振り込み指定日20日固定で返却する(){ Calendar cl = Calendar.getInstance(); cl.set(2020, 8, 1); //9月を指定 9月は日曜日 String transferDate = PointExchangeUtil.getTransferDate(cl); assertEquals(“20200918”, transferDate); cl.set(2020, 7, 10); //8月を指定 8月は木曜日 transferDate = PointExchangeUtil.getTransferDate(cl); assertEquals(“20200820”, transferDate); cl.set(2020, 5, 15); //6月を指定 6月は土曜日 transferDate = PointExchangeUtil.getTransferDate(cl); assertEquals(“20200619”, transferDate); } |
このケースでは簡単な処理なので、わかりづらさがあまりないかもしれませんが、メソッドの結果は一個のメソッドで一個の検証したほうがほかの人が見るときに理解しやすいです。
そのため、以下②のようにしてテストコードを書いています。
②ユニットテストコード
三つのテストケースに分けている。テストケース名に意味を持たせている場合。
public void test_振り込み指定日20日固定で返却する() { Calendar cl = Calendar.getInstance(); cl.set(2020, 7, 10); //8月を指定 String transferDate = PointExchangeUtil.getTransferDate(cl); assertEquals(“20200820”, transferDate); } public void test_振り込み指定日20日固定で返却する_20日が日曜日の場合は二日前に設定される() { Calendar cl = Calendar.getInstance(); cl.set(2020, 8, 1); //9月を指定 String transferDate = PointExchangeUtil.getTransferDate(cl); assertEquals(“20200918”, transferDate); } public void test_振り込み指定日20日固定で返却する_20日が土曜日の場合は一日前に設定される() { Calendar cl = Calendar.getInstance(); cl.set(2020, 5, 15); //6月を指定 String transferDate = PointExchangeUtil.getTransferDate(cl); assertEquals(“20200619”, transferDate); } |
まとめ
入社当時は、慣れないこともあり、ロジックを書いて動作確認することに追われて、ユニットテストを書くのがとても負担に思えました。
カバレッジ・・とにかくとにかく緑にすればいいんでしょ・・・って感じで。
でも今は違います。
すごく率直な意見になってしまうのですが、ユニットテストってすごいなーと思います。
複雑なソースコードを書くときは、途中までソースコードを書いて途中でユニットテストを書くこともあるくらいです。
頭の中が整理されますし、自分が書いてき たソースコードの期待値が正しいか途中でチェックもできます。
また、一個のクラスを作ったらユニットテストを完成させてレビュー依頼をしています。
これも結構大事で、レビューする人の負担にならないし、すぐに見てもらえるので、お互いにとってよいことだらけです。
まだまだ相変わらずレビュー指摘をもらってしまう私ですが、これからも諸先輩から教わった事を生かして取り組んでいければと思っています!
それでは、最後まで読んでくださってありがとうございました!
*
GMOリサーチでは、WEBエンジニア(サーバーサイド、インフラ、フロントエンド)を随時募集しております。
興味のある方は、ぜひこちらからご応募ください!
詳しい募集要項など採用情報はこちら