もう大規模開発で詳細設計書は作れない。単体テストコードを詳細設計と再定義しよう

こんにちは。てぃろです。

大規模ソフトウェア開発の品質を考える第二弾です。第一弾と思ってる記事はコチラです。

今回は特に、どうすれば大規模開発においてデグレを防いだ開発を進めることができるか?という課題について現実的な話を考えていきたいと思います。

以下では、ソフトウェア開発では当たり前と思われる内容も多少かみ砕いて順番に議論を進めたいと思います。多少冗長な部分もあると思いますが、一つの読み物としてお付き合いいただけると幸いです。

議論の前提

本記事で扱うソフトウェア開発プロジェクトは以下のような状態であることを前提としています。

  • 下請け会社を活用した大規模開発である
  • 工数はひっ迫しており、品質確保にあてる潤沢な工数はない
  • CIによる自動テスト環境は存在する

このような前提の中で、どのようにデグレを防ぎながら開発を進めればよいのか?を考えたいと思います。

なぜデグレを防ぎたいのか

デグレとは、デグレード(degrade)の略で、ここではある修正によって意図せずアプリケーションの挙動が変わってしまうことと定義します。より狭義な言い方をすると、ある修正の影響によってある関数やメソッドの返り値が変わってしまうことと言えます。

わざわざ言うまでもありませんが、デグレを防ぎたい理由はアプリケーションが意図しない挙動をする危険性を排除したいからにほかなりません。

しかし、デグレが起きることそのものを防ぐことはほとんどの場合できません。大規模な開発になるほど修正の影響は広範囲に及ぶため、修正の影響を見切ることは非常に困難になるからです。

ならば、デグレは起きるものとして考え、そのデグレをちゃんと潰すという方向で検討を進めます。

デグレを防ぐには、デグレを検知する必要がある

先述の通り、デグレは必ず起きます。しかし、それをちゃんと潰していけばアプリケーションの品質は保たれます。

ならば、そのデグレがアプリケーションに残らないよう、デグレの発生を検知する必要があります

ここでデグレの定義に戻ると、デグレとはある修正の影響によってある関数やメソッドの返り値が変わってしまうことですから、デグレ発生の検知とは、メソッドの返り値の変化の検知と言い換えることができます。

メソッドの返り値を検証する方法といえば、単体テストです。つまり、デグレ検知には単体テストを活用します。

単体テストはこれ以上デグレさせないためのテストと定義する

ここで一度単体テストの定義を確認します。

一般に言われる単体テストは、プログラムを構成する比較的小さな単位(ユニット)が個々の機能を正しく果たしているかどうかを検証するテストと考えられています。

言語やそのプロジェクトの考え方次第ですが、比較的小さな単位というのは、たいていの場合、クラスやメソッド(関数)を意味します。つまり、単体テストとはクラスやメソッドの機能を検証するテストと言えます。

では、単体テストコードはどのようにして作るものなのでしょうか。

単体テストはクラスやメソッドの機能を検証するテストなので、クラスやメソッドの機能として何が正しいか定義したモノが必要になります。その定義したモノとはクラスやメソッドの機能を定義した設計書です。詳細設計書と言われることも多いです。ではその設計書とテストコードがどのような関係にあるかというと次のようになると考えられます。

設計書をもとにした理想的な開発の成果物の関係性

つまり、設計書をもとにしてソースコード(プロダクションコード)を作成。正しい機能の検証をするために設計書の定義を参照して単体テストコードも作成。ソースコードと単体テストコードを合わせて単体テストを実行し、テスト結果(デグレ検知)を得る。

これは非常に理想的な方法論です。

現実はどうかというと、多くの場合設計書にあたるドキュメントが存在しないまま開発が進行しています。当然ビジネス上必要な要件定義書や概要設計書が存在していることは多いことでしょう。しかし、クラスやメソッドの機能や引数、返り値のパターンまですべて網羅した設計書(以下、詳細設計書と呼ぶ)を作っている現場は稀だと思います。

なぜそのようなことになるかというと、そんな詳細設計書を作っている工数はなく、そのような時間があるならさっさとソースコードを開発したほうが早いからです。(※このような状況になってしまった理由の分析は後述します)

ではそんな状況で単体テストを作ろうとするとどのように作ることができるのか?それは以下のようにするしかないのです。

十分な設計書が存在しない現実の成果物の関係性

先述した通り、ソースコード(プロダクションコード)だけが存在しているので、そのロジックや取りうる引数の型や返り値を分析して単体テストコードを作るしかありません。TDD(テスト駆動開発)でもしていない限り、単体テストコードがソースコードより先に存在していることはありませんから。

ここで重要なのは、ソースコードがテストの対象なのにそのソースコードを正しいものとして単体テストコードを作成している、という点です。

さらに思い出していただきたいのですが、本議論の目的はデグレを検知する = 意図しない機能の変化を検知することにあります。つまり、デグレを検知するという目的においては現在のソースコードの挙動を正しいものとしてその挙動を保証する単体テストコードを作成することはむしろ理にかなった方法と言えます。

さらに言えば、この現実的な単体テストコード作成の方法から考えて単体テストとはデグレを検知するための装置であると再定義できると考えています。

そうすれば、自動テストでマージするごとに単体テストを実行することによって、自動的にデグレを検知することができるようになるというわけです。

詳細設計書 = 単体テストコード

しかしながら、詳細設計書(クラスやメソッドの機能を定義した設計書)が不要かと言われると、そんなことはないと考えています。何が意図した挙動であるかを定義しておかないと障害が発生した際にどのように修正すればよいかわからないからです。

しかし、十分な設計書を作る工数がないことも現実です。

であれば、単体テストコードを詳細設計書と定義することがよいのではないかと考えます。

ここまで見てきたとおり、単体テストコードはソースコードを正しいものとしてその機能を保証する形で作られています。つまり、単体テストコードを見ればソースコードに期待される挙動を把握することができます。

このように単体テストコードを詳細設計書とみなした場合、設計書を作成する手間がまるまるなくなると思われるかもしれませんがそうではありません。実際には設計書相当とは言わないまでも単体テストコードにコメントを入れるなど多少のドキュメンテーションをすることが望ましいです。やはりコードだけではその設計の背景や意図まで伝えることはできないからです。

また、入力と出力のパターンや状態遷移が非常に多いクラスやメソッドについては、それらのパターンを網羅をする必要があるため、その整理をした設計書くらいは書いたほうが良いかと思います。

時代に合わせて詳細設計と単体テストの再定義が必要なのではないか

今回こんな記事を書いた理由がここにあります。ウォーターフォール開発における詳細設計と単体テストの定義をし直す時期に来ているのではないか、と考えたのです。

このような議論を始めると「ウォータフォールが時代に合わないんでしょ?そもそもアジャイルにすればいいのでは?」という意見が出てくると思います。しかし私はすべての開発をアジャイルでしなくてよいと考えています。

アジャイルは、要件が決まり切っていない場合に柔軟に仕様変更を行うことを可能とする考え方です。対してウォーターフォールは、要件が決まっている場合に着実に段階を踏んで開発を進めるという考え方です。

上記の通り、アジャイルとウォーターフォールは性質の違う考え方であり、どちらかに寄せる必要はなく場合によって使い分けることが望ましいと思います。しかもアジャイルだからと言って詳細設計書や単体テストの開発をしなくてよい、と決められているものではありません。

話を戻します。

詳細設計書をつくり単体テストを実施し…という理想的なウォーターフォール開発ができたのは、昔の開発が今より格段にシンプルだったからだと私は思っています。

過去の大規模開発といえば、以下のような特徴で語れるのではないかと思います。

  • 開発対象は既存業務をシステム化すること = 開発要件が明確
  • 画面がリッチではない = 画面制御とそれに関連する複雑な状態遷移ロジックが少ない
  • 開発言語自体の機能が(今に比べて)乏しい = ロジックがシンプルになりやすい
  • マシンの性能が低くやれることに限界が多い = 要件が少なめ

このような点から、現在に比べて設計の複雑度は低く設計書も単体テストコードも十分に書ける量で収まったのではないかと考えています。

しかし、現在の開発言語は非常に機能が多様(マルチスレッドや非同期処理、複数言語対応など)で、画面もアニメーションが入るなどリッチになっています。そのわりに工数見積もり手法は更新されておらず、見積もりの担当者や顧客は過去の大規模開発をベースに考えるため納期は相対的に短くなる傾向にあるのではないかと考えています。

以上から、設計書を作成する時間が十分に取れないという事態に追い込まれてしまうのではないかと思っています。

つまり、過去理想的とされた設計書を作成するウォーターフォール開発は現在においては実行不可能な開発方式であると言わざるを得ないと考えています。

それでもなんとか高い品質を維持した開発を進めたい。総合テストのフェーズから品質を上げたい。そのために修正を入れなきゃいけないけどデグレが怖い。ならばまずデグレを検知して品質を維持しよう。こういう切り口から詳細設計書と単体テストに関する今回の議論に至りました。

総合テストフェーズまで進んだ開発で品質が悪いとしたら、それはマネジメントや開発メンバーの誰かが悪いという話ではありません。納期を短くする顧客が悪いという話でもありません(ビジネス上必要な締め切りであることがほとんど)。開発方法が間違っている可能性が大きいと思っています。

会社で進められる開発はウォーターフォールを前提とした管理がされることがまだまだ多いです。ひとたび品質分析をされると「詳細設計は作ったのか?」という指摘がされがちです。しかしその指摘は現実的ではありません。詳細設計を作る工数がないから作っていないのですから。

品質分析をする側もこのような型にはまった指摘ではなく、ソフトウェアの品質を維持するという目的のもと現実的な指摘をしなければなりません。

そのためには現実と合わない過去の方法論は捨てて、現実的に実行可能な方法論に更新する必要があると思います。

つまり、今回の議論では、上記のような時代背景の中デグレを検知して品質を維持するということを目指したところ、詳細設計と単体テストの作り方や定義を更新するべきだ、という結論に至ったのです。

品質分析を会社に説明するには

それでも会社が品質を判定する基準について過去の方法論をベースにしている場合には、そこに合わせた説明をしていく必要があります。大規模開発をする場合には避けて通れない道です。

今回ご紹介した詳細設計 = 単体テストコードとして定義するのは従来の考え方と少し違いますので、なんらかの説明が必要になると思います。もし私が説明をするとしたら、という例を示しておきたいと思います。

  • 詳細設計書は、単体テストコードとして作成しているが、十分な記述量を確保している
    • コードだけでなく十分なコメントを残しており、従来の詳細設計書相当の記述をしている
    • 入出力網羅が必要な場合には別途ドキュメントを作成し、テストケースの網羅性を確認している
  • デグレ検知を十分にできるだけのテストケース数がを確保している
    • テストケース密度(= テストケース数 / ソースコード行数)とバグ検知数やデグレ発生件数で分析し、テストケース密度が増えるほどバグ検知数やデグレ発生件数が減っていることを示す
    • つまり、十分品質維持が可能であると示している

まとめ

今回は詳細設計と単体テストの再定義について考えてみました。

そもそもなぜこんな議論をしようと思ったかというと、品質分析等を仕事で担当していく中で現実的な品質向上施策を考えていたからです。これでもまだまだ汎用的ではなかったり、完璧に品質保証が可能な方法として定義しなおせた、ということではないと思っています。

最初から品質問題が発生しないようにスケジュールを調整するという方法もあると思いますし、むしろそれがあるべき方法だとも思います。ですが、そうならないのが現実というもの。どのように無理難題を克服し顧客の要望に応えるかということを考えるのがプロなんじゃないかと思います。

そのために方法論が間違っているならそれは変えていかねばならず、そのための議論は絶えず続けていかねばならないものだと思っています。

でも、確実にできないことは断らないといけないですけどね!