我々は非公開の関数やメソッドを個別に単体テストするべきなのか?

非公開の関数やメソッドにたいして独立した単体テストを実施するか否かについては、正直なところ「ケースバイケース」と答えるしかない。ただし、ある種の傾向は見られるように思う。

そもそも関数やメソッドにたいする単体テストには、大まかに以下の観点がある。

  • 関数やメソッドの外部品質を検証する。
    • 関数やメソッドの外部仕様を明確化して、仕様に基づいてテストを実施し、仕様を満たしているか否かをテストする。
  • 関数やメソッドの内部品質を検証する。
    • 関数やメソッドの内部実装を元にテストケースを作成して、テストを実施し、中身が健全で問題ないか否かをテストする。

外部品質や内部品質に関するテストをどこまで実施するかは、以下の要素を勘案して決めることになる。

  1. 要求される品質
  2. 開発に使用する言語・フレームワーク・OS等の抽象度
  3. そのソフトウェアが頻繁かつ継続的に書き換えられるか否か

先ほど「ケースバイケース」と書いたのは、これらの要素が可変のパラメータだからである。一方で、開発するソフトウェアの分野に応じて各要素の値に偏りが生じることにより、俯瞰視点では「ある種の傾向」が見えてくる。

要求される品質

要求品質について、少なくとも組み込みソフトウェア(ファームウェアや、ファームウェアに組み込まれる可能性のあるミドルウェア)の開発ぐらいに高い品質が求められるケースにおいては、単体テストにおいて外部品質と内部品質の両方が検証対象となる。

組み込み開発では、コードカバレッジの基準として命令網羅や分岐網羅が挙げられることがある。内部品質が検証対象に含まれているからこそ、網羅条件に関する用語が登場する訳だ。それも、単純に網羅することを目的とするのではなくて、「このテスト条件では、この経路を通過するはずだよね?」という仮説と検証を繰り返した結果としての「命令網羅100%」や「分岐網羅100%」のような基準が設けられている世界である。

外部に公開されている関数やメソッドについて、外部品質だけでなく内部品質も検証するのなら、それらの中身の一部を構成する非公開の関数ないしメソッドについても、詳細にテストする必要がある。詳細にテストするにあたり、非公開の関数・メソッドを個別にテストできると、色々と都合がよい。

開発に使用する言語・フレームワーク・OS等の抽象度

開発に用いる言語については、「C言語ないし『C由来のAPIを頻繁に利用するC++』」とそれ以外の言語の間に大きな壁があるように思う。

C言語では手動でリソース管理する。malloc(3)したらfree(3)するし、open(2)したらclose(2)する。このような環境においては、ある関数において「内部で動的に確保したリソースを、関数終了時に解放する」という仕様を満たしていることを保証するためにも、実行時に関数内で通過する式や文の経路を検証することでリソース解放関数が呼ばれていることを確認する――つまり内部品質の観点での検証を取り入れることになりがちである。

C++においても、ベタにC言語由来のAPIを叩いているなら状況は同じだ。std::mutexにたいするstd::lock_guardのようにRAIIを取り入れる、みたいな工夫も考えられるが、それが面倒なことも多い(しっかりと設計しようとすると、意外と時間や手間がかかるものだ)。

C言語C++以外で業務で使われることが多いモダンな言語では、ガベージコレクションObjective-C/SwiftのARCのような仕組みが導入されている。それらにも苦労するポイントは無くはないのだが、しかし手動でリソース管理しなくてもよくなることで、リソース管理に関して内部品質の観点での検証が不要となる傾向にある。C言語C++と比べて、単体テストにおける網羅条件のプレゼンスは低い。

そのソフトウェアが頻繁かつ継続的に書き換えられるか否か

アジャイルなどのモダンな開発スタイルにおいては、コード・ベースは頻繁かつ継続的に書き換えられる。このような環境においては、単体テストにおいて内部品質に踏み込んだテストコードを書いてしまうと、コード・ベースの書き換えによってテストコードの書き換えも発生してしまう。

このコストをどうとらえるべきだろうか? 律儀にテストコードも書き換えるコストと、ソフトウェアの内部品質が保たれるメリットは、釣り合いがとれているだろうか? 要求品質は開発するソフトウェア次第だ。もしかしたら、単体テストのレイヤーにて内部品質に踏み込むよりも、例えば「単体テストによる外部品質の検証」に「他の何かしらの手法」を併用することで、より低コストで要求品質をクリアできるかもしれない。

そもそも、全てのソフトウェア開発において、コード・ベースが頻繁かつ継続的に書き換えられるものだろうか? 少なくともファームウェア開発においては、一旦リリースされたコード・ベースが頻繁かつ継続的に書き換えられることは非常に稀だろう。

キーポイント:組み込みソフトウェア開発か否か

2024年現在においては、おそらく「組み込みソフトウェア開発か否か」がキーポイントだと言えるのではないだろうか?

組み込み開発ぐらいの高い品質が要求される場合には、単体テストにおいて外部品質だけでなく内部品質の観点での検証も行う必要がある。

特に組み込み開発においては、今でもC言語が多用されている。C++を用いる場合にも、「組み込みC言語」相当の標準ライブラリ機能しか使えない、ヒープ領域を用いる機能を使えない等の制約があり、C言語と同じように手動でリソース管理することも多い。リソース管理の観点においても、内部品質に踏み込んだ検証が有効である。

そして組み込み開発で書かれたコードはあまり書き換えられる事がない。コード・ベースがFixされる環境においては、内部品質に踏み込んだ単体テストを書いても、テストコードのメンテナンスのコストは低い。

以上を勘案すると、組み込みソフトウェア開発においては、単体テストにおいて内部品質に踏み込んだ検証を行うことは有効だろう。その一環として、非公開の関数やメソッドにたいして独立した単体テストを実施することは、悪くないアイデアだと言える。

一方で、一般的なアプリケーション開発においては、単体テストにおいて内部品質も検証することは、目標品質にたいして少々オーバースペックとなる可能性がある(より高水準なテストにおいては、依然として内部品質の検証も重要であるが……)。

開発においては、C言語C++よりもモダンな言語とフレームワークが用いられる。この結果として、大半のシチュエーションにおいて手動でのリソース管理が不要となる。C++を用いる場合にも、モダンC++流のリソース管理を導入することにより、リソース管理の複雑さは低減する。内部品質に踏み込んでリソース管理に関する検証を行う必要性は低減する。

アジャイル等のモダンな開発スタイルが導入されることも多く、コード・ベースは頻繁かつ継続的に書き換えられる。そのため、内部品質に踏み込んだ単体テストを書いてしまうと、テストコードをメンテナンスするコストが高くついてしまうだろう。

色々と考えると、一般的なアプリケーション開発においては、単体テストにおいて内部品質も検証することは、コストとメリットの釣り合いが取れているとは言い難い。単体テストでは外部品質の検証をメインとして、単体テスト以外の手法(テストだけでなく内部設計も含む)を併用することで品質を確保する、という方針をとるべきだろう。内部の「詳細な実装」の一部である非公開の関数やメソッドについては、独立した単体テストを実施するべきではない。

今はこんな感じだが、環境の変化によって今後変わる部分も多々あるだろう。特に組み込みソフトウェア開発においては、C言語C++に代わるモダンな言語(今はRustが注目されているのだろうか?)の採用や、組込みLinuxなどでユーザランドで動かすソフトウェアを書く機会の増加など、単体テストの実施方針に影響を与えそうな要素が見え隠れしているように思う。