To mock, or not to mock, that is the question

さて、テストコードなんて書きたくなかった私ですが、世の流れには逆らえず今はせっせとテストコードを量産しています。 開発完了=試験完了=出荷可能が求められる忙しない世の中でありますから。

目下開発しているのは Kubernetes 向けのネットワークソフトウェア Coil のバージョン 2 なんですが、この開発では main 文以外はすべて自動テストする徹底ぶりです。他にも近年様々にテストコードを書いてきた過程で、以下の知見を得るに至りました。

  1. 外部依存はなるべく実物を使う
    • etcd を使うなら etcd を用意、ネットワークをいじるなら network namespace を用意、...
  2. 内部依存はなるべくインタフェースで依存注入する

多分この結論に似たことはあちこちで言われている気はするのですが、結論に至った理由が大事と思うため、以下少し書きます。 あ、以下モックと言ってる用語は専門的には stub だったり test double だったりするかもしれませんが、細かいことは気にしないでください。

外部依存

外部依存というのは、同一のプログラミング言語内で完結しない外部のシステムのことです。

例えば etcd を使うプログラムをつくるとして、始めはインタフェースを切って etcd を使う実装とテスト用のモック実装を用意して、とやってみたのです。 その経験はお世辞にも良いとはいえませんでした。

  • モック実装が etcd のふるまいをきちんと実装できてない
  • ⇒ モック実装のみ、テストが失敗する
  • ⇒ モック実装を頑張って充実する

... 俺、何がしたかったんだっけ、という気持ちになります。逆もあります。

  • モックの実装がいい加減で通すべきでないテストを通してしまう
  • ⇒ 実物の etcd では動作しない
  • ⇒ モックと実装を直す

はい。害悪ですね。

外部依存が例えばファイルシステムなら、あまりモックを作ろうとか言わずファイルシステムを利用してテスト書く場合が多いでしょう。 そういう経験を経て、どうしても用意ができないとか、用意するにはあまりに時間・金がかかるといった事情がない限り、外部依存は極力実物を用意するようになりました。

内部依存

内部依存とは、同じ言語で作られているプログラムの部品間の依存です。外部依存と違うのは、内部依存は実物を用意するのが極めて簡単なことです。 例えば以下のような部品があると考えてください。

  • DB と通信して IP アドレスを管理する部品 A
  • OS 上でネットワークを設定する部品 B
  • gRPC でクライアントのリクエストを受け付け、A, B を呼び出す部品 C

まあこれ、つい最近実際書いた処理なんですが、ここで C に実物 A, B を注入して試験するとどうなるでしょう。

... 答えは gRPC のリクエストを処理中に DB にアクセスし、OS のネットワークをいじる試験をすることになります。

DB にアクセスするには先に書いたように実物の DB を用意するでしょうし、OS のネットワークをいじる試験は root 権限が必要そうです。 部品 C で検証したいのは gRPC サーバーの動作だけなのに、なんとも大袈裟ですね。

そこで部品 A, B のインタフェースを用意し、C のテストでは A, B のモックを使えば、gRPC サーバーの動作だけに専念できるようになります。 もちろん、A, B の実物もそれぞれにしっかりテストをします。結果 root 権限を要求するのは部品 B のテストだけになります。

このように、内部依存では実物がすぐ用意できる誘惑に負けず、それぞれの部品の関心事のみをテストできるようにするとテストしやすくなります。

まとめ

というわけで、テストコードを書くにあたって以下の方針をとるに至った経験を書いてみました。

  1. 外部依存はなるべく実物を使う
  2. 内部依存はなるべくインタフェースで依存注入する

内部依存の例で説明した部品 A, B, C は実物が以下にあります。よろしければこちらも見てみてください。