前提
本記事は個人レベルでユニットテストを導入する際の観点で書いています。
組織単位で導入する際の観点では書いて書いておりません。
Unit テストとは
wikipedia より
コンピュータプログラミングにおいて単体テスト(たんたいテスト)あるいはユニットテスト(英語: Unit test)とは、ソースコードの個々のユニット、すなわち、1 つ以上のコンピュータプログラムモジュールが使用に適しているかどうかを決定するために、関連する制御データ、使用手順、操作手順とともにテストする手法である。
つまり、関数やメソッド単位(ユニット)が正しく機能を果たしているか確認するテストのことです。
ユニットテストを書くメリット
-
素早いフィードバックが得られる。
コマンド1つ叩けば、結果が得られるので、実装が合っているかの確認を素早く済ます事が出来ます。
-
テストコードがコードリーディングのためのドキュメントになる。
テストコードにはプロダクションコードの振る舞いが例示されるので、コードリーディングの手助けとなる。
-
影響範囲が特定しやすくなる。
プロダクションコードの変更時、既存と振る舞いが変わる場合、テストが失敗します。そのため、変更が影響する範囲を特定しやすくなります。
-
実装者が安心して実装を進められる。
リファクタリングの際の安心材料になります。胃へのダメージを抑えられます。心的保全、ダイジです。
-
プロダクションコードが疎結合な分かりやすいコードになる。
ユニットテストをしやすさを意識すると自然とコードの分離が必要となります。コードの分離によって、複雑に絡みあった難しいコードになることを避けることが出来ます。この利点を最大限利用したものが TDD(テスト駆動開発) と呼ばれる開発手法です。
手動テストとの比較
-
未知の不具合を発見するには、手動テストのほうが効率がよい。
- あくまで自動テストは実装者の意図どおりにプログラムが動作しているか確認するためのものです。
-
作成コストは自動テストの方が大きく、実行コストは手動テストの方が大きい。
- コスト比較で手動テスト < 自動テストになりそうな場合、手動テストの方を選択するのも1つの手段です。
ユニットテストを書いたほうがよい箇所
-
ちゃんと動くか不安なコード
- 基本ですね。テストを書いて確認して自信を得ましょう。
-
仕様が追加される度に検証したいコード
- テストを書くと変更時の影響範囲を特定しやすくなるので、デグレなども防ぎやすくなります。
-
手動だと手間のかかるテスト
- プロダクションコードで値をいじって出力処理を書いて確認...などを一々やっていては手間がかかりますし、バグ混入リスクも高まりますのでユニットテストを書いて確認できると良いです。
実践
以上の前提を踏まえまして、実際にコードを書いてみます。
シナリオ
2020 年の 8 月 9 日中まで公開するサイトを制作する。
そのサイトが公開しているかどうか判定する処理を実装する。
実装
❌Bad : テストしづらいコード
<?php
public function isSiteClosed(): bool
{
$now = new DateTime;
$expiration = new DateTime('2020-08-09 23:59:59');
return $this->expiration < $now;
}
上記コードだとテストを書いても、テストを実行する日時によって結果が変わってしまいます。
実行タイミングによってテスト結果が変わってしまっては、せっかくテストを書いてもナンセンスなものになってしまいます。
⭕Good : テストしやすいコード
<?php
public function isSiteClosed(DateTime $expiration, DateTime $now): bool
{
return $expiration < $now;
}
比較する日時をテスト時に変更できるように、値を引数で注入できるように変更しました。
上記のコードのような実装にすると、値と処理の依存関係が排除され、テストを実行しやすくなります。
また、再利用性なども向上し、非常に取り回しの良いコードになりました。
更にオブジェクト指向的な実装に落としこんでみます。
<?php
namespace App;
class Site
{
private $expiration;
public function __construct(\DateTime $expiration)
{
$this->expiration = $expresion;
}
public function isClosed(\DateTime $now): bool
{
return $this->expiration < $now;
}
}
上記のような実装にすれば、DI なども利用しやすくなりますね。
テスト作成
テストを書いていきます。
大切なのは、成功するテスト、失敗するテスト両方のケース書くことです。
(常に成功するテストは意味がないため。)
<?php
namespace Tests;
use PHPUnit\Framework\TestCase;
use App\Site;
require "vendor/autoload.php";
class ClosedTest extends TestCase
{
private $site;
public function setUp(): void
{
date_default_timezone_set('Asia/Tokyo');
$this->site = new Site(new \DateTime('2020-08-09 23:59:59'));
}
public function test_site_is_closed()
{
$now = new \DateTime('2020-08-10 24:00:00');
$this->assertTrue($this->site->isClosed($now));
}
public function test_site_is_not_closed()
{
$now = new \DateTime('2020-08-09 23:59:59');
$this->assertFalse($this->site->isClosed($now));
}
}
それでは作成したテストを実行しましょう。
無事テストが通りました。
良いユニットテストを書くための心がけ
-
小さい単位でテストを書く
- ひとつのテストの単位を大きくしすぎると、メンテナンス性が悪い上に目的が分かりづらいテストコードとなってしまいます。
-
DB などの外部環境への依存をなくす
- ユニットテストはあくまでロジックの検証に留めるために、Mock などのテストダブルを活用しましょう。(テストダブルとはテスト対象が依存しているモジュールなどの代替となるプログラムのこと)
-
何度実行しても同じ結果が得られるテストを書く
- 実行のたびに結果が変わりうるテストはユニットテストのメリットを低減させてしまいます。
-
テスト間の重複を最低限にする
- ユニットテストのメンテナンスを楽にするには、ひとつのプロダクションコードの変更に対して影響を受けるテストの数が少ないようにテストを記述するのが望ましいです。
ユニットテストを取り入れるためのファーストステップ
-
デバッグの代替手段として使用する
-
echo
やvar_dump
などの出力処理をプロダクションコードに書いてデバッグしたくなるのをぐっと堪えて、テストを書いてみましょう。1 回確認したものを再度確認するのが楽になります。また、テスト用に書いたコードをプロダクションコードから消すのを忘れていたら事故ですが、テストコードはその心配をする必要はありません。
-
-
ロジックが正しく動くか心配なものだけユニットテストを書く
- はじめから仕様すべてに対してテストコードを書くのは難しいので、動作するか不安なロジックに対してのみテストを書いてみましょう。動作するという安心を得られますし、テスト対象をテストしやすくリファクタリングしてるいくうちにもっと簡単なコードにできるかもしれません。