はじめに
UIの不具合を調査していると、
- ある属性を持たないデータでは正常に動く
- ある属性を持つデータでは期待した動作をしない
という傾向が見つかることがあります。
こうなると、
「この属性だから動かないのでは?」
と考えたくなります。
この仮説自体は、調査の出発点として有効です。
しかし、似た意味を持つ属性が複数存在していたり、対象データ以外の状態が処理に影響していたりすると、最初に目についた特徴がそのまま原因とは限りません。
ここで重要なのが、
「Aのときに起きる」という観察結果と、「Aが原因で起きる」という因果関係を分けて考える
ことです。
この記事では、説明用のTypeScriptコードを使いながら、似た属性が複数登場するコードで仮説を切り分ける方法を整理します。
※掲載しているコード、データ構造、名称は説明用に単純化したサンプルです。実際のシステム固有の実装や仕様を示すものではありません。
まず「何の属性なのか」を分ける
例えば、次のような構造を考えます。
type Item = {
special: boolean;
owner: {
special: boolean;
} | null;
};
一見すると、どちらも同じ special です。
しかし、主語が違います。
item.special
は対象データ自身の属性です。
一方、
item.owner?.special
はそのデータの所有者の属性です。
名前が似ていても、意味は同じではありません。
ここを区別せず、
specialがtrueだから、この処理になっている
と考えてしまうと、「何が special なのか」が曖昧になります。
そこで、まず主語を分けます。
対象データ自身
└─ special ?
所有者
└─ special ?
コードを読むときに「誰/何についての値なのか」を整理することで、似た属性を混同しにくくなります。
さらに「操作する側」を分ける
もう一つ考慮したいのが、現在操作しているユーザーです。
同じ対象データでも、誰が操作するかによって利用可能な操作が変わる場合があります。
一般化すると、
対象データ自身
+
所有者
+
操作者
↓
利用可能な操作
という関係です。
例えば、説明用に次のような判定を考えます。
const canPerformAction =
currentUser?.role === "editor" ||
currentUser?.role === "admin";
この場合、対象データだけを見ても「その操作ができるか」は判断できません。
対象が同じでも、
操作者A → 操作できる
操作者B → 操作できない
ということがあり得ます。
つまり、
「この種類のデータでは操作できない」
と考える前に、
「誰が、何に対して、どの操作をしようとしているのか」
まで分けて考える必要があります。
画面で見えた特徴は「原因」ではなく「手掛かり」
不具合を再現したとき、特定の特徴を持つデータで問題が発生していることに気づいたとします。
これは重要な情報です。
ただし、
ある属性を持っている
↓
不具合が発生した
という観察だけでは、
その属性
↓
不具合を引き起こした
とは言えません。
その属性と一緒に、別の条件も変化している可能性があるからです。
例えば、
対象自身の属性
所有者の属性
操作者の状態
APIから返される操作可否
など、複数の値が最終的な挙動に影響しているかもしれません。
そのため、画面上で見つけた特徴は、
「原因」ではなく「次に何を調べるかを決める手掛かり」
として扱います。
変数名だけで意味を決めない
似た属性が複数ある場合、変数名だけから意味を推測すると誤解する可能性があります。
そこで、
この値はどこで作られるのか
↓
何を表しているのか
↓
どの条件分岐で参照されるのか
↓
最終的に何へ影響するのか
まで追います。
例えば、
item.special
と、
item.owner?.special
があったとしても、名前の類似だけでは両者の関係は判断できません。
生成元や利用箇所を確認することで、
item.special
→ 対象自身についての判定に利用
item.owner.special
→ 所有者についての判定に利用
のように整理できます。
変数名を意味そのものだと思わず、「どこから来て、どこへ行く値なのか」を確認する。
似た名前の値が複数存在するときほど、この確認が重要になります。
コード上の可能性と、実行時に起きていることを分ける
コードを読んで、
この条件なら、この処理になる
と理解できても、それだけで不具合の原因だとは言えません。
必要に応じて、ブレークポイントなどを使って実行時の値も確認します。
例えば、
対象自身の属性はどうなっているか
所有者の属性はどうなっているか
どの操作が利用可能になっているか
どの処理まで到達しているか
などです。
ここで重要なのが、
「コード上で起こり得ること」と「実行時に起きていること」を分ける
ことです。
コードから、
条件Xなら処理Yになる可能性がある
とは言えます。
しかし、実際の再現ケースで条件Xだったことを確認していなければ、
条件Xだったから処理Yになった
とは言えません。
後者まで説明するには、実行時の値や処理経路を確認する必要があります。
事実・推測・判断を分ける
不具合調査では、得られた情報を事実・推測・判断に分けると整理しやすくなります。
事実
コードや実行結果から確認できたことです。
例えば、
・処理Aには到達している
・実行時には操作Bだけが利用可能だった
・属性Xと属性Yは別の対象を表していた
・条件Cでは操作Bも判定対象になっていた
などです。
推測
事実から考えられるものの、まだ確認していないことです。
・別の同属性データでも同じ状態になるのではないか
・別の操作者でも同じ結果になるのではないか
・別画面でも同じ条件が使われているのではないか
などです。
判断
確認した事実をもとに、どのように対応するかです。
・どこまでを修正範囲とするか
・どの条件を変更するか
・どこまでテストするか
この3つを分けておけば、
確認できたことなのか
まだ可能性として考えていることなのか
そのうえで、どう対応すると決めたのか
を混同しにくくなります。
特に調査の途中では、推測がいつの間にか事実として扱われることがあります。
情報を分類しておくことで、
「これは確認済みなのか?」
と立ち止まりやすくなります。
「特定の属性でだけ起きる」ときの切り分け方
「特定の属性を持つデータでだけ不具合が起きているように見える」ときは、次の順序で整理すると調査しやすくなります。
1. まず観察した事実を書く
例えば、
属性Aを持つデータで問題が再現した
とします。
この時点では、
属性Aが原因である
とはしません。
2. 仮説として置く
次に、
属性Aが何らかの形で関係しているのではないか
と仮説を立てます。
ここではまだ原因と確定しません。
3. 主語を分解する
似た属性が複数あるなら、
対象自身
所有者
操作者
のように、「誰/何の属性なのか」を分けます。
4. 実際の値を確認する
コードを読むだけでなく、必要に応じて実行時の値を確認します。
どの属性が有効だったか
どの条件分岐を通ったか
どの操作が利用可能だったか
などを確認します。
5. 仮説を更新する
確認結果が最初の予想と違えば、仮説を修正します。
観察
「Aのときに起きている」
↓
仮説
「Aが関係しているかもしれない」
↓
分解
「Aとは誰/何の属性なのか?」
↓
検証
「実際にどの値が使われた?」
↓
判断
「原因として説明できるか?」
最初の仮説が外れること自体は問題ではありません。
仮説を事実として扱わず、検証結果に応じて更新することが重要です。
まとめ
特定の属性を持つデータで不具合が発生していれば、その属性を疑うことは調査の入口になります。
ただし、
「Aのときに起きる」ことと、「Aが原因で起きる」ことは同じではありません。
不具合を切り分けるときは、
- 何を観察したのか
- その属性の主語は誰/何なのか
- 似た属性が他にも存在しないか
- 操作者など別の主体が判定に関与していないか
- 実行時にはどの値だったのか
- どの値が実際の条件分岐に使われたのか
を順番に確認します。
そして、
観察
↓
仮説
↓
分解
↓
検証
↓
仮説の更新
↓
判断
と進めることで、最初に目についた特徴だけを原因だと決めつけることを避けやすくなります。
画面で見つけた特徴は、原因ではなく調査を始めるための手掛かりかもしれない。
そう考えて、事実と推測を分けながら処理を追うことが、不具合調査では重要です。