Adventカレンダー5日目の記事です。
レガシーコードが存在するプロジェクトの出来事
わい「このシステム出来ましたで!確認オネシャス!」
リーダー「お! 早いやんけ!どれどれ.......なんやこれは」
わい「何ってご所望のシステムですやん」
リーダー「テストはどこやねん」
わい「テストもちゃんとやってますわ!ここのExcelにあるやろ!」
リーダー「これはテストとは言わんのや」
わい「なんやて?」
これがレガシーコードの無法地帯におけるテスト導入との始まり。
前提として、コーディングが終わった時点でテストというのは必須です。(何を当たり前のことを言うてるんや)
当たり前ですがテストをしないとバグを含んでいるのか、そもそもちゃんと動くのかという保証が全く分かりません。
「テストしてあるよ!」
「ちゃんと動いてるじゃん!」
これでは保証にはなりません。個人のチェックというだけで証拠となりません。
それが仕事をサボりたいだけの嘘だったら?
テストが実行された/されているという証拠が絶対に必要なのです。
それは決してExcelなどで管理されるものではないのです。
※前の会社はめちゃくちゃ大規模サービスの開発をしていましたがテスト仕様書からテストの結果までほぼ全てSVNのExcel管理でした...。
単体・結合テスト結果をExcelに書かないこと
詳細は以下を読んでくださいまし。
≫テスト自動化プロジェクトでExcelを使わない10の理由
Excelは改竄されることもあるし、確認方法にもよりますが過去に本当に確実な方法で実行されているのか確かめるにも時間がかかります。担当者にその事を聞こうにも退職していたりと、どこに真実があるのか不明です。
そして、証拠を探す旅に出るために時間を消費したりと永遠にそのループから抜け出せません。
手動で「○, ×」を付けていくあの作業は...個人的に結構トラウマ...。
テスト自動化しようよ
弊社ではPHPを使用したプロジェクトが90%以上を占めています。
なので「PHPUnit」という、 PHPプログラミング言語用の単体テストを行うためのフレームワークを使用します。
※弊社ではここ1.2年で始まっているプロジェクトはほとんとがLaravelを使用したプロジェクトなので、元々全て導入されていますが、一番最古のプロジェクトは独自のFWなので導入されていませんでした。
テスト自動化自体の歴史については見やすい記事があったので以下をご覧くださいまし。
≫ テスト自動化の歴史
PHPUnit導入
composer使って導入。--dev
をつけるのを忘れずに。
composer require --dev phpunit/phpunit
composer.jsonに以下のように入っていたらOK。
"require-dev": {
"phpunit/phpunit": "**",
}
PHPUnitの設定ファイルを追加
**phpunit.xml
**が設定ファイルとなります。
公式のドキュメントにサンプル載っています。
弊社の今回導入するプロジェクトの設定は以下になっています。
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
verbose="true"
stopOnFailure="true"
processIsolation="false"
backupGlobals="false">
<testsuite name="Migrate">
<!-- API Server Version -->
<directory suffix="Test.php" phpVersion="5.5.38">tests</directory>
</testsuite>
</phpunit>
PHPUnitの使い方
細かい使い方などは公式の見たりするといいです。
https://phpunit.readthedocs.io/ja/latest/index.html
http://www.phpunit.cn/manual/6.5/ja/writing-tests-for-phpunit.html#writing-tests-for-phpunit.test-dependencies
PHPUnitをもっと使いやすくするためにComposer.jsonにショートカットを設置
phpunitを実行する時のショートカットを設置しましょう。
都度ローカルで実行したりするときや、CIで実行する際に楽になります。
composer.json
のscriptsの部分にショートカットを登録することが出来ます。
登録されるとcomposer {scripts_name}
で実行可能になります。
以下の場合であればcomposer test
でテスト実行可能。
"scripts": {
"test": "APP_ENV=test php vendor/phpunit/phpunit/phpunit"
}
テストカバレッジの表示方法(テストの結果をグラフで表示する)
カバレッジは、所定の網羅条件がテストによってどれだけ実行されたかを割合で表したものです。
以下のような感じでテストが実行されている部分・されていない部分などを一目でわかるようにグラフ化してくれる機能があります。[%]の割合で表示されているので、これが90%(緑)になっていない部分はほとんどテストがされていないという事になります。
実際にテストが通っている部分と、通っていない部分は以下のように表示されます
この機能を使えるようにこれもcomposer.jsonの中のscripts
で定義しましょう。
tests/coverage/ 下に吐くように設定しています。
"scripts": {
"test": "APP_ENV=test php vendor/phpunit/phpunit/phpunit",
"test:cov": "APP_ENV=test php vendor/phpunit/phpunit/phpunit --coverage-html tests/coverage",
}
GitHubActionsでテストを自動実行させる
細かい説明は割愛します。使い方は以下を読んでいただければ。
https://qiita.com/1915keke/items/8b18097d2981e88eca93
git push
が作業ブランチに行われた際に、GitHub側でテストを自動で実行してもらう仕組みを入れてあげましょう。これで手動で実行しなくても強制的に実行されます。
※先ほど、ComposerのScript
を設定したのもここで楽になるからです。
name: CI Lint Check
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: composer install
run: composer install -n --prefer-dist
- name: Update composer
run: composer self-update
- name: Update Lockfile
run: composer update
- name: Install Dependencies
run: composer install --dev
- name: Run Test
run: composer test
テストが失敗すると以下のように表示されます。
とりあえず、テストコードを書いてみる
わし「さあ、テスト書いたるでー!」
リーダー「おう頑張りや」
わし「なあテストってどう書くんや。わからんわ。もう今日帰っていい?」
リーダー「ちょ待てや」
どうやってテストコードを書くんや
リーダー「例えばだけど、名前を連結する処理があったとするやろ?『Qiita』『太郎』と2つの値がって渡ってきたら『Qiita太郎』ってしたい場合とする。このテストを書きたいって場合は以下のように書くんや。
class ConneectNameTest extends TestCase
{
public function testConnectName()
{
$firstName = 'Qiita';
$lastName = '太郎';
$connectNameObj = new ConnectName($firstName, $lastName);
# 想定値
$expect = 'Qiita太郎';
# 実際に自分で作った処理を通して同じデータが作成されているか確認する
$actual = $connectNameObj->conect();
$this->assertSame($expect, $actual);
}
}
リーダー「この
assetSame
が同じデータかちゃんと調べてくれるメソッドになってるから、さっき設定したテストコマンドを実行してみて問題なければテスト成功や」
わい「なるほど。テストをコードベースでちゃんと書いてくんやな!任せとき」
テストによって確認したい項目は異なります。文字列なのか、オブジェクトなのか。
以下で大体の項目は網羅できると思います。
http://www.phpunit.cn/manual/6.5/ja/appendixes.assertions.html
テストを書きやすいコードにする方法
わし「ふう...。大体テストを書いてみたな。...あれ?テスト失敗するな。何?HogeRepositoryクラスがないやと?テストが出来ひんな帰ろ〜」
リーダー「ちょ待てや! HogeRepositoryがなくてもテストは出来るんや」
わし「なんやて!(帰れると思ったのに)」
例えば以下のクラスのテストコードを書きたいと思った時。
class HogeService
{
private $repository;
public function __construct()
{
$this->repository = new HogeRepository();
}
public function getHogeAll()
{
return $this->repository->getAll();
}
}
これをテストするにはどうしたらいいのか。
HogeService
使用するには、これ自身インスタンス化して普通に使えば良いやん!
と思ったけどHogeRepository
が存在しない場合って...?
これは**HogeService
はHogeRepository
に依存している状態**となります。つまり、このHogeService
はHogeRepository
がいないと成立しないメンヘラクラスです。
依存している状態だと「このHogeService
を動かしてテストしたい!」っていうときに積み状態となります。
public function testHogeGetAll()
{
// これ自体を使うことができない
$service = HogeService();
$actual = $service->getHogeAll();
$expect = ['hoge'];
$this->assertSame($expect, $actual);
}
このメンヘラ状態を打破するには?
**SOLID原則の5つ目「依存関係逆転の原則」**を使うとメンヘラ状態を打破できます。
- Single Responsibility Principle:単一責任の原則
- Open/closed principle:オープン/クロースドの原則
- Liskov substitution principle:リスコフの置換原則
- Interface segregation principle:インターフェース分離の原則
5) Dependency inversion principle:依存性逆転の原則 // こいつを使う
SOLID原則を使う
Dependency inversion principle(依存性逆転の原則)
- 上位レベルのモジュールは下位モジュールに依存してはならない、どちらのモジュールも抽象に依存すべきである
- 抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである
わい「なんや急に難しいな...」
リーダー「ここをしっかり読むんや」
【上位レベルのモジュールは下位モジュールに依存してはならない】
リーダー「この依存の原因である
HogeRepository
というのはレイヤーで言うと一番下層のものになるねん。HogeService
はHogeRepository
を使用しているから、HogeService
はHogeRepository
より上のものやな。」
わい「なるほど!つまり上記の法則に従うと、この依存関係はいかんってことか!」
リーダー「その通りや!上記の法則に従うと『抽象に依存するべき』とあるやろ?抽象ってどういうことか分かるかいな?」
わい「インターフェースやないか!」
リーダー「その通りや。図で表すと以下のようになるんや。」
リーダー「さらに言うとな、
HogeService
にとってはHogeRepository
がDBから取得したデータを保持しているのか、外部のAPIを呼んだ戻り値を使用しているか。参照元のデータがどこにあるかっていうのは干渉しない事が重要なんや。」
リーダー「つまり、抽象的に依存する事で相手の役割を隠す事ができる。だから今はDBから取得するっていう仕組みを使っていて、今後外部のAPIを呼ぶだけに変更したいっていう時でもHogeService
自体からHogeRepository
の呼び出し方は変わらないんや。」
テストがやりやすいコードへ修正する
一旦、インターフェースだけ作成。
interface HogeRepositoryInterface {
public function getAll();
}
リーダー「実はさっきのコードには問題点がもう一つあってな。インターフェイスだけ使っても上手くいかんねん。というのも、コンストラクタの中でインスタンス化しているやろ?これがあかんねんな。
じゃなくて、渡すのであれば引数にオブジェクトごと渡してあげるんや。
俗に言うと「依存性の注入」と言うんや。これを「オブジェクトの注入」と言い換えて言うこともあるで。
依存性の注入 / DI(Dependency Injection について
詳細は以下のサイトで綺麗に纏まっています
≫ DI・DIコンテナ、ちゃんと理解出来てる・・?
上記の原則を、当てはめて先ほどのコードを修正すると以下のようになります。
コンストラクタで抽象化したオブジェクト(インターフェース)を渡すようにします。
class HogeService
{
private $repository;
public function __construct(HogeRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function getHogeAll()
{
return $this->repository->getAll();
}
}
リーダー「これで実態の
HogeRepository
がなくてもヨシ!」
リーダー「さらにテストで必須なことを教えるで」
Mockeryを使ったれ
リーダー「さっきの
HogeService
を実際にテストを行う際にはもうちょい考慮が必要なんや」
わい「なんやて?」
リーダー「さっきの『HogeService
にとってはHogeRepository
がDBから取得したデータを保持しているのか、外部のAPIを呼んだ戻り値を使用しているか。参照元のデータがどこにあるかっていうのは干渉しない事が重要なんや』って事覚えてるか?」
わい「覚えてるで。これがどういう事なんや」
リーダー「これ自体はテストからでも同じ事で、DBにアクセスするならデータベースが、外部のAPIを呼ぶには外部のAPIが出来ていないとこのテスト自体は成立しない。ただ、これらを完成するまでも待ってられないやろ? そのときに使うのがMockery
っていうライブラリや。」
わい「ワッキー?」
リーダー「(これはなオブジェクトをモック化する事ができる便利なものなんや)なんやこいつ」
わい「え?」
リーダー「え?」
Mockeryのインストール
またまたcomposerでインストール
composer require --dev mockery/mockery
Mockeryの使い方
リーダー「Mockeryを使うとなオブジェクトをモック化する事ができて、実態がなくても記述者が想定する動きを作る事ができるんやな。以下のコードを使うとこれで
HogeRepositoryInterface
のMockが生成されるんや」
わい「...ほん?🤔」
$mock = \Mockery::mock(HogeRepositoryInterface::class)->makePartial();
リーダー「
HogeRepositoryInterface
はgetAll
というメソッドがいたやろ?こいつがどういう値を返してくれるのかここで設定ができるんや」
わい「❓❗️❓❗️❓❗️❓❗️❓❗️(メタルギア効果音)
$mock->shouldReceive('getAll')->andReturn(['hoge' => 'Qiita']);
リーダー「このモック化したオブジェクトをさっきの
HogeService
に渡してあげるんやな」
リーダー「まあ実際には、返ってきた値を色々変換して〜と思うがそこは自分で頑張り」
わい「あざます!」
public function testHogeGetAll()
{
// Mock化
$mock = \Mockery::mock(HogeRepositoryInterface::class)->makePartial();
# 想定する動きをモック化
$mock->shouldReceive('getAll')->andReturn(['hoge' => 'Qiita']);
$service = HogeService($mock);
# ['hoge' => 'Qiita']が返ってくる
$actual = $service->getHogeAll();;
}
テストちゃんと書こう
結構アドベントカレンダーに間に合わせるために急ぎ足で書いたので大分雑なところが多いですが、こんな感じでテスト文化を築いていきました(主にリーダーが)
ぶっちゃけテストって納期が差し迫っていたりすると「後で書くわ!」ってなりがちで、実際にコーディングが終わったらもう時間がなくてめっちゃテスト雑 or テストが中途半端な状態になる→それが積み重なるみたいな事が多々。
それを打破するべく生まれた手法?「TDD(テスト駆動開発)」ですね。(正直まだ、その域に達していませんが...)この開発手法になんとか切り替えてプロジェクト、プロダクトをより良いものにしていきたいと日頃思っています。
自戒を含めた記事でした。(ちなリーダーはエセ関西弁は使いません)