自称問題解決王の僕がバグフィックスのコツを言語化します

[Increments × cyma (Ateam Inc.) Advent Calendar 2020](https://qiita.com/advent-calendar/2020/increments-cyma) の4日目は、株式会社エイチーム EC事業本部の@bayasistが担当します。

## 初めに

ソフトウェアの不具合の解決は非常に重要なスキルだと考えています。
開発環境の中であれば、最初から完璧なソースコードが書けない限り、開発してバグを埋め込んで、見つけて修正しての繰り返しではないでしょうか。
そのため、不具合の解決は個人のソフトウェア開発を高速化するうえでとても重要なスキルと言えます。

また、本番環境に出てしまっている不具合であれば、場合によってはサービスの運用できない状態に陥ったり、会社の存続の危機につながったりする可能性も十分にあり得ます。
そのため、不具合の解決に向けてスピード感と正確性の両方の側面から高いスキルが求められます。

このように不具合の解決は非常に重要なスキルであることから、不具合の解決が早く・正確にできるような知識を私の経験を踏まえてまとめようと思います。

## 不具合を検知したときにやること

早速ですが、不具合を検知したときには下記の順で行っていきます。

* 事実確認
* 仮説立て
* 仮説検証
* 修正

それぞれ順を追って説明していきます。

### まずは事実をちゃんと確認しよう

まずは不具合の事実をしっかり確認します。
具体的には、「どういう環境(端末等)で」「どういう時に」「どういう操作をすると」「どういう現象が発生するのか？」が分かっていればよいです。「自転車Aの在庫があるときに」「自転車Aをカートに追加しようとすると」「500エラーが起こる」などです。
そしてできる限り開発環境でその不具合を再現させてください。上記の情報が分かっていれば大体の時は再現できるはずです。

聞いた情報を鵜呑みにして調査を始めると認識の齟齬が生まれる可能性が高くなり、不要な調査をしてしまう可能性があるので、必ず自分の目で不具合を確認する努力をしましょう。
どうしても自分で確認ができない場合は、できる限りほかの手段を使ってでも詳しく確認します。
不具合を踏んだ人と連絡が取れるのであれば、詳しくヒアリングをするなど、様々な方法を駆使してください。

この段階で「どういう環境(端末等)で」「どういうときに」「どういう操作をすると」が明確になっていなくとも、「どういう現象が発生するのか？」は確実に明確にしておいてください。
「どういうときに」「どういう操作をすると」はできれば明確になっていた方が好ましいですが、次の仮説立ての時に気づく場合もあるので、難しければいったん先に進めても大丈夫です。

### 仮説を立てて検証しよう

事実確認ができたら次に、何故そのような不具合が発生するのかの仮説立てをしていきます。
その不具合を起こしてしまいそうなミスなどをできる限り考えていきます。あくまで仮説なので間違っていてもよいです。とにかく沢山出していくことが大切です。
例えば以下のような感じです。

- XXXの設定を本来YYYにするはずをZZZにしているのではないか？
- XXXメソッドを呼び出すときの第n引数の値を間違えてないか？
- データベースのXXXテーブルにデータがないときに、メソッドYYYが思った動作をしてくれないのではないか？

そして、上記で立てた仮説の正しさを実際にプログラムを動かしながら確かめていきます。
例えば、メソッドの引数が間違ってないか？という仮説であれば、プログラムを一行ずつ実行して変数の中身を確認したり、ログを吐いたりし、本当にメソッドの引数が間違ってないかを確かめていきます。

### 仮説が立たないときのコツ

仮説立てが最も重要かつ、最も難しいポイントとなるかと思います。
仮説立てがうまくいかないときに考える手順やコツをまとめていきます。

#### 不具合を実行順とは逆から追ってみよう

開発環境で不具合を再現出来ていて、一行ずつ実行したりができる環境の時に有用です。
例えば、下記のプログラムでAと表示されるはずがBと表示されてしまう不具合があったとします。

```php
01: $b = ....;
02: $c = ....;

// 中略

11: $a = $b + $c;

// 中略

21: print $a; // Aと表示されるべきなのにBと表示されてしまう
```
この場合、変数aに意図しない値が入っていることになります。変数aに値を入れているのは11行目。すなわち、変数bかcが意図しない値が入っていないことになります。
なので、11行目でプログラムを止めてみてbかcのどちらに意図しない値が入っているかを確認しましょう。bが意図しなければ次は1行目を見てみる。。。。というように逆からプログラムを追っていきます。

#### 関数などの中身がブラックボックスの場合

上記でほとんどの仮説は立ちますが、まれに難しいことがあります。
ライブラリやフレームワークで用意されたメソッドの結果が思い通りにいっていなく、その理由が分かってない場合などが当てはまります。

```php
$b = ...;
// $bは調べたら想定通りのデータが入っていた
$a = hogehoge($b);
// $aが想定外のデータだった
print($a);
```
上記の場合、hogehoge関数が自分が作った関数でなければこれ以上掘り下げて調査はできなくなります。hogehoge関数内に不具合がある可能性は否めませんが、有名なフレームワークやライブラリの関数であれば不具合の可能性は少ないです。自分が仕様を勘違いしている可能性があるので、まずは関数hogehogeについてのマニュアルをもう一度深く読み直してみましょう。

またhogehogeが別のシステムを呼び出すような関数であれば、どのように別のシステムを呼び出しているか確認してみるとヒントになります。
hogehogeがデータベースを呼び出す関数であれば、DBMSから実行されたSQLを確認したり、外部のWebAPIを呼び出すような関数であれば、通信内容を確認してみたり、システムコールを呼び出す関数であればどのようにシステムコールが呼ばれているかを確認してみるとよいでしょう。
大概、外部のシステムとのデータのやり取りを見ていると不具合に気づけます。

またhogehogeが外部のシステムとの連携ではない関数の場合は上記の手法が利用できないため、hogehogeがオープンソースのライブラリなどによって用意されているのであれば、ソースコードを追っていくのは一つ手法として持っておくとよいでしょう。ただし、オープンソースのソースコードを読むのには時間がかかることが多いため、優先順位は下げることが多いです。そこまでしなくとも解決することが大半です。

#### それでも仮説が立たないときや、開発環境で再現できないときは

今まで書いていった方法を進めていっても仮説が立たなかったり、開発環境で再現できなかった場合、開発環境と本番環境が異なるがゆえに再現できず、仮説立てを困難にしている可能性があります。主に開発環境と本番環境では下記の違いが生まれていていることが多いです。それぞれ解決方法を解説していきます。

##### データの違い

開発環境と本番環境でデータベースなどのデータが異なることによって仮説立てを困難にしている可能性があります。
まずはできる限り開発環境のデータを本番環境に近づけてみましょう。(個人情報とかは開発環境にもってきては駄目ですよ)
どうしても近づけることができない場合は、データが違うということを意識して再度プログラムを読み直してみましょう。
特に例外的な処理が漏れていたりしませんか？

* データが0件だったときの挙動はあってるか？
* キャンセル注文など普通とは異なるデータの扱いはあってるか？

このような例外処理に注意をしながら再度ソースコードを読み返してみましょう。

##### 設定や環境の違い

設定や環境の違いによって開発環境と本番環境のプログラムの動作の違いが出てしまい、それが開発環境での仮説立て・再現を困難にしていることがあります。
OS・利用しているソフトウェアのバージョンの違いや、HTTP, HTTPSなどプロトコルの違い、キャッシュをするしないの設定など、いろんなレイヤーでいろんな違いがあるかと思います。
まず、開発環境と本番環境の違いを洗い出してみて、そのうえでできる限り開発環境と本番環境に近づけて実行してみてください。そうすると開発環境で再現でき、仮説立てができるようになるかもしれません。

##### 同時接続数の違い

本番環境は沢山の人が見てくれますが、開発環境は基本的には開発者一人のみが見ると思います。複数人が同時に操作することで初めて起きる不具合もありますが、このような不具合を開発環境で再現することは困難です。
まず、複数人が見ているということを前提に、プログラムを見返してみてください。そのうえで主に下記の二つのミスがあることが経験上多いので、注意して見てください。

###### キャッシュ使用のミス

DBの実行結果などをサーバーにキャッシュをしたりするときに不具合を起こしていないでしょうか？

```php
function getData($inputA, $inputB) {
  $cache_key = create_cache_key("getData", $inputA); // ここで間違えていませんか？
  $cache = fetch_cache($cache_key);
  if($cache) return $cache;
  // 中略
  set_cache($cache_key, $data);
  return $data;
}
```
上記の$inputA, Bがユーザsessionのみによって決まるような構造だと、一人で開発環境にアクセスしていても再現ができないことがあります。
ABテストで、Aの人が作ったキャッシュをBの人が見るとエラーになるなどありませんか？そのような視点で再度プログラムを見返してみてください。

またキャッシュする気がなくても、キャッシュをされているなんてことはありませんか？例えばRuby on Railsをpumaで起動していると、クラス変数はリクエストを跨いでも初期化されなかったりします。([詳しくはこちら](https://qiita.com/suhirotaka/items/3d4e5116e6dc9dcb6e3a))外部のライブラリを用いた実装でも意図せずにキャッシュを利用していたということも過去にありましたので、キャッシュされていることが疑われるときは念のため確認してみてください。

###### DBのロックのミス

多くのDBMSのデフォルトのトランザクションの分離レベルはREAD COMMITTEDです。そのため複数人が同時実行をすることで、ノンリピータブルリードやファントムリードが発生して不具合になる可能性があります。これを防ぐためにテーブルや行を事前にロックをする必要がありますが、ロックが適切にされているかを疑ってみてみると、仮説が立つかかもしれません

```php
transaction();
$count = execute("SELECT quantity FROM stocks WHERE id = ?", $id);
execute("UPDATE stocks SET quantity = ? WHERE id = ?", $count + 1, $id);
commit();
```

上記がダメな理由が分かりますか？
stocksのquantityが10だったとき、上記を実行すると下記のようなSQLが実行されます。

```sql
UPDATE stocks SET quantity = 11 WHERE id = xxx;
```

2つ同時にアクセスがあり、上記のクエリが2回同時に発行されたらどうなるでしょうか？
2回実行したらquantityが12になることを期待しますが、11にしかなりませんので、不具合につながることがあります。
この場合であれば、以下のSQLが実行されるようにする必要があります。

```sql
UPDATE stocks SET quantity = quantity + 1 WHERE id = xxx;
```

このようなSQLを発行するための記述などは、言語やフレームワークによって異なるので割愛させてください。


### あとは修正しよう

仮説が立って、そこが実際に間違っていることが検証できればあとは正しく直すだけです！頑張りましょう！(雑)

(雑なのは修正の仕方はケースバイケースすぎるので書けないだけです。。。)

## 次回予告

[Increments × cyma (Ateam Inc.) Advent Calendar 2020](https://qiita.com/advent-calendar/2020/increments-cyma) の5日目は、Increments株式会社の@xrxoxcxoxさんがお送りします。