はじめまして。2016年2月にTimes.incに入社しましたサーバーサイドエンジニアの山口です。 よろしくお願いいたします。
はじめに
今回は、「テストに投資した時間が、チームにどのような利益をもたらすか?」について弊社で採用しているPHPテスティングフレームワークCodeceptionで記述したサンプルコードを交えながら、ご紹介したいと思います。
Codeception
Github: https://github.com/Codeception/Codeception
Reference: http://codeception.com/
サンプルコード
<?php class Number { const ENCRYPTION_TEXT = 'p4MtosJxWr9THgU-fu6nAVlqIOyPkEXNi/mzjGK0BF713QdYLCw25ca8RDZhevbS'; const ENCRYPTION_NUMBER = 23047908131; /** * 数値を暗号化する * * @param int * @return string */ public static function encryption($v10) { $v2 = decbin($v10 + self::ENCRYPTION_NUMBER); $values = str_split(str_repeat('0', 6 - strlen($v2) % 6) . $v2, 6); $v64 = ""; foreach ($values as $i => $v) { $v64 = self::ENCRYPTION_TEXT[bindec($v)] . $v64; } return $v64; } /** * 文字列を復号化する * * @param string * @return int */ public static function unencryption($v64) { $v64 = str_split($v64, 1); $encryption_text = array_flip(str_split(self::ENCRYPTION_TEXT, 1)); $v10 = -self::ENCRYPTION_NUMBER; foreach ($v64 as $i => $v) { $v10 += ((int)$encryption_text[$v]) * 64 ** $i; } return $v10; } }
10進数を暗号化/復号化するクラスを実装しました。
暗号化/復号化した結果が一致することが期待される動作です。
正しく実装されているか確認してみましょう。
php -r "require 'Number.php';echo Number::encryption(1192);" #=> "TSwLEV" php -r "require 'Number.php';echo Number::unencryption('TSwLEV');" #=> 1192
期待された値が出力されました。ここまでは開発者の誰でも通常行っている検証方法だと思います。動作確認するためのこのコードはその後どうしますか?通常は捨ててしまうのではないのでしょうか。このコードを捨ててしまうのはもったいないですね。このコードとその結果を捨てずに残しておくことでチームに以下の4つのメリットをもたらします。
- メソッド使用例を知ることができます。
- どのような検証を行ったかを知ることができます。
- メソッドの正しい挙動を知ることができます。
- メソッドを実行する前に必要な手順を知ることができます。
ワンライナーで実行したコードをPHPUnitで書きなおしてみましょう。
UnitTest
<?php class NumberTest extends \PHPUnit_Framework_TestCase { # 成功 public function test_1() { $this->assertEquals(Number::encryption(1192), 'TSwLEV'); } # 成功 public function test_2() { $this->assertEquals(Number::unencryption('TSwLEV'), 1192); } }
元のコードとの違いはクラス化されたことですが、もっと大事な違いがあります。それは、結果を標準出力に出力していたものが、コードに内包されたことです。
UnitTest化する前は仕様を知る人間がコードを実行して動作確認をする必要がありましたが、UnitTest化した後は結果がコードに内包されるようになったことでコンピューターに動作検証を任せることができるようになりました。「Github上にPUSHしたら」「ステージング環境にデプロイしたら」といったアクションをトリガーにテストケースを実行するようにすれば、テストを自動化することもできますね。
元のコードよりもコード量が増えたことで、開発工数も増えましたが、見た目の長さほどは実装に工数はかかりません。なぜならNumberTestクラスは、関数を呼び出し、戻り値と比較しただけのシンプルなコードですから、Numberクラスと比較して簡単に実装できます。慣れてさえしまえば見た目のコードの長さよりは頭を使わない分、素早く実装できてしまいます。
さて、実行してみましょう。
$ php codecept.phar run unit Unit Tests (2) ------------------------------ Example\NumberTest::test_1 Ok Example\NumberTest::test_2 Ok --------------------------------------------- Time: 217 ms, Memory: 9.75Mb
ユニットテスト化することに工数をかけたことで、たった一行のコマンドで2つのテストケースの実行結果が得られるようになりました。簡単な操作ですばやく検証のフィードバックが得られるというのはとても重要です。もし、一度の検証のための準備に膨大な時間がかかるとしたら、何度も気軽に検証することができませんね。検証される回数が減ってしまえば、その分だけ不具合が入り込む確率が高くなります。手軽に検証ができるようになったことで、アプリケーションの品質を保つのが容易になっていきます。
WebAPI
Number::encryptionメソッドをWebAPI化した場合のテストケース記述例を紹介します。
<?php require __DIR__ . '/number.php'; $result = ['value' => Number::encryption(@$_GET['v'])]; header("Content-Type: application/json; charset=utf-8"); header("HTTP/1.1 200 OK"); echo json_encode($result);
# WebServerの起動 $ php -S localhost:3000 # 検証 $ curl http://localhost:3000/encryption.php?v=1 {"value":"j3wLEV"}
AcceptanceTest
tests/acceptance/encryptionCest.php
<?php class encryptionCest { public function _before(AcceptanceTester $I) { } public function _after(AcceptanceTester $I) { } // tests public function tryToTest(AcceptanceTester $I) { $I->sendGET('encryption.php?v=1'); $I->seeResponseContainsJson(['value' => 'j3wLEV']); $I->seeResponseCodeIs(200); } }
AcceptanceTestを実行します。
$ php codecept.phar run acceptance Acceptance Tests (2) ----------------------------- Try to test (encryptionCest::tryToTest) Ok --------------------------------------------------
おわりに
Codeceptionを利用することで、本来チームの誰かが手間ひまかけてやらなければならなかった様々な検証がコンピューターで実行可能になりました。最初はどのようにテストを書いていけばよいかわからないこともあると思いますが、Codeceptionが様々な難しいテストを書くための機能を用意してくれています。ぜひチームのために、テストを書く習慣を!