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

はじめに

みなさんお久しぶりです。
榊 統です。
最近は就活とゼミに追われる日々で、あまりツイートもできていません。
さて、今回はLOCAL学生部アドベントカレンダー5日目の記事としてブログを書きます。
明日はこのブログの続きを書きます。
adventar.org

導入

みなさんはプログラムを書く時に、テストを書きなさいと口酸っぱく言われたことはないでしょうか。
そもそもテストって何なのかわからない、必要性が感じられない等と思っていませんか?
厳密なテストをしなくても、テストをしようと思うだけで、そのプログラムをより深く知ることができます。
このブログと明日のブログで、下記2点が何となくわかったつもりになることを目指します。
①テストの重要さ
②テストでは何をすれば良いのか

プログラムの動作について

プログラムに厳密な仕様があるかどうかはその時々だと思います。
大学の課題のために書いたり、自分が便利だと思うツールを書いたり自動化したり......。
どのような場合でも、「このような時にはこう動いて欲しい」というものは決まっているはずです。
ここでは、学生の中間試験と期末試験の点数から、成績(不可、可、良、優、秀)を求めて返り値とする関数を考えます。
使用する言語はC++で、成績評価方法は以下の通りです。

  • 中間試験と期末試験はともに100点満点で、点数は整数値をとる。
  • 中間試験の点数×0.4+期末試験の点数×0.6をAとします。
  • Aが0以上60未満ならば不可
  • Aが60以上70未満ならば可
  • Aが70以上80未満ならば良
  • Aが80以上90未満ならば優
  • Aが90以上100以下ならば秀

この条件をそのままソースコードにした関数は、以下のような感じになります。

#include <iostream>

std::string grade_evalution(const int &mid_term_exam_scores,const int &final_exam_scores){
    const double grade=mid_term_exam_scores*0.4+final_exam_scores*0.6;

    if( 0<=grade&&grade<60) return "不可";
    if(60<=grade&&grade<70) return "可";
    if(70<=grade&&grade<80) return "良";
    if(80<=grade&&grade<90) return "優";
    if(90<=grade&&grade<=100) return "秀";
}

int main(){
    std::string grade=grade_evalution(54,73);
    std::cout<<grade<<std::endl;

    return 0;
}

中間試験の点数と期末試験の点数を引数として与えると、その成績を表す文字列が返り値になる関数です。
試しに実行してみれば、可という出力が得られます。
何となく普通に動いていて、これで問題ないように見えますが、非常に危険な欠陥を抱えています。
例えば、grade_evalution関数に与える引数に、0から100以外の値を入れてみましょう。

std::string grade=grade_evalution(120,150);

実行すると、grade_evalutionはどのif文にも当てはまらず、何も返り値を返すことができません。
このような場合は、プログラムが実行時エラーを吐いて突然終了してしまうことがあります。
また、仮にデータの出力ができても、意味不明な文字列が出力され、それが原因で他のプログラムが意図しない動作をしてしまうかもしれません。


ところで、試験の点数は0点から100点までだから、そのようなケースを考える必要はないと思う人もいるでしょう。
以下のような考察から、そのようなケースを考えるべきだとわかります。
この関数が参照するのは成績なので、外部の.csvファイル等からデータを読み込んで使用する、ということはあり得ます。
.csvファイルへの入力時に、点数を打ち間違えてしまうことはないでしょうか?
人の手が関わるところには、絶対大丈夫はあり得ないという意識を持っておくべきです。


これまでの話から、「このような時にはこう動いて欲しい」という気持ちだけで書いたプログラムには、意図せず不具合を発生させる可能性が潜んでいることがわかって頂けるのではないでしょうか。
次は、このプログラムが意図しない挙動をしないための工夫を考えます。
このようなことを考えると、どのようなテストが必要なのかが見えてきます。

意図しない挙動を減らすために

今回見つけた不具合になり得る要因は、引数が0から100までの値をとらないかもしれない、というものです。
引数がどちらも0以上100以下の整数値ならば、このプログラムは正しく動作します。

    for(int i=0;i<=100;i++){
        for(int j=0;j<=100;j++){
            std::string grade=grade_evalution(i,j);
            std::cout<<grade<<std::endl;
        }
    }

どちらか、もしくは両方の引数が0以上100以下の整数値をとらない時は、"エラー"という文字列でも返してあげましょう。

std::string grade_evalution(const int &mid_term_exam_scores,const int &final_exam_scores){
    if(mid_term_exam_scores<0||mid_term_exam_scores>100||final_exam_scores<0||final_exam_scores>100) return "エラー";
    
    const double grade=mid_term_exam_scores*0.4+final_exam_scores*0.6;

    if( 0<=grade&&grade<60) return "不可";
    if(60<=grade&&grade<70) return "可";
    if(70<=grade&&grade<80) return "良";
    if(80<=grade&&grade<90) return "優";
    if(90<=grade&&grade<=100) return "秀";
}

これで、引数の値が0以上100以下でないような時でも、文字列を返すことができるようになりました。
イメージ的には、プログラムを書いた時に考察不足になったところがよくバグの原因になったりします。
関数であれば、どのような引数の時にどのような返り値を返すのか、詳細な仕様を残しておくことで、この関数を再利用する時、とても楽になるでしょう。

まとめ

自分が意図した通りにプログラムが動いていても、自分が想定していなかったようなケースが存在することもあります。
それが原因でプログラムが意図しない動作をして、その他のプログラムにまで影響を与えて...と、小さな1つのプログラムが原因で、広範囲にバグをまき散らすと、どのプログラムが原因となっているかがわからず、特定にとても時間が掛かったりします。
そのような状況にならないためにも、ある程度のまとまった機能(例えば関数やクラス)が出来たところで、1つ1つ正しく動作するかを確認すべきです。

明日のブログでは、テストを書く、ということがわかったつもりになれるような記事を書きたいと思います。