C++ 14日目:テストとまとめ

ここまで、電卓のアプリを作成してきました
この章では、作成した機能(四則演算、小数点、AC、戻る、+/-、ルート)ごとに、正常系と異常系のテストケースを具体的に記述しテストを実施していきます
また、最後に、全体のソースの公開および今後の課題についてもまとめていきます

テスト設計

ソフトウェアの品質を担保するために、テストの目的や内容を決め、テストケースやテストシナリオを作成するプロセスのことを「テスト設計」と言います

テストの必要性

まず、テストの必要性について考えてみましょう
品質の高いソフトウエアを作るためには、以下の3点が重要なプロセスとなります

  1. 開発の信頼性
    プログラムが期待通りに動くかを確認します
  2. バグの早期発見
    複雑な機能を追加する前に、小さな問題を解決することができます
  3. 品質の保証
    一貫したテストを繰り返し行うことで、安心して使うことができるアプリになります

テスト設計の注意点

テスト設計を行うためには、以下の注意点があります

  1. テスト対象の全体像を把握し、重要な機能やリスクの高い部分を優先的にテストすること
  2. 手順や期待値を明確に記述すること
  3. テスト設計仕様書を作成し、テストの目的や背景、範囲、条件、技法などを文書化し、関係者と共有すること
  4. テストケースをレビューおよび検証し、エラーや不一致をチェックし、必要なすべての条件をカバーするようにすること

これらの注意点を考慮し、電卓アプリのテスト項目を挙げてみましょう

テスト項目の整理と実行

テスト項目を「正常系テスト」と「異常系テスト」に分けます
「正常系テスト」とは、アプリが正しく動作することを期待するテストです
一方、「異常系テスト(エッジケース)」は 意図しない入力や、境界値での動作を確認するテストです

AC(All Clear)ボタン

このボタンは、電卓の状態を初期化する役割を担います
以下のテストを実行して、画面と内部の変数が正しくリセットされるか確認します

項目テスト内容期待結果
UI(画面)のクリア画面に何らかの数字や計算結果が表示されている状態でACボタンを押す数値と演算子の両方の表示がクリアされる
ボタンの有効化エラー表示が出た状態でACボタンを押すACボタン以外の無効化されていたすべてのボタンが押せるようになる

数字、小数点

数字ボタンは、ユーザーが値を入力するための基本機能です
入力のルール(0の扱い、桁数制限など)が正しく適用されるかを確認します

項目テスト内容期待結果
正常系テスト123と続けて数字を入力する画面に123と表示される
0の後に5と入力する画面が5と表示される
0.の後に5と入力する画面が0.5と表示される
特殊な入力後の挙動1 + 1 = の後に3と入力する画面が23ではなく、3と表示される
1 × の後に3と入力する画面が13ではなく、3と表示される
1+1=としたあと、.と入力する画面が2.ではなく、.と表示される
0.1のあとに.を入力する画面が0.1.ではなく、0.1と表示される
異常系テスト10桁以上の数字を入力しようとする入力した数字が受け付けられず、表示が変わらない
0.のあとに.を入力する画面が0..ではなく、0.と表示される

演算子、イコールボタン

これらのボタンは、電卓の核となる計算ロジックの部分です
様々な組み合わせの計算で、期待通りの結果が表示されるかを確認します

項目テスト内容期待結果
正常系テスト1 + 2 =、1 + 2 + 3 =、1 + 2 + 3 × 4 = と入力するそれぞれの計算結果が3、6、24と正しく実行され、最終的な結果が表示される
+、-、×、/など、単独の演算子を最初に押す0が自動的に左辺値として設定され、画面に0と演算子が表示される
異常系テスト5 ÷ 0 を計算する画面に「Cannot Divide by Zero」といったエラーメッセージが表示され、ACボタン以外は無効化される
1 + × や 1 + + のように、演算子を続けて押す最後の演算子に上書きされ、画面の演算子表示が更新される
9999999999×10を計算する画面に「整数部が許容桁数を超えています」といったエラーメッセージが表示され、ACボタン以外は無効化される

戻るボタン

このボタンは、入力中の数字を一桁戻す機能を実現します
数字を入力中に最後に入力した値が削除されて表示されるかを確認します

項目テスト内容期待結果
正常系テスト123と入力後、戻るボタンを押す最後に入力した3が削除され、画面に12と表示される
異常系テスト1と入力後、戻るボタンを押す画面には何も表示されない
1+2=と計算実行後に戻るボタンを押す画面の表示は変化せず、3と表示される

ルートボタン

このボタンは、表示している数字のルート値を計算する機能を実現します
数字が表示している状態でルートボタンを押すことで、ルート値が正しく計算されて表示されるかを確認します

項目テスト内容期待結果
正常系テスト2と入力後、ルートボタンを押す画面に1.414213562と10桁で表示される
1×2=と入力後、ルートボタンを押す画面に1.414213562と10桁で表示される
4×2と入力後、ルートボタン、=と順番に押す画面に1.414213562と表示され、=が押された後、5.656854248と表示される
異常系テスト起動時またはACボタンを押した後、ルートボタンを押す画面には何も表示されない
1-2=と計算実行後にルートボタンを押す画面に、「Invalid Input」といったエラーメッセージが表示され、ACボタン以外は無効化される
0と入力後、ルートボタンを押す画面に0と表示される
4×と入力後、ルートボタン、=と順番に押す数字キーのあとにルートを押すことでその数字の平方根を計算します。したがって、4の平方根の値2が画面に表示される

+/-ボタンを

このボタンは、表示している数字の値の符号を反転する機能を実現します
数字が表示している状態で符号ボタンを押すことで、符号が反転して表示されるかを確認します

項目テスト内容期待結果
正常系テスト2と入力後、+/-ボタンを押す画面に-2と表示される
1×2=と入力後、+/-ボタンを押す画面に-2と表示される
4×2と入力後、+/-、=と順番に押す画面に-2と表示され、=が押された後、-8と表示される
4×と入力後、+/-、=と順番に押す画面に-4と表示され、=が押された後、-4と表示される
異常系テスト起動時またはACボタンを押した後、+/-ボタンを押す画面には何も表示されない

修正

すべてのテスト項目を実際に実行した結果、1件の不具合を発見しました
この問題は、単なるバグ修正ではなく、電卓の表示仕様を根本から見直すきっかけとなりました

  1. 不具合の内容:計算結果の表示桁数超過
    本アプリの仕様では、表示可能な数字の桁数を10桁に制限しています
    しかし、9999999999 × 10 のような計算を行うと、結果が10桁を超えて表示されてしまうことが判明しました

この不具合の原因は、ConvertDoubleToCString()関数にありました
当初、指数表記を避けるために_T(“%.15g”)と指定していましたが、これが意図せず10桁を超える表示を許可していました

CString CMyCalculatorDlg::ConvertDoubleToCString(double value) {
    CString buffer;

    // 精度を大きく指定して指数表記を抑制
    // 例: 15桁の有効数字
    buffer.Format(_T("%.15g"), result);
}
  1. 修正内容と技術的判断
    この問題を解決するため、double型からCStringへの変換処理を根本的に見直しました
    単に桁数を制限するだけでなく、整数部と小数部の桁数を動的に計算し、10桁という仕様に沿って表示するロジックを実装しました

具体的には、C++標準ライブラリのstd::stringstreamとstd::fixed、std::setprecisionを組み合わせて使用しました
また、std::setprecisionを使用して小数を変換すると、四捨五入されてしまうので、それを防ぐため指定された桁数で切り捨てる処理を追加しました(0.1234567896と入力した場合0.123456789と表示する)
そして、小数点より後ろの末尾に不要な0がある場合はそのゼロを削除するように対応しました

/*  引数の整数部および小数点以下の桁数を計算し小数部の桁数を戻り値とする */
/*  ただし、整数部が許容桁数を超えている場合、エラーを発生する           */
size_t CMyCalculatorDlg::getDecimalDigits(double value)
{
    // valueを文字列に変換する
    std::wstringstream ss;

    // 小数部が0でも固定小数点表記を強制し、精度を設定
    ss << std::fixed << std::setprecision(15) << value; // double型の最大値で変換する  
    std::wstring temp_str = ss.str();

    // 整数部の桁数を計算
    size_t dot_pos = temp_str.find(L'.');
    size_t integer_digits = (dot_pos == std::wstring::npos) ? temp_str.length() : dot_pos;

    // 整数部が指定された最大桁数を超えているかチェック
    if (integer_digits > MAX_SIZE) {
        throw std::invalid_argument("整数部が許容桁数を超えています。");
    }

    // 小数部の桁数を計算
    size_t decimal_digits = (integer_digits >= MAX_SIZE) ? 0 : MAX_SIZE - integer_digits;

    return decimal_digits;
}

// valueを小数点以下指定された桁数で切り捨てる関数
double CMyCalculatorDlg::truncate_double(double value, int precision) {
    double factor = std::pow(10.0, precision);
    return std::trunc(value * factor) / factor;
}

CString CMyCalculatorDlg::ConvertDoubleToCString(double value) 
{
    std::wstringstream ss;

    // 小数部の桁数を計算
    size_t decimal_digits = getDecimalDigits(std::abs(value));

    double truncated_value = truncate_double(value, decimal_digits);

    // 小数部が0でも固定小数点表記を強制し、精度を設定
    ss << std::fixed << std::setprecision(decimal_digits) << truncated_value; // 許容範囲内の桁数で文字列に変換する  

    std::wstring s = ss.str();

    // 小数点の位置を取得
    size_t real_dot_pos = s.find(L'.');

    if (real_dot_pos != std::wstring::npos) { // 小数点が見つかった場合
        // 小数点より後ろの文字列を取得
        std::wstring fraction_part = s.substr(real_dot_pos + 1);

        // 末尾の不要なゼロを探す
        size_t last_non_zero = fraction_part.find_last_not_of(L'0');

        if (last_non_zero != std::string::npos) {
            // 小数部が有効な数字を含んでいる場合、不要なゼロを削除
            s.erase(real_dot_pos + 1 + last_non_zero + 1);
        }
        else {
            // 小数部がすべてゼロの場合、小数点とゼロを削除
            s.erase(real_dot_pos);
        }
    }

    return CString(s.c_str());
}

この修正により、以下の仕様を厳密に満たすことができました

  • 計算結果が10桁を超える場合は、「Error」と表示する
  • 計算結果が10桁以内の場合は、固定小数点表記で、不要な末尾のゼロや小数点が表示されないようにする

全体のソース

不具合も修正したソースは以下のリンクからご確認ください

まとめ

プロジェクトを振り返る

この電卓アプリを開発、設計する上で、次の3点について特に意識して作成しました

1. 状態管理の重要性

現在選択されている演算子(last_operator)、入力中の文字列(current_buffer)、左辺値(stack[0])、右辺値(stack[1])をそれぞれ格納する変数をそれぞれ用意し、現在の状態を管理するようにしました
そして、どのボタンが押された時、これらの変数の値がどのように変わっていくかを明確にすることで、複雑な計算ロジックをシンプルにすることができました
設計する前に、以下のような表を作成することでどのように値が変化しているかを確認しました

入力current_bufferlast_operatorstack[0]stack[1]
開始    
11   
212   
12   
312.3   
+ +12.3 
44+12.3 
545+12.3 
= +12.345
  =12.3+45 

2. 関数の役割分担

下請け関数として、特定の役割に専念した関数を用意することで、コード全体がすっきりとまとまることができました
同時に、関数の役割をはっきりさせたことで、修正しやすくなりました

たとえば、下請け関数として用意した関数は次のようなものがあります

  • calculate():四則演算を実行する
  • IsNumberAndConvert():文字列を数字に変換する
  • setEditBoxText():表示エリアに文字列をセットする
  • ConvertDoubleToCString():引数のdouble型の値を文字列に変換する

3. エラーハンドリング

ユーザーの誤った操作(例:0での除算、負の数のルート計算)に対して、プログラムが予期せぬ終了をせず、適切にエラーメッセージを表示するようにするため、try-catchとthrowを使いました

たとえば、下請け関数のIsNumberAndConvert()の場合、文字列を数字に変換した値を返します。変換が失敗した場合、throwを返すことで、エラー処理を呼び出し側に任せることで、一か所に集約しました。これにより統一されたエラーハンドリングが可能になりました

double CMyCalculatorDlg::IsNumberAndConvert(const TCHAR* lpText) {
    TCHAR* endptr;
    double result = _tcstod(lpText, &endptr);

    // 文字列全体が消費され、かつ変換された文字が1つ以上あるか確認
    if (*endptr == '\0' && endptr != lpText) {
        return result;
    }
    else {
        throw std::invalid_argument("数式が不完全です");
    }
}
void CMyCalculatorDlg::clickOperatorButton(LPCTSTR lpText)
{
    bool flg = current_buffer.IsEmpty();
    double result;

    try {

        if (flg == false) { //  1. current_bufferに値が入っている場合
            //  current_bufferの値をstack[0]もしくはstack[1]へコピーする
            //(stack[0]が空文字のときはstack[0]へ、そうでなければstack[1]へ)
            if (stack[0].IsEmpty()) {
                stack[0] = current_buffer;
                // stack[0]が数値として正しいかチェックし、double型に変換する
                result = IsNumberAndConvert(stack[0]);
            }
            else {
                stack[1] = current_buffer;
                result = calculate();
            }
        }
        else // current_bufferが空文字のとき
        {
            //  current_buffer、stack[0]が空文字のとき
            // stack[0]は0とする
            if (stack[0].IsEmpty()) {
                stack[0] = "0";
            }

            result = IsNumberAndConvert(stack[0]);
        }

        // 計算結果をstack[0]にコピーする
        stack[0] = ConvertDoubleToCString(result);

        setEditBoxText(stack[0]);

        //  (2)stack[1]の値をクリアする
        stack[1] = "";

        //  2. operatorの値を選択された演算子に変更する
        last_operator = lpText;
        setOperatorText(lpText);

        //  3. current_bufferの値を空文字にする
        clearCurrentBuffer();
    }
    catch (const std::invalid_argument& e) {
        CString str(e.what());
        setEditBoxText(str);
        disableButton();
    }
}

今後の課題

このプロジェクトでは、最低限の四則演算機能を実装し、基本的な電卓の骨組みを完成させました
ここからは、さらに高度な機能やより良いUIを目指して、拡張した電卓を考えていきます

機能の拡張

この電卓アプリをさらに進化させるために、以下の機能を追加していくこともできます

  1. 括弧(())と演算子の優先順位
    2 + 3 * 4 のように、掛け算や割り算を先に計算するルール(演算子の優先順位)を実装します
    これにより、より複雑で一般的な数学の計算に対応できるようになります
  2. 履歴機能
    過去の計算結果をさかのぼって表示する機能を追加します
    これにより、過去の計算の再利用や計算ミスの確認など、ユーザーにとって便利な機能を提供することができます
  3. メモリ機能
    M+、M-、MR、MCなどのメモリー機能など電卓では、一般的に搭載されている機能を実装します
    これにより、計算の手間が減り複雑な計算にも対応することができるようになります

コードの改善

  1. クラスの設計
    現在のコードはCMyCalculatorDlgクラスにすべてのロジックが書かれています
    これを、計算ロジックとUIロジックに分離することで、コードの可読性とメンテナンス性が向上し、将来的な機能追加が容易になります
  2. UI/UXの改善
    デザインの変更や、キーボード入力への対応など、ユーザーインターフェースをより使いやすくするための改善点があります

今後の課題を参考に、この電卓アプリから、さらに使いやすい電卓アプリを作成してみてください

コメント

この記事へのコメントはありません。

関連記事

C++ 発展編 1日目

Python 応用編 5日目

C++ 基礎編 6日目

PAGE TOP