Software Design (2024年2月号) 輪読&座談会に参加して

# 前置き
先日参加してきた、Software Designの２月号
「テストの設計してますか？」のイベントでのディスカッション内容を通して
感じたことをここに残す。
その中で、テストスキルが高いとはどういう状態か？
よいテストを設計するにはどういう思考プロセスが必要か？
という議題が出たので、そこでの内容と自分なりの見解をまとめました。

# テストスキルの高さ
テストスキルの高さは以下の項目で構成されているよねという議題があった。
自分は主に設計原則とかを項目として挙げて、
ブロッコリーさんたちは、テストという観点での思考やTDDについて言われてらした。
### 関心の分離
抽象度を揃えながら、違う抽象度のものが混在しないようにするスキル。

### 設計原則を当たり前に体現できていること

### 具体と抽象スキル

### ハイリスクな所とローリスクな所の濃淡に対応して戦略的テスト
これは丁度先月適した内容の記事を書いたので、そちらを参照ください。

https://qiita.com/Kudo_panda/items/3c91887dcd7775893339

### テストという観点での各フェーズの検証


# 
では、上で挙げた項目を１つずつ掘り下げていく。

# 縦の関心の分離ができている
## テスト分析
要件定義のテストに対応し、「何をテストするのか？」
を考えるべきフェーズである。

たとえば要件定義で、重要なあるユースケースの優先度高い非機能要件として、
高いセキュリティ(レベルを10とする)が求められており、
正常フローの場合には、レスポンスが返ってくるまでの時間が0.5s以内
という指標があった際に、
実際に0.5s以内であるのか？ということを検証することになる。
一般に高いセキュリティを求めると、速度が落ちる傾向あることから、
果たして本当に、トレードオフ関係にあるこの２つの水準を共に満たせるのか？
を検証するということを考えるのが、このテスト分析。
ここには、その仕組みをどのように設計するか？という内容は一切含まない。
こんな風にテストの仕組をつくるとか、フレームワークは何を使うとか
って書かれてる時点で、他の関心事がテスト分析に混ざっている。

## テスト設計
設計部分のテストに対応し、テスト分析で定義したものをどのようにテストするか？
を考えるところである。
もともとのプロダクトの方のクラスやコンポーネントが
メンテ性に優れている構造をしていれば、
その大きさの粒度に合わせて何を検証しなくてはいけないのか？
が自ずと見えてくる。


ここまでのテスト分析とテスト設計は、
自動テストなどの対象外のスコープなので、この２つのフェーズはめっちゃ重要。
もちろん、一度のサイクルですべてをテストしきることは難しいが、
継続的にイテレーションサイクルを回しながら、
見落としていたテストすべき箇所を次のサイクルのテスト分析で明らかにし、
ということを繰り返す。
<img width="500" alt="1cd02-model-2.webp (26.9 kB)" src="https://img.esa.io/uploads/production/attachments/5827/2024/02/10/135213/a4917323-b55c-4d5d-bc2f-fcfee14eeb1c.webp">


## テスト実装
実装のテストに対応し、テストの実行に必要なものを用意したか？

## テスト実行
実装のテストに対応し、実際にテストスイートを実行。
このテスト実装と実行のうち、頻繁にテストしないといけないものは
どんどん自動化して生産性低下を防がないといけないです。

またテスト設計・実装では、仮にテストが失敗した際に、
なぜ通らなかったのか？を学習しやすい内容にしておかないといけないです。

## ここまでのまとめ
各フェーズに他のフェーズの関心は混ざっていないか？を考えていないと、
この縦の関心の分離ができていないと、
テスト分析にテスト設計の内容が混ざっていたりして、
あとからテストの設計を変えようとした際に、
テスト自体の構造の変更がしにくいというリスクをはらんでしまう。

またイテレーションを繰り返す中で、
全体へのリスクも低く、変更頻度が落ちてきて安定してきたテスト部分については、
分析すべき内容だけは把握しておいた上で、あえて設計はしないなど
テストのメンテコストを抑えるなど考えていく。

そして思うのは、やはり要件定義・設計を行う人自身が、
何をテストすべきなのか？　を把握しておくことが非常に重要である。

# 設計原則
SOLID原則と、コンポーネントの6大原則は、
テストアーキテクチャを考える際にも非常に重要である。
プロダクトコードの方は、綺麗に整理されまとまっているのに、
テストの方は、ぐちゃぐちゃでは話にならない。
そのままではテストが技術的負債のボトルネックとなり得る。
そうならないために代表的なものだけここに記載しておく。

## 単一責任の原則
１つのテストクラスがなぜか責務過多になっているとかっていうのは、
テストの観点での単一責任原則に違反する。
プロダクトコード側が単一責任原則を満たすようにしているなら、
同様にテストの方も対応させておくのが望ましい。

## 開閉原則とリスコフ置換原則
他にもプロダクトコードが頻繁に変更される場合に、
テストコードと直に繋いでしまっていると、当然テスト側は直に影響を受ける。
それに伴い、そのテストクラスを使用してる他のテストも変更しないといけなくなるかもしれない。
結果的にテストを変更できない→　プロダクトコードに変更を加えること
を恐れるようになるという構図を生み出しやすい。
なので、以下の図のように間にインターフェイスをかまして、
変動からの保護をしてあげる必要がある。
<img width="500" alt="検証.png (18.5 kB)" src="https://img.esa.io/uploads/production/attachments/5827/2024/02/10/135213/dfe40b91-b163-4795-8aa0-bbd885fd2e1f.png">
この際には、勿論図のようにテストAPIになっているインターフェイスを
リスコフの置換原則を満たすようにテスト実装は存在していなくてはいけない。

## DRY原則
役割・概念が一緒のテストが存在してしまっていると、この原則に反する。
その分、テストの実行コストもかかるし、
何よりテストを変更しないといけなくなった場合に、単純計算倍になるし、
変更のし忘れリスクも存在する。
そのため、完璧なMECEである必要はないが、できればMECEを意識して、
余計な重複がなく、抜け漏れのないテストをすることを意識しておきたいところ。

## コンポーネントの6大原則
テストも意味のある単位でまとめられていないといけない。
ここでは端折るので、コンポーネントの原則記事を参照ください。

# 具体と抽象スキル
パッケージというわりとミクロな単位のテストなのか？
それともよりマクロなサービスという単位でのテストなのか？
それとも複数のマイクロサービスが連携したServicesとしてのテストなのか？
Servicesとしてのテストが通らなかった
　→　どこが通らなかったんだろう？と１段階拡大。
　→　あ、ここのサービスで通らなかったのか。じゃあそこを拡大。
というように全体と部分を行き来できる具体と抽象スキルはテストアーキにも
重要なスキルであると思っている。

これは後述の状態遷移にお繋がる話である。
マクロに見てどんな状態か？　ミクロに見てどんな状態か？ということ。
ようは、状態という観点で具体と抽象を行き来できるかってこと。

# 

# 状態遷移テスト
無効なも含めて、全ての遷移を把握しておくことが重要。
たとえば、計測中という状態から　停止中への移行は存在しない
にもかかわらず、予期せぬ変な処理の呼び出しが発生して、
あってはならない状態遷移が起きてしまいかねない。

これは個人的に契約による設計に紐づいてくると思っている。
なんか知らないけど、ある処理を呼び出したら変な状態遷移してしまうとか。
【契約による設計】に関する詳細記事はここでは省くが、
特にマイクロサービスとかでは、重要な考え方のように感じる。

というのも、個々の個別のマイクロサービスは正常に動くにもかかわらず、
他のマイクロサービス量子と連携させるとなぜか障害が発生するとか、色々な問題が起きることがある。
この理由の１つとして、連携した全体としてのテスト分析と設計が
必要十分になされていないことが要因だと私は考えています。

あるサービスAから　サービスBのある処理の呼び出しをする際に、
Bの状態がどのように遷移することが許されているのか？
良からぬ遷移をしていたらすぐにアラート出すなりのことを仕込んでおく。

## 組織構造における状態モデル
これは組織においても同様だと考えられます。
あるチームAから他のチームBにメッセージを送ると、
普段は正常なレスポンスが正常範囲内の時間内に正確な情報で返ってくるのに、
日によってはめっちゃ遅かったり、ということ。

容易にいろいろ想像つくと思いますが、
この際にチームBはめっちゃ認知負荷の高いことをしていたり、
もしくは半分以上が体調不良で休んでいてチームとして機能できていなかったり、
こういったことが理由で、Aに対する返答が遅くなるという現象。

要は【状態】という事前や事後条件の判断材料になるものを通して、
なぜ問題が発生したのか？を類推しやすくしておく。
そうすればちょっと待てばチームBは自己回復し、目的を達成できるのか？
それともそもそも今の制約上では無理だから別の方法探さないといけないのか？
判断もしやすくなりますよね。

これと一緒のメカニズムのことが、状態遷移テストに求められていると感じます。
その重要性はマイクロサービスのような場合には、さらに重要性が増すでしょう。

# テストという観点での各フェーズの検証
どうやったらこれはバグが出るだろうか？
というテストが通らないようなケースを考えるスキル。
これは個人的には、ある意味クリティカルシンキング力が高い人ほど優れていると思う。

また事前にバグが出ることを想定したテストを書いておくことで、
強制的にプロダクトコード側で求められる品質の水準になるコードを書く。

プロダクトコード書く側からしたら、
「この設計なら通るっしょ」という前提を置いた上で、
テストによって検証をし、無事に通れば今の設計でOK！
通らなければ今の設計では求められる水準に達していないから再設計。
という所に、自分は演繹法的思考プロセスを感じた。

なので、クリティカルシンキングで違った観点からバグが出るような状況を考え、
TDDで事前にテスト設計。
そして演繹法による検証で、プロダクトコードの品質を保たせる！！