プログラミングの学び方(9) – バグを極力生み出さないための工夫とテストについて
プログラミングをやっていると必ずついて回るのは「バグ」だ。バグ一つでシステム全体がダウンしてしまうことも少なくない。
今回は、私の体験をもとに、バグへの付き合い方とテストの手法についてお伝えする。
バグ(Bug)って何?
よく「バグる」という言葉を聞くと思うが、もともとバグは「悪さをする虫」の意味だ。
エラーだけでなく、プログラムが作成者の意図した動きと違う動作をする原因を総称して「バグ」という。
実は、バグにもいくつか種類がある。
- 仕様バグ:仕様の誤りに起因するバグ
例:)軽減税率対象カテゴリの品目は8%になるはずが、仕様書にはすべて10%と記載されており、そのまま実装したら仕様バグになってしまった。 - 実装バグ:プログラミングした結果、テストで見逃されてしまったバグ
例:)持続化給付金サイトで、特定の条件以外申請時にエラーになってしまい、処理が続かなくなってしまった。 - 潜在バグ:実際は機能不全になる処理が、普段殆ど再現条件に当てはまらないため、バグとして顕在化しないもの。
例:)年月日を2099年までしか考慮しておらず、2100年1月1日になると、システムが使えなくなるバグ。
では、なぜバグが生まれるのだろうか?
バグが生まれる原因
バグが生まれる大半の要因は、
「プログラムを書くからバグは発生する」
に帰結するともいわれている。
殆どのケースが実装バグに起因するもので、テスト漏れだったり、テストが難しい条件のために見逃されてリリースされ、その後発見されたものだ。
実際、私もプログラミングは得意ではなく、一般的なエンジニアと比べてかなり多くのバグを出す方だ。
その経験をもとに分析していくと、ある共通項があった。
実行確認していない、または不十分
一番起こりえるケースである。
新規実装もそうだが、修正するときにプログラムのソースコードを見て、問題ないと短絡的に判断してしまうと、しばしばこの問題に直面する。
特にロジックだけ確認し、その処理対象となるデータのことを考慮していなかったりすると、特定条件下でエラーが発生することがある。
また、短納期の案件だと、プログラマは実装に追われてしまい、デバッグやテストを満足に実施できずに納品してしまうこともある。
境界値の確認が不十分
次によくあるのが、境界値の確認漏れだ。
境界値とは、条件分岐(IF文等)で判断される境界となる値とその隣の値であり、仕様書に記載されている境界値をベースにテストするのが一般的だ。
例えば、
- 購入金額が1,000円以上3,000円未満は、クーポンAを発行する
- 購入金額が3,000円以上5,000円未満は、クーポンBを発行する
- 購入金額が5,000円以上は、クーポンCを発行する
といった仕様があったとき、テストケースやテストデータを境界値ごとに作るのが理想だ。
- 購入金額が999円→期待値:クーポンは発行されない
- 購入金額が1,000円→期待値:クーポンAが発行される
- 購入金額が2,999円→期待値:クーポンAが発行される
- 購入金額が3,000円→期待値:クーポンBが発行される
- 購入金額が4,999円→期待値:クーポンBが発行される
- 購入金額が5,000円→期待値:クーポンCが発行される
といった形で、テストケースを作成し、期待値通りになっているかをチェックする必要がある。
ところが、短納期だったりエンジニアの仕様の理解不足が起こると、このようなテストケースがしばしば不十分になり、バグとして残ってしまうことがある。
特に、未来日をテストしずらい日時系の境界値バグは、個人的に見てもかなり多いと思う。
影響範囲の確認をしていない、または不十分
これもよくあるケースだが、バグを修正する(Fixする)時に、新たなバグを生んでしまうこともある。
原因は、修正の影響範囲を十分に確認せずに修正してしまうことにある。
エンジニア目線でいわせてもらえば、「いつまでに修正できる?」という質問に満足な回答できるのは、修正の影響範囲が十分に把握してからになる。
それまでは、回答すら難しいというのが本音だ。
バグをなくすために行うテストとは?
バグを極力減らすためには、どうすればよいのか?テストにはいくつか手法がある。
デバッグ
プログラミングで最も重要なテスト手法はデバッグだと思う。デバッグとは、書いたソースコードを1行1行ステップを踏みながら処理結果を確認していく方法だ。
非常に面倒ではあるが、バグを見つけるには最も確実な方法でもある。
ただし、テストケースが漏れると、せっかくのデバッグも意味をなさないので、少なくとも境界値周りはデバッグでチェックしておいたほうが良い。
単体(機能)テスト
単体テストとは、機能ごとに仕様が妥当かをテストする方法だ。
デバッグと併用したり、自動化(ユニットテスト化)を図って省力化しているところが多い。
条件分岐やループに至るまで、処理の分岐を隅々までチェックするのが単体テストだ。
余談だが、顧客からバグが多いとのクレームがあった場合、単体テストの網羅度が不十分であるケースが多い。
結合テスト(内部結合、外部結合)
結合テストとは、機能間のテストやシステム間のテストを指す。
実際に運用のシナリオに近い形で、システムの機能間の整合性、他のシステムとの連携の正常性を確認するために行う。
APIのテストは、どちらかというと外部結合テストで実施されることが多い。
運用テスト
運用テストは、文字通り実際の運用を想定したテストだ。
場合によっては、運用テストは顧客が行う、そのまま受け入れ試験になることもある。
運用テストは、実は仕様が妥当か否かという側面もあるので、ここで見つかるバグは仕様バグであることが多い。
バグを減らすプログラミング
バグを減らすためには、ある一定のプログラミング手法がある。
例えば、
- テスト(デバッグ)しやすいソースコードを書くように心がける
- 極力ソースコードを書かない。仕様をシンプルにする。
- 出来る限り1つのクラスや関数に処理を複数書かない
といった手法で、バグの発見率をあげてリリース後に不具合を減らすことも十分に可能だ。
特に駆け出しエンジニアの皆さんには、このリーダブルコードを読んでいただくことをおすすめする。