💋

仕様に対してメソッドを実装し、仕様に対して単体テストをする

2021/09/01に公開

メソッドを実装するときに仕様に対して実装して仕様に対してテストをするとよいのではないかと言う話です。
メソッドに関わない話だと思いますが、本記事ではメソッドにフォーカスしています。

仕様とは

Wikipedia より

仕様(しよう、英: specification スペシフィケーション)とは、材料・製品・サービスなどが明確に満たさなければならない要求事項の集まりである[1]。日常的には英語を短縮して「スペック」とも。

耐荷重50kgという仕様であれば50kgまで耐えてほしいし、失敗したらリトライするという仕様であれば失敗したらリトライしてほしいですが、50kgまで耐えてほしくても、耐荷重が定義されていなければその要求が満たされるかどうかはわかりません。(ただの期待でしかない)
が、仕様が定義されていなかったり仕様を読まなかったりすると、期待ベースで物事を進めることになるので良くないことが起きるかもしれない、みたいな話です。

メソッドに仕様をかく

public boolean isActive() {
	if(!this.isExpired()) {
		return false;
	}
	if(!this.isLocked()) {
		return false;
	}
	return true;
}
_人人人人人_
> Activeって何 <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

"アクティブ"という言葉の意味がどこかで合意できているなら良いですが、できていないならこれは仕様として曖昧です。
実装を見ると「期限切れではなくてかつロックされてないユーザー=アクティブ」ですが、それは仕様ではなく実装がたまたまそうなっているだけかもしれません。例えば仮ユーザーはアクティブではない、という要件があるのであれば、このメソッドにはバグがありますが、そうでないのであればバグのないコードになります。

こういう状態になると、この isActive を呼び出す人は「俺の思うアクティブ」に従って isActive を呼び出したり、「アクティブってなんだよ」って疑問を持ったりします。前者はバグの原因になります。(俺の思う有効 != isActive 実装者にとっての有効)

こういうバグは、以下のような状態なので不幸なことになります。

  • 呼び出し元はアクティブかどうかを判定したかったときにアクティブかどうかを判定するメソッドがあったので呼んだんだが
    • 正しいっちゃただしい
  • 実装者は俺にとってのアクティブを実装し、俺にとってのアクティブに対してちゃんとテストをしたんだが
    • 正しいっちゃただしい

仕様を書く

/**
 * ユーザーがアクティブかどうかを返します
 * <p>アクティブなユーザーとは、「期限切れではなくてかつロックされてないユーザー」のこと</p>
 * @return アクティブなら true
 */
public boolean isActive() {
	if(!this.isExpired()) {
		return false;
	}
	if(!this.isLocked()) {
		return false;
	}
	return true;
}

アクティブの定義が仕様化されました。(期限切れとは、みたいな話もでてきますが)
アクティブの定義が正しいかどうかはともかく、isActive の実装者にとってのアクティブ、は呼び出し元に伝わりそうです。

_人人人人人人人人人人人人_
> 仕様を読んでくれれば <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

仕様を読む

メソッドの呼び出し時に、メソッド名だけ、ここでは isActive という文字列をみて、俺の思うアクティブ = isActive が満たすアクティブ、と判断してもよいのでしょうか。
アクティブとは何か?がチーム内で合意できていれば問題ないですが、そうではない場合自分の思うアクティブと実装者のアクティブには差があるかもしれません。

この差を埋めるために仕様は役に立ちます。
が、仕様が書いてない、ということもよくあることです。

Wikipedia より

仕様(しよう、英: specification スペシフィケーション)とは、材料・製品・サービスなどが明確に満たさなければならない要求事項の集まりである[1]。日常的には英語を短縮して「スペック」とも。

仕様がないということは、「そのメソッドがどういう要求を満たすのかがわからない」という状態です。
どういう要求を満たすのかが決まっていないものを使うのは危うい気がします。(耐荷重の書いていないカラビナを登山に使うのは危ない)

また、そのメソッドが本当に自分の想定している要求を満たすのか確認するためにコードを全部読むのは、場合によってかなりめんどくさいことになります。(耐荷重の書いていないカラビナの耐荷重を自力で検証するにはコストがかかる)

このようなメソッドを見つけた場合、実際にそのメソッドが何をしてくれるのか確認するためには実装を見る必要があります。
見た結果、もしくは実装者に直接確認した意図などの情報はその後も利用できるように仕様化しておくとよいかもしれません。

コードが仕様という発想

コードが仕様であればコードと仕様の間に差分が発生しません。
つまりそのメソッド単体で見た場合、常にバグがない状態になります。
ただしそのメソッドに求める要求は各個人の頭の中にあるので、各個人の頭の中にある期待に差分が出ると呼び出し方によってバグの原因になります。

仕様に対してテストをする

Wikipedia より

仕様(しよう、英: specification スペシフィケーション)とは、材料・製品・サービスなどが明確に満たさなければならない要求事項の集まりである[1]。日常的には英語を短縮して「スペック」とも。

テスト、特に単体テストは「明確に満たさなければならない要求事項」をメソッドが満たしているかどうかの確認であることが多いと思います。
要求事項の実態は仕様がなければ「俺はこう動いてほしい」ですが、仕様があればそれが実態です。
前者のテストは「俺はこう動いてほしい=実際の挙動」の確認ですが、後者は「定義された仕様=実際の挙動」を確認することになります。

俺はこう思う、は結構ブレる

人なので思っていることは結構ブレます。
で、ブレると「実装してたときに思ってたやつとテストしてたときに思ってたやつ」に差が出ることがあります。
差が出るとどうなるかというと、テストが不足したりします。実装してたときは「ロックされてるユーザーって非アクティブだよなー」と思っていても、テスト時に「ロックしてるかどうかとアクティブの関係」が抜けたりします。

あとは時間がたって「アクティブってこうだよね」がみんなの頭の中で確立してきたときに、あのときのアクティブと今のアクティブがずれますが、そのずれは自動で修正されないのでずれがバグにつながったりすることもあると思います。

実装に対してテストしたくなる

仕様を決めないで書いたコードに対して網羅を意識すると、「満たしたい要求」ではなく「コードが網羅できているか」に対してテストをしたくなることがあります。

public boolean isActive() {
	if(!this.isExpired()) { // <- expiredかどうかに対してテストしたい
		return false;
	}
	if(!this.isLocked()) { // <- lockedかどうかに対してテストしたい
		return false;
	}
	return true;
}

これはこれで有効なことも結構あると思いますが、「満たしたい要求」が主軸になっていないので「テストはしていて網羅もできてるけど要求が満たされていない」みたいなことが起きることがあります。

例えば「ロックされてるユーザーって非アクティブだよなー」と思って実装したけど実際のコードではロックの概念が抜けていた(バグ)場合、ロックされているかどうかのテストが行われません(コードにはロックの概念がないので)。

仕様に対してテストする場合

/**
 * ユーザーがアクティブかどうかを返します
 * <p>アクティブなユーザーとは、「期限切れではなくてかつロックされてないユーザー」のこと</p>
 * @return アクティブなら true
 */
public boolean isActive() {
	if(!this.isExpired()) {
		return false;
	}
	if(!this.isLocked()) {
		return false;
	}
	return true;
}

に対してテストを書く場合、

/**
 * ユーザーがアクティブかどうかを返します
 * <p>アクティブなユーザーとは、「期限切れではなくてかつロックされてないユーザー」のこと</p>
 * @return アクティブなら true
 */
public boolean isActive() {

だけ見ればテストを書くことができます。この観点でテストをすると、テストが「満たしたい要求」と実装のすり合わせになります。ただしコード上の網羅は担保できないかもしれません。(デッドコードとかは気づけない)

が、個人的にはコード上の網羅よりも仕様を網羅しているかどうかのが大事だと思っています。(仕様を満たさないと困るけど、デッドコードとかあってもそこまでは困らない)

ただし、コードに対してテストすることで見つかる仕様バグみたいなものもあるので、コードを見ながらテストケースを考える、が常に間違っているとは思いません。
仕様化してないけどテストしたいやつ、みたいなののテストは抜けることがあるのでこの辺は個人的に悩みがちです(モック呼んでるかの検証とか)。

まとめ

  • メソッドの仕様を決めて文章化するとよい
    • 仕様が決まっていないメソッドの呼び出しは、「多分期待通り動くであろう、という勘」か「実装を読む時間を使う」のいずれかになる
  • メソッドを呼ぶときは仕様を理解してから呼ぶとよい
    • 勘で呼んだら勘なりのバグがでる
    • 定義されてなかったら改めて定義できるとめっちゃ良い
  • 仕様に対してテストをすると、「満たしたい要求」と実装のすり合わせができる
    • そもそも仕様がないもののテストは不要
      • だってコードが仕様なのだからバグは発生しない
        • とはならない
          • 実装者の意図 & 呼び出し元の期待 & 実装のズレはバグにつながる
GitHubで編集を提案

Discussion