23
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

株式会社アドベンチャーAdvent Calendar 2020

Day 5

レガシーコードの無法地帯にテスト文化を築いていく道のり

Posted at

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**が設定ファイルとなります。
公式のドキュメントにサンプル載っています。

https://phpunit.readthedocs.io/ja/latest/configuration.html

弊社の今回導入するプロジェクトの設定は以下になっています。

<?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.jsonscriptsの部分にショートカットを登録することが出来ます。
登録されるとcomposer {scripts_name}で実行可能になります。
以下の場合であればcomposer testでテスト実行可能。

	"scripts": {
		"test": "APP_ENV=test php vendor/phpunit/phpunit/phpunit"
	}

テストカバレッジの表示方法(テストの結果をグラフで表示する)

カバレッジは、所定の網羅条件がテストによってどれだけ実行されたかを割合で表したものです。

以下のような感じでテストが実行されている部分・されていない部分などを一目でわかるようにグラフ化してくれる機能があります。[%]の割合で表示されているので、これが90%(緑)になっていない部分はほとんどテストがされていないという事になります。

coverage.png

実際にテストが通っている部分と、通っていない部分は以下のように表示されます
cov2.png

この機能を使えるようにこれも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

実行されると以下のようにGitHub側で実行されます。
github_actions.png

テストが失敗すると以下のように表示されます。

test_fail.png

とりあえず、テストコードを書いてみる

わし「さあ、テスト書いたるでー!」
リーダー「おう頑張りや」
わし「なあテストってどう書くんや。わからんわ。もう今日帰っていい?」
リーダー「ちょ待てや」

どうやってテストコードを書くんや

リーダー「例えばだけど、名前を連結する処理があったとするやろ?『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が存在しない場合って...?

これは**HogeServiceHogeRepositoryに依存している状態**となります。つまり、このHogeServiceHogeRepositoryがいないと成立しないメンヘラクラスです。

相関関係は以下のようになっています。
相関関係01

依存している状態だと「このHogeServiceを動かしてテストしたい!」っていうときに積み状態となります。


    public function testHogeGetAll()
    {
        // これ自体を使うことができない
        $service = HogeService();
        $actual = $service->getHogeAll();
        $expect = ['hoge'];
        $this->assertSame($expect, $actual);
    }

このメンヘラ状態を打破するには?
**SOLID原則の5つ目「依存関係逆転の原則」**を使うとメンヘラ状態を打破できます。

  1. Single Responsibility Principle:単一責任の原則
  2. Open/closed principle:オープン/クロースドの原則
  3. Liskov substitution principle:リスコフの置換原則
  4. Interface segregation principle:インターフェース分離の原則
    5) Dependency inversion principle:依存性逆転の原則 // こいつを使う

SOLID原則を使う

Dependency inversion principle(依存性逆転の原則)

  • 上位レベルのモジュールは下位モジュールに依存してはならない、どちらのモジュールも抽象に依存すべきである
  • 抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである

わい「なんや急に難しいな...」
リーダー「ここをしっかり読むんや」
上位レベルのモジュールは下位モジュールに依存してはならない

リーダー「この依存の原因であるHogeRepositoryというのはレイヤーで言うと一番下層のものになるねん。HogeServiceHogeRepositoryを使用しているから、HogeServiceHogeRepositoryより上のものやな。」
わい「なるほど!つまり上記の法則に従うと、この依存関係はいかんってことか!」
リーダー「その通りや!上記の法則に従うと『抽象に依存するべき』とあるやろ?抽象ってどういうことか分かるかいな?」
わい「インターフェースやないか!」
リーダー「その通りや。図で表すと以下のようになるんや。」

依存関係

リーダー「さらに言うとな、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();

リーダー「HogeRepositoryInterfacegetAllというメソッドがいたやろ?こいつがどういう値を返してくれるのかここで設定ができるんや」
わい「❓❗️❓❗️❓❗️❓❗️❓❗️(メタルギア効果音)

$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(テスト駆動開発)」ですね。(正直まだ、その域に達していませんが...)この開発手法になんとか切り替えてプロジェクト、プロダクトをより良いものにしていきたいと日頃思っています。

自戒を含めた記事でした。(ちなリーダーはエセ関西弁は使いません)

23
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?