Upgrade to Pro — share decks privately, control downloads, hide ads and more …

大規模PHPプロジェクトでPHPUnitを3世代アップグレードするためにやったこと / PHPUnit Upgrade Story

DQNEO
December 15, 2018

大規模PHPプロジェクトでPHPUnitを3世代アップグレードするためにやったこと / PHPUnit Upgrade Story

PHPConference 2018の発表資料です。

DQNEO

December 15, 2018
Tweet

More Decks by DQNEO

Other Decks in Programming

Transcript

  1. ミッションとバリュー 新たな価値を生みだす 世界的なマーケットプレイスを創る Create value in a global marketplace where

    anyone can buy & sell Be Professional プロフェッショナルであれ All for One 全ては成功のために Go Bold 大胆にやろう 5
  2. メルカリ日本事業 2013年に日本でのサービスを開始 出典:会社資料。JP版メルカリ事業の通期決算概況より。四半期ごとの数値はIRページより確認可能 1. キャンセル等を考慮後の取引高の合計 2. .Monthly Active Userの略であり、1ヶ月に一度以上利用した登録ユーザーの数(「メルカリ アッテ」「メルカリ

    カウル」「メルカリ メゾンズ」「メルチャリ」「teacha」は含まず) 流通総額(1) 3,468億円 FY 2016.6 FY 2017.6 FY 2018.6 MAU(2) 1,075万人 7 1326 2320 3468 525 845 1075 FY 2016.6 FY 2017.6 FY 2018.6 FY 2018.6 売上高 334億円 122 212 334 FY 2016.6 単位:億円 単位:億円 単位:万人 FY 2017.6
  3. $ git log --reverse commit 444da10d96299849be93aa7928b50182be1069aa Author: Tatsuya Tsuruoka Date:

    Sat Apr 6 16:46:32 2013 +0900 Initial checkin .gitignore ... dietcake/LICENSE dietcake/README.md dietcake/core/controller.php 最初のコミット
  4. 新しいPHPで動かなくなるリスク PHP 7.2 で PHPUnit 4を動かすとDeprecatedエラー ➔ 将来のPHPで動かなくなる可能性あり $ php

    vendor/bin/phpunit PHP Deprecated: The each() function is deprecated. This message will be suppressed on further calls in vendor/phpunit/phpunit/src/Util/Getopt.php on line 38
  5. 新しいPHPUnitの利点 コードがモダンになり、可読性や拡張性が向上 ◦ 名前空間導入 ◦ self::assertThat() → static::assertThat() ◦ Exception

    → Throwable ◦ 型宣言導入 ◦ Abstract → Interface + Trait 細かい新機能やパフォーマンスの向上
  6. どうやってPHPUnitのバージョンをあげるか 1. composer.jsonを編集 "phpunit/phpunit": "^4.0" → "^5.0" 2. composer update

    --with-dependencies phpunit/phpunit 3. こけたテストを修正 基本的にはこれだけ
  7. mercari-api という大規模プロジェクト ※ 2017年11月頃の状況 • テストファイル数 $ find app/tests tests

    -type f -name '*.php' | wc -l 1,310 • テスト行数 $ find app/tests tests -type f -name '*.php' | xargs wc -l | tail -n 1 243,802 total
  8. 並列ブランチの問題 FeatureB 開発 composer.lockを変更 ・・・ phpunit5 平行する 数十ブランチ FeatureA 開発

    PHPUnit 4 前提のテストコードを追加 master (PHPUnit 4) • composer update • テスト大量修正
  9. 具体的な手順 1. 互換レイヤを作る 2. テストコードの前方互換を達成する (PHPUnit N と N+1でパス) 3.

    master で前方互換状態が崩れたら、都度修正 4. PHPUnit N+1 用のテストが他の開発者のブランチに浸透するまで 2-3 を繰り返す 5. composer updateして PHPUnitのバージョンをあげる
  10. 本番環境へ影響がでるかも疑惑 • 一部の本番環境で、composer install --no-dev つけていなかった • 本番サーバに "require-dev"パッケージがインストールされている! •

    composer update --with-dependenciesすると、本番アプリケーションの 挙動が変わる可能性 ➔ 気軽にPHPUnitのバージョンをあげられない
  11. どういうこと? 例 • phpunit ver5をrequire-devに宣言してインストールすると、 symfony/yamlもインストールされる。 (phpunit 5はsymfony/yamlに依存してるので) • プロダクションコードで

    Yaml::parse()などを使えてしまう。 • この状態で phpunitをver5→6にあげると、symfony/yamlが消えてしまう (phpunit 6はsymfony/yamlをrequireしてないので) • Yaml::parse()使ってる箇所で Fatal Error
  12. PHPUnit 4 -> 5 引数付きgetMock() はgetMockBuilder() に置き換える $mock = $this->getMock(Foo::class,

    ['methodA'], ['val']); $mock = $this->getMockBuilder(Foo::class) ->setMethods(['methodA']) ->setConstructorArgs(['val']) ->getMock();
  13. PHPUnit 4 -> 5 引数なしgetMock() をcreateMock() に置き換えたい $mock = $this->getMock(Foo::class);

    $mock = $this->createMock(Foo::class); → createMock()はPHPUnit4では存在しない
  14. PHPUnit 4 -> 5 前方互換レイヤを作る protected function createMock($className) { if

    (method_exists(PHPUnit_Framework_TestCase::class, 'createMock')) { // for PHPUnit 5 return parent::createMock($className); } else { // for PHPUnit 4 return $this->getMock($className); } }
  15. PHPUnit 5 -> 6 シェル芸で置換 $ find tests -type f

    -name '*.php' \ | xargs perl -pi -e 's/use PHPUnit_Framework_TestCase/use PHPUnit\Framework\TestCase/g' $ find tests -type f -name '*.php' \ | xargs perl -pi -e 's/extends PHPUnit_Framework_TestCase/extends TestCase/g' -use PHPUnit_Framework_TestCase; +use PHPUnit\Framework\TestCase; -class MercariTestCase extends PHPUnit_Framework_TestCase +class MercariTestCase extends TestCase
  16. PHPUnit 5 -> 6 前方互換レイヤの足りない分は自作 // Forward Compatibility to PHPUnit6

    namespace PHPUnit\Framework\Constraint; if (!class_exists('PHPUnit\\Framework\\Constraint\\Constraint', true)) { class_alias('PHPUnit_Framework_Constraint', 'PHPUnit\\Framework\\Constraint\\Constraint'); } bootstrapファイルとかに書く
  17. PHPUnit 5 -> 6 TestListenerのシグネチャ問題 use PHPUnit\Framework\TestListener ; use PHPUnit_Framework_TestSuite

    as TestSuite; class MyTestListener implements TestListener { public function startTestSuite (TestSuite $suite) 前方互換レイヤーを使おうとするとシグネチャ不一致エラー。 引数の反変(contravariant)に違反するため。 前方互換レイヤーを使わずにお茶を濁した。
  18. PHPUnit 5 -> 6 setExpectedException() 廃止 $this->setExpectedException(\RuntimeException::class); /** * @expectedException

    \RuntimeException */ @expectedException系 アノテーションに置き換える
  19. PHPUnit 5 -> 6 setExpectedException() 廃止 $this->setExpectedException( \RuntimeException::class, 'something wrong',

    500); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage( 'something wrong'); $this->expectExceptionCode(500); もしくは expectExceptionXXX() を使う
  20. PHPUnit 5 -> 6 $ ./composer.phar update --with-dependencies phpunit/phpunit Problem

    1 - Installation request for friendsofphp/php-cs-fixer 1.11.8 -> satisfiable by friendsofphp/php-cs-fixer[v1.11.8]. - phpunit/phpunit 6.4.0 requires sebastian/diff ^2.0 -> satisfiable by sebastian/diff[2.0.1]. ... - Conclusion: don't install sebastian/diff 2.0.1 composer updateがエラー
  21. PHPUnit 5 -> 6 $ ./composer.phar why-not sebastian/diff:2 friendsofphp/php-cs-fixer v1.11.8

    requires sebastian/diff (~1.1) composer why-notで原因調査
  22. PHPUnit 5 -> 6 php-cs-fixer v1とphpunit 6.4が共存不能 • php-cs-fixer v1

    は sebastien/diff v1に依存 • phpunit v6.4 は sebastien/diff v2に依存 php-cs-fixer v2にアップグレードする・・? → 今はNo → php-cs-fixerをcomposer.jsonから除外して、phar版を 使うことで解決
  23. PHPUnit 6 -> 7 abstract BaseTestListener が廃止に 代わりに interface と

    trait (TestListener, TestListenerDefaultImplementation) を使用する
  24. PHPUnit 6 -> 7 class Constraint protected function matches($other): bool

    interface SelfDescribing public function toString(): string いくつかのクラスで戻り値型宣言が追加
  25. PHPUnit 6 -> 7 前方互換したいが、これは動くのか? public function toString() public function

    toString(): string 親 子 ➔ 動く。戻り値の共変(covariant)はOK
  26. PHPUnit 6 -> 7 DbUnit ver 3 -> 4 protected

    function setUp(): void protected function tearDown(): void 全TestCaseに影響 → 面倒なので自動化 https://gist.github.com/DQNEO/5471032715ada025dee51be0b6568932
  27. (ちなみにPHPUnit 8) protected function setUp(): void protected function tearDown(): void

    voidがつく予定 https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/TestCase.php#L407
  28. PHPUnit 6 -> 7 • PHPUnit 6, 7 両方で動くテストコードになった ➔

    特別な互換レイヤは不要 • composer update
  29. PHPUnit 4 -> 5 -> 6 -> 7 完了! JP,

    US, UK, SET, SRE, その他 協力してくれた全ての同僚に感謝
  30. DietCake (弊社で大規模利用しているWAF) PHPUnit 4 > 5 > 6 > 7

    https://github.com/dietcake/dietcake/pull/27 https://github.com/dietcake/dietcake/pull/30
  31. AWS SDK for PHP AWS公式ライブラリ PHPUnit 5 > 6 (まだ道半ば)

    https://github.com/aws/aws-sdk-php/pull/1519 https://github.com/aws/aws-sdk-php/pull/1604 https://github.com/aws/aws-sdk-php/pull/1605