デバッグのコツ


まえがき

これまでの経験で得た、デバッグの考え方を書き留めます。
プログラミング、テスト、バグトラッキングなどのノウハウには触れていません。

好きな言葉

  • バグのないプログラムはない
  • 再現性のないバグはとれない

テストの種類

  • 系統、無作為
    • あらかじめ定めた手順に従って行う
    • 実際の使用に即して自由に使ってみる
  • 開発、アルファ、ベータ
    • 部分的な実装を確認する
    • 一部未完成がある状態で、全体を確認する
    • 一通りの実装が完了した状態で、不備や不具合を洗い出す
  • クローズド、オープン
    • あらかじめ限定し特定されNDAを交わしたテスターを使う
    • 不特定多数のテスターを使う

※あくまでも私の定義です。業界や組織や個人によって他の分類や異なる定義があります。

準備

テスト指示書

概略の仕様や必要なテストプレイの内容などを、分かり易く図説を交えた文書にまとめておくと役に立ちます。

バグレポートの要素

  • バグレポートを作成する(してもらう)場合に必要な情報を挙げてみます。
  • あらかじめバグレポートの提出フォームのようなものを用意すると便利です。
  • 例えば、Googleフォームを使えば、Googleスプレッドシートにレポートを自動的に蓄積できます。

※バグトラッキングについては、この記事では扱いません。

症状

  • 不具合の現象がどのようなものかという話です。
  • 「最初に何が起きたのか」が最も重要です。発症後に操作を続けて生じる様々な現象は、既に壊れた実行環境に起因する二次的なものである可能性があり、あまり意味がない場合も多いです。
  • スクリーンショットやムービーがあると分かり易くなります。

再現性

  • 発症の頻度です。必ず再現するものもあれば、一度見たきりで、以後、全く見ないものもあるでしょう。
  • バグの原因を究明する難易度は、発症頻度に反比例します。(発症頻度が低いほど難易度が高いです。)

再現方法

  • レポートを受け取った者が現象を確認するにはどうすれば良いのかという話です。
  • 明確な方法が不明な時点では、「発症の前に、どういう状況で、何をしたのか」が重要です。
  • 少し前からのムービーがあるととても参考になります。

  • バージョン情報は必須です。
  • テストリリース毎にバージョンを更新し、実行時に表示して確認できるようにする必要があります。
  • バージョンが確認できない場合は、リリースを入手した日時で代替できる可能性があります。

環境

  • 発症した機種やOSなど、取り巻く環境です。
  • あらかじめテストに使用する環境の詳細スペックをリストアップしておくと便利です。
  • オープンベータテストなど、不特定多数の場合は、レポートが寄せられる都度、調査して蓄積していきます。
  • プラットフォーマーが、機種データベースを整備している場合があります。

課題

  • 症状のどこが問題で、どうなっていることが正しいのかという話です。
  • 仕様通りの現象がバグとしてレポートされることもあります。

バグの洗い出し

  • バグを見つけてレポートにします。

※テストプレイでバグを見つけるコツは、この記事では扱いません。

デバッグの要素

現象の再現

  • 開発者の手元で現象を再現することは重要です。
  • 再現方法が不明な時点では、「実行環境」、「進行状況」、「直前の操作」が重要な手がかりです。
  • 確実に修正されたと認めるには、現象とその再現方法が分かっていなければなりません。

何が起きているのか

  • 症状が、どのような現象であるのかを分析します。
    • この分析によって再現方法が明らかになることもあります。
  • 例えば、「モーダルダイアログが閉じない」という症状が、「確認ボタンが反応するのに閉じない」のか「ボタンが反応しない」のか、「ボタンが表示されない」のか、といった話です。
    • さらに、「ボタンが反応しない」のは、「ボタンが応答していない」とか、「ボタンより手前に見えないオブジェクトがあってボタンにイベントが届いていない」とか、といった話もあるでしょう。

いつ発症したのか

  • どのバージョンで発症したのか、どのバージョンまでは正しく動いていたのか、という話です。
  • バージョン間の変更箇所から、次の「何処で起きているか」のヒントまたは答が得られます。

※リビジョン管理については、この記事では扱いません。

何処で起きているか

  • プログラムの何処で起きた現象なのかを探ります。
    • 現象の分析から、「何処で起きているか」よりも先に「何が間違っているか」が明らかになる場合もあります。
  • 前述の例なら、「ダイアログの初期化の冒頭で例外が発生」といった話です。
    • 例外の発生であれば、発症箇所は既に特定できていて、スタックトレースが使えれば呼び出し経路も明らかです。
  • ソースで流れを追い、何処まで正しく動き、何処でバグったのかを発見します。
    • トレース可能なデバッガーが使えるなら、経路を順に追うことができます。
      • ブレークポイントが設定できれば、直前までトレースを端折ることができます。
    • デバッガーがなくても、経路に順にコンソールへのデバッグ情報出力を仕込んで走らせることで、流れを追うことができます。
    • デバッグにコンソールが使えない環境では、あらかじめ、画面にオーバーラップするデバッグ情報表示専用のレイヤーを仕込んでおくのも方法です。
  • 逐次リアルタイムに更新されるような情報は、コンソールに出すよりも画面の固定位置に表示された方が確認しやすい場合もあります。
  • 多数のデバッグ出力を仕込んで、詳細な動作ログをファイルに保存し、実行後に分析するという選択肢もあります。
  • ソースをコメントアウトして、どの処理を飛ばすと発生しなくなるかを調べることもできます。
  • コールバック、コルーチン、別スレッドなどの非同期処理が存在する場合は、そちらの影響も配慮する必要があります。
    • 前述の例だと、コールバックされるルーチンにもデバッグ情報出力を仕込み、「ボタン押し下げのイベントが届いているか」を確認する必要があります。
    • 非同期処理では、スタックトレースで呼び出しの大元を知ることが難しくなります。

何が間違っているか

  • データバグなのかプログラムバグなのか、書き間違い程度なのか、計算式が誤っている程度か、クラスの構造やアルゴリズムに起因するような設計ミスなのか、具体的にどう間違ったのか、といった話です。
  • 前述の例だと、「未初期化でNULL参照がある」とか、「ボタンのイベントハンドラの設定漏れ」とか、「ダイアログのレイアウトデータの瑕疵」とか、といった話です。
  • ソースを追っても不審な点を見つけられないときは、逆に「どうすればこの現象を起こすことができるか」と考えることで糸口がつかめるかもしれません。
    • 前述の例で、「ボタンにイベントが届いていない」場合に、「ボタンの手前に透明なオブジェクトを置いてイベントを横取りすれば…」などと考えるわけです。
  • いちばん最後に、環境やツールを疑ってください。
    • 「公式」ドキュメントを見直しましょう。使い方の間違いや仕様かも知れません。
    • 実行環境や開発環境を再起動してみましょう。
    • ビルドし直してみましょう。
      • コードだけがビルドされていてデータの更新が反映されていないとかはないですか?
    • 実行しているバイナリは間違いないですか?
      • コードに埋め込んだバージョン識別子を表示させられませんか?
      • ビルドできていないのではなく、ビルドが更新されていないのかも知れません。
      • 別名で残っていた古いアプリを起動していませんか?
    • キャッシュやセーブデータを初期化してみましょう。
    • 実行環境や開発環境のバージョンを新しくしてみましょう。
    • サーバ側の一時的な不具合を疑ってみましょう。
    • 取り敢えず寝ましょう。
    • どうしてもダメなら、最小の実証プロジェクトを構築してみましょう。

どう直すのか

  • 間違いの程度によって、修正方法を検討する必要が生じます。
  • 書き間違いや式の誤りであれば話は単純ですが、プログラムやデータの構造に及ぶようなアルゴリズムのバグの場合はやっかいです。
  • 自律的なコード群が相互のタイミングに依存していたり、アルゴリズムが錯綜していたりして、アルゴリズムのバグなのか判断が付かない場合は、思い切ってリファクタリングするのも選択肢です。

確かに直ったのか

  • 修正版で、現象が再現されなくなっていることを確認します。
  • 現象と再現方法が明らかでないと「修正完了」の確認ができず、「発症しなくなったようだ」としか言えません。

記録を残す

  • 発症報告から現象確認、分析、対処、修正確認に至る課程で、逐次履歴を残して一元管理しましょう。
  • 準備 > バグの要素で例に挙げたスプレッドシートに書き加えていく方法が考えられます。

※バグトラッキングについては、この記事では扱いません。

デバッグの極意?

  • デバッグとは切り分けです。
  • あらゆる可能性を考慮して、最初は大まかに、次第に子細に、該当しないものを外していくと、最後に真理が現れます。
  • 長い模索の果てに真理に至るのは便座の上です。
    • (開発環境のないところで、思索にふけっていると、真理が降りてきます。)
  • 灰色の脳細胞にコードがロードされ、現象がインプットされていれば、安楽椅子探偵のように、思索のみで答に行き着くことでしょう。

冒頭で「バグのないプログラムはない」などと嘯いておきながらアレですが、バグは作らないことがベストですし、理想へ近づけることはできます。

  • コーディング前にアルゴリズムの検証をしましょう。
  • バグが出にくいプログラムにしましょう。
    • 範囲チェックをちゃんとするとか、ループで回す場合に定数を使うより実際のサイズを得て使うとか、「(値が減少して)ゼロになったら」でなく「ゼロ以下になったら」と書くとか、マジックナンバーを使わないとか、一定の書法を守るとか…
  • バグったときに解るプログラムにしましょう。
    • 柔軟性の高いプログラムは使い勝手は良いですが、エラーにならずに動き続けるとバグが潜在する原因にもなります。想定外の事態に対して、対応するか例外を発生させるかのバランスを考える必要があります。
    • 返されるエラーをチェックしていなかったり、むやみに例外をトラップしている場合にも注意が必要です。
  • あらかじめ計画し小刻みにテストし、確認済みの小さな部品を集めて組み立てましょう。

※これらのプログラミングのノウハウは、この記事では扱いません。

あとがき

最初に手に触れた開発環境は、8桁の16進表示と16進キーでマシン語を直接読み書きして、マシン語の命令毎にステップ動作ができるだけでした。
開発環境の進歩はめざましいですが、今でも、過去の経験が役に立つこともあるので、記事にしておこうと思いました。
書き忘れていることや、うまく表現できていないことも多いと思うので、随時、更新していくつもりです。
最後までお読みいただきまして、どうもありがとうございました。