プログラムはちゃんとテストをしよう②

はじめに

榊 統です。
LOCAL学生部アドベントカレンダー6日目の記事としてブログを書きます。
昨日の続きです。テストを書くってどういうこと?というのがわかったつもりになれることを目指します。
次回のアドカレはたかぴろくんです。楽しみにしています。
adventar.org

前回の振り返り

テストを行うことで、その部分が意図通りに動くかどうか、欠陥がないかといったことがわかります。
もちろん、テスト自体に不備があって、実は欠陥があったりすることもありますが。
テストをして正しく動作すると考えられるところは、バグの原因になりにくいためトラブルを解消するのが楽になります。

本日の内容

テストを書く時はどのようなことを考えれば良いかを考えます。
実際にテストを書く場合はフレームワークとか使うのかもしれませんが、今回は本題からずれてしまいそうなので考えません。

本題

テストをする、というのはそこまで難しいことではありません。
要するに多くの(できれば全ての)場合について、その機能が正しく動いているかを確認できれば良いです。
一番わかりやすいのは競技プログラミングではないでしょうか。
atcoder.jp
この問題では、将棋でいう角行のような移動を繰り返すことで到達可能なマス目の数を求めます。
このようなマス目の数を求めてみましょう。

直感的には、おおよそH×W/2個存在していそうだ、と考えられます。
H×Wが奇数の場合はH×W/2+1。
入力例1,2,3について、正しく答えが求まるはずです。
しかし、実はまだ考察が不足していて、これでは正しくない答えが出てしまうようなケースが存在します。
HかWが1である時です。このような時は実は駒は動けず、答えが1となります。

このような特殊な場合のことをコーナーケースと言ったりします。
テストでは、できる限り多くの場合を考え、動作が意図したものであるかを調べることが大事です。
かといって、あり得る入力全てを試そうと思えば、寿命が来るまで確認作業をしても終わらないでしょう。
適切に条件分岐をして、必要な場合だけテストするのが望ましいです。

テストを書く

今回も関数を用います。
以下のような動作をする関数benefitを作ります。

  • 単価と個数が引数として与えられる。
  • 単価か個数のどちらかまたは両方が負の数の場合は-1を返す。
  • そうでない場合は単価×個数を返す。
#include <iostream>

int benefit(const int &unit_price,const int &quantity){
    if(unit_price<0||quantity<0) return -1;
    return unit_price*quantity;
}

int main(){
    std::cout<<benefit(5,7)<<std::endl;
    return 0;
}

この関数が正しく動いているかを確認するには、どのような個数、単価を与えれば良いでしょうか?

  1. 単価が負の整数で、個数が正の整数
  2. 単価が正の整数で、個数が負の整数
  3. 単価も個数も負の整数
  4. 単価も個数も正の整数

この4つはあると良さそうです。
他には、0という整数を入れてみるのも良いですし、オーバーフローするような大きい数字を入れることを考えて、benefit関数をオーバーフローしないように変更したりしても良いでしょう。

この関数が何をするのかかをテストして、動作を確認できれば、仕様としてまとめることができます。
かなり面倒に思えますが、大規模なプログラムを書きあげる時には、バグの原因を見つける速さが全然違います。
正直、1つ1つ丁寧にテストして確認しながらプログラムを書いた方が、結果的には早く完成しがちです。

最後に

テストを書け、とかテストコードを書け、とか言われても、どうすればテストをしたことになるのかがわからないのではないか、と思っています。
私もよくわかっていません。
どういう条件で実行時エラーを起こすのか。
どのようなデータを持っていて、どのように使われているのか。
テストをしようと思えば、中で行われるべき操作についてよく調べなければなりません。
プログラムを調べることで自分の意図とプログラムをすり合わせることになり、足りなかった仕様や考慮できてなかったケースに気付くことができるかもしれません。
コーディングにおいて、想定外が減ることを切に願います。