📝

テストコードの設計について考える

2022/01/08に公開
1

はじめに

現代の開発では、自動テストコードは必須です。しかし、そのテストコードの設計については伝えづらいと感じています。この記事では、自分が何に着目してテストコードを書くか、どのようなテストコードを書いて、どのようなテストコードを書かないかについて記載します。

前提条件、あるいは書かないこと

この記事では自分の経験に基づいており、次の点に依存した記事になっています。

  • コードレビューが行えるチーム開発
  • Python + Djangoによる開発
    • 静的型付け言語など、当てはまらないこともあると思います。
  • 機能テストのみ
    • 逆に言えばE2Eテストや非機能テストなどは除きます。

この記事では次のことについては基本的に書きません。

  • テストコードの実際の書き方

「テストを通すこと」を目的としない

当たり前すぎて言うまでもありませんが、「テストを通すこと」を目的としてはいけません。テストが通らないならテストコードか、実コードのどちらかが間違っています。わからないなら素直に他の人に聞きましょう。もしごまかしをする人がいれば、全てが台無しになります

実際にそのようなことをする人に遭遇したことはありませんが、信頼の前提となるのであえて記載しておきました。

また、「通常起きない例外」などは無理にテストを通す必要はありません。通さなくていい箇所は具体的には次の記事に記載しています。

https://zenn.dev/ikemo/articles/test-coverage-100-percent#カバレッジ100%25の基準

テスト設計について学ぶ

テストをコードで書く時代になっても、基本的なテスト設計については変わりません。自分はこの本で学びましたが、「同値クラステスト」「境界値テスト」などの基本的な考え方は今でも必須です。

https://books.ikemo3.com/book/software-test-design/

原理的にはコードカバレッジを計測して、ミューテーションテストを実行すればテスト不足は検知できますが、ミューテーションテストは遅すぎるので実際に開発で使用したことはありません。

全てのファイルは1ケースは通す

これはおそらくPython固有の問題だと思いますが、過去にDjango 3.0から3.1に上げた時に、1つだけ動かなくなった機能がありました。その機能はDjangoのコマンドだったのですが、インポートの形式が適切ではなく、Djangoの内部実装に依存していました。

例えば、django.http.response.HttpResponseRedirect を使う場合、次のように書くこともできます。しかし、これはあくまでDjangoの実装に依存したもので、バージョンが上がると使えなくなる可能性があります。このような書き方が引っ掛かっていました。

from django.views.generic.edit import HttpResponseRedirect

これがテストで検知できなかったのは、ファイル丸ごとテストがなかったからです。かといってテストを十分書くだけの時間がありませんでした(本当はそれが問題だとは思いますが)。なので、「少なくともimportができることのテスト」を追加しました。

「ロジック」と「ポカヨケ」のどちらかを意識する

テストには主に2つの目的があると思っています。1つは、ロジックのテストです。例えば閏年の判定ロジックの場合、「4で割り切れるか」「100で割り切れるか」「400で割り切れるか」のそれぞれについてテストを追加します。こちらは分かりやすいと思います。

もう1つは「ポカヨケ」です。ポカヨケはうっかりミスを防ぐための装置のことです。

https://ja.wikipedia.org/wiki/ポカヨケ

例えば先ほど記載したDjango 3.1で動かなくなった機能は「ポカ」で、それを防ぐための「少なくともimportができることのテスト」は「ポカヨケ」です。これ以外にもリファクタリングをしやすくするためのテストはポカヨケの1つとも言えます。

もちろんこの2つが重なることもよくあります。しかしこの2つを分けて書く理由は、どこに書くのが望ましいか、どのレベルで書けばいいかどうかが異なるからです。

ロジックは同じレイヤーにテストを書く

まず、ロジックはなるべくモデル側に寄せて書き、依存するものを減らします。具体的には次の記事で解説しています。

https://zenn.dev/ikemo/articles/django-keep-away-from-fat-model

そして、例えばモデルにロジックを書いた場合、そのロジックのテストはモデルレベルで行うことが重要です。別の言い方をすると、モデルのテストをするために、ビューを呼び出さないことが重要です。レイヤーが上の方になるほど、検証しづらくなります。

実際に書くときには models.py のテストは test_models.py に記載し、 forms.py のテストは test_forms.py に記載する、そのように考えておけばいいでしょう。

「ポカヨケ」はカバレッジ重視

一方で「ポカヨケ」のためには、ロジックを網羅するテスト、例えば「境界値テスト」の必要性は低いです。逆に言えば、広く浅く網羅していることが重要です。そのためにはカバレッジが重要です。逆に言えば、コンパイル時にチェックしてくれる言語ではこちらの重要性は高くありません。

この2つを意識することで、低いレイヤーのテストは綿密に、高いレイヤーのテストは大雑把に書き、メンテナンスしやすいテストが書けるようになります。

フレームワークやライブラリのテストは書かない

あと重要な観点として、前提としているフレームワークやライブラリ「自体」のテストは書きません。例えばDjangoでは組み込みでCSRFチェックがサポートされていますが、「CSRFトークンをダミーの値にしたときに403 Forbiddenになる」といったテストは不要です。それはDjangoの責務です。

また、例えば @require_http_methods(["POST"]) がある場合にGETでアクセスするとエラーになる、そのようなコードを見ればわかるものについても不要です。そのようなものについてはコードレビューで行いましょう。もしコードを読めない人の承認のために必要なら、 そのプロセスが有害無益 なので、まずそれをなくしてください[1]

ただし、Djangoの内部実装に依存せざるを得ないコードや、ライブラリの乗り換えの際に互換性を担保するためにテストを書くことはあります。

コードレベルで考えること

この記事では基本的にテスト設計の話ですが、次の点については重要なので記載しておきます。

実コードとテストコードは真逆だと考える

例えばコードの重複やマジックナンバーはよくないコードですが、テストコードについてはむしろ推奨されます。また、条件分岐もテストコードでは避けた方がいいです。

1ケースにいろいろ詰め込まない

1つのテストでいろいろ検証しようとして、詰め込むケースをたまに見かけます。そのようなテストはメンテナンスしづらいため、避けてください。ただし、ユースケースに沿ったシナリオを検証するのであれば構わないと思います。

おわりに

「TDDについては書かない」と冒頭に書きましたが、やっぱりTDDは学んでおいた方がいいでしょう。自分は書籍の写経はしないタイプですが、この本については写経することを強く推奨します。

https://books.ikemo3.com/book/tdd/

脚注
  1. ここだけ強い口調ですが、過去に酷い目にあったのが原因です。 ↩︎

Discussion

Akira KashiharaAkira Kashihara

今、テストについて勉強していますが、右も左も分からなかったので、書籍など紹介していただいて、とても助かりました。ありがとうございます。