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

7b606c5039f083d13e2d2320ce6ddcfa?s=47 DQNEO
December 15, 2018

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

PHPConference 2018の発表資料です。

7b606c5039f083d13e2d2320ce6ddcfa?s=128

DQNEO

December 15, 2018
Tweet

Transcript

  1. 大規模PHPプロジェクトで PHPUnitを 3世代アップグレードするため にやったこと 2018 PHPConference @DQNEO (Kashiwagi Daisuke)

  2. 自己紹介 @DQNEO (どきゅねお) US版メルカリのバックエンドエンジニア PHP: Contributed to Ethna, DietCube, DietCake,

    Symfony Go: Go compiler by Go https://github.com/DQNEO/minigo
  3. メルカリについて

  4. メルカリとは フリマアプリ「メルカリ」は、スマホで簡単に売り買いを楽しめるマーケット プレイスです。 4

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

    anyone can buy & sell Be Professional プロフェッショナルであれ All for One 全ては成功のために Go Bold 大胆にやろう 5
  6. 株式会社メルカリ 2013年2月1日 東京、仙台、福岡、 Palo Alto、Portland、 London 約1,350名(連結) 従業員数 オフィス 会社設立日

    6
  7. メルカリ日本事業 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
  8. 海外展開 2014年にUS、2017年にUKでサービスを開始。 USでの成功が、重要なマイルストーンであると認識し注力しています。 Mercari US Mercari UK 8

  9. PHPとメルカリとOSS

  10. 「PHPからメルカリが 生まれました」 by @tsuruoka

  11. $ 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 最初のコミット
  12. https://speakerdeck.com/ttsuruoka/merukarifalsechao-gao-su-kai-fa-wozhi-eruphp-phpcon2014 メルカリの超高速開発を支えるPHP (PHPCon2014)

  13. メルカリは、これからもPHPと関 わっていきます。そして、PHPと PHPコミュニティを支援していき ます。

  14. ありがとうPHP

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

  16. 今日お話すること PHPUnitの紹介 アップグレードをなぜやるのか 大規模プロジェクトで起きた問題と解決法 v4->5->6->7 で具体的にやったこと OSSへの貢献 質疑応答

  17. PHPUnitの紹介

  18. PHPUnitの紹介 UnitTestを書くためのフレームワーク 有名ライブラリの多くで採用されている Symfony, Laravel, Guzzle, Monolog, Composer, etc Webアプリケーションのテスト基盤としても使われる

  19. アップグレードを なぜやるのか

  20. アップグレードをなぜやるのか • サポート切れ • 新しいPHPで動かなくなるリスク • 新しいPHPUnitの方がよくなっている

  21. PHPUnit 4 と 5 は既にサポート切れ https://phpunit.de/supported-versions.html

  22. PHPUnit 6 はもうすぐサポート切れ https://phpunit.de/supported-versions.html

  23. 新しい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
  24. 新しいPHPUnitの利点 コードがモダンになり、可読性や拡張性が向上 ◦ 名前空間導入 ◦ self::assertThat() → static::assertThat() ◦ Exception

    → Throwable ◦ 型宣言導入 ◦ Abstract → Interface + Trait 細かい新機能やパフォーマンスの向上
  25. どうやって バージョンを上げるか

  26. どうやってPHPUnitのバージョンをあげるか 1. composer.jsonを編集 "phpunit/phpunit": "^4.0" → "^5.0" 2. composer update

    --with-dependencies phpunit/phpunit 3. こけたテストを修正 基本的にはこれだけ
  27. 大規模プロジェクトで 起きた問題

  28. mercari-api という大規模プロジェクト メルカリの主要コンポーネント コミッターが3ヶ国、数十人 1週間でPR 75 件 ※ 2017年11月頃の状況

  29. 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
  30. 大規模環境で遭遇した問題点 • テストコード修正が大量 ➔ 並列ブランチ・他の開発者への影響 • 本番環境に影響あるかも疑惑 • composer updateがエラー

    (バージョン制約の問題)
  31. 並列ブランチの問題 master (PHPUnit 4)

  32. 並列ブランチの問題 phpunit5 master (PHPUnit 4)

  33. 並列ブランチの問題 • composer update • テスト大量修正 phpunit5 master (PHPUnit 4)

  34. 並列ブランチの問題 • composer update • テスト大量修正 phpunit5 平行する 数十ブランチ master

    (PHPUnit 4)
  35. 並列ブランチの問題 FeatureA 開発 PHPUnit 4 向けのテストコードを追加 phpunit5 平行する 数十ブランチ master

    (PHPUnit 4) • composer update • テスト大量修正
  36. 並列ブランチの問題 FeatureB 開発 composer.lockを変更 ・・・ phpunit5 平行する 数十ブランチ FeatureA 開発

    PHPUnit 4 前提のテストコードを追加 master (PHPUnit 4) • composer update • テスト大量修正
  37. ジレンマ 他の開発者は旧PHPUnit前提でテストを書くので、 • masterに旧スタイルのテストが追加され続ける • 新phpunitブランチをmasterにマージすると、他の開発者の テストを壊してしまう

  38. composer.lockコンフリクト問題 一つのブランチで長いこと作業していると、composer.lock のコンフリクトが頻繁に起こる ➔ composer updateやりなおし ➔ composer.lockのコードレビューやり直し

  39. 戦略を考える • 大規模環境では一つのPRで長く作業すると泥沼化しや すい • 立ち止まって戦略を考えました

  40. None
  41. TickTock Model https://en.wikipedia.org/wiki/Tick%E2%80%93tock_model#cite_note-IntelTickTockModel-1

  42. IntelのTickTock Model 半導体の密度 アーキテクチャの刷新 1年半ごとに 別の軸を進化させる

  43. 別の言い方 • 難問は分割せよ • ひとつのことをうまくやれ

  44. PHPUnit更新に応用 • テストコードを修正 • PHPUnitのバージョン上げる を別々に行う

  45. PHPUnit更新に応用 • 旧PHPUnitに依存した状態のまま • 互換レイヤーを作り • テストコードを新PHPUnit向けに書き換える Forward Compatible (前方互換)

  46. 具体的な手順 1. 互換レイヤを作る 2. テストコードの前方互換を達成する (PHPUnit N と N+1でパス) 3.

    master で前方互換状態が崩れたら、都度修正 4. PHPUnit N+1 用のテストが他の開発者のブランチに浸透するまで 2-3 を繰り返す 5. composer updateして PHPUnitのバージョンをあげる
  47. PHPUnit更新に応用 テストコードの修正 PHPUnitのバージョン ver 4 ver 5 ver 6

  48. Master 平行ブランチ問題を解決 テストコードの修正のみを行う (PHPUnit 5用に)

  49. Master FeatureA 開発 PHPUnit 4用のテストが増える 平行ブランチ問題を解決 テストコードの修正のみを行う (PHPUnit 5用に)

  50. FeatureB 開発 composer.lockに変更が入る Master FeatureA 開発 PHPUnit 4前提のテストコードが増える ・・・ 平行ブランチ問題を解決

    テストコードの修正のみを行う (PHPUnit 5用に)
  51. Master 平行ブランチ問題を解決 composer update (PHPUnit 4 ➔ 5)

  52. 平行ブランチ問題を解決 • 他の開発者への影響を極小化できる • テストコード修正をこまめにmasterにマージできる • composer.lockがコンフリクトしない

  53. 本番環境へ影響が でるかも疑惑

  54. PHPUnitのバージョンをあ げるだけで 本番に影響?

  55. 本番環境へ影響がでるかも疑惑 • 一部の本番環境で、composer install --no-dev つけていなかった • 本番サーバに "require-dev"パッケージがインストールされている! •

    composer update --with-dependenciesすると、本番アプリケーションの 挙動が変わる可能性 ➔ 気軽にPHPUnitのバージョンをあげられない
  56. どういうこと? 例 • 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
  57. devパッケージが本番で使われてる? require-devパッケージのクラスがプロダクションコードから呼 ばれてないか、全件調査した。 結果、呼ばれていなかった。

  58. composer install "--no-dev" のあるべき姿 これで、うっかりdevパッケージ依存のコードを書いてしまっても、 QA環境で検出できる 環境 インストール方法 本番 composer

    install --no-dev QA composer install --no-dev CI composer install ローカル composer install
  59. これにより require-devパッケージのupdateは気軽にできるようになった。 検証方法: comopser update 前後のcomposer.lockを使って composer install --no-devしてみて、./vendor に変更がなければOK

    本番への影響を簡単に検証可能
  60. ここまでのまとめ • TickTock Model 「テストコードの修正」と「PHPUnitバージョンアップ」を分け る • "Forward Compatible" (前方互換)を達成する

    • 本番環境とQA環境では composer install --no-dev
  61. その他のTips • 実行したコマンドをコミットメッセージに入れる "php ./composer.phar update --with-dependencies phpunit/phpunit phpunit/dbunit" •

    大量のテストコード修正はスクリプトで自動化
  62. ver 4 -> 5 -> 6 -> 7の 各アップグレードで 具体的にやったこと

  63. PHPUnit 4 -> 5

  64. PHPUnit 4 -> 5 getMock()がDeprecatedになる。 createMock() か getMockBuilder() に置き換える。 (もしくはこれを機に

    Prophecy に置き換えるのもアリ)
  65. PHPUnit 4 -> 5 引数付きgetMock() はgetMockBuilder() に置き換える $mock = $this->getMock(Foo::class,

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

    $mock = $this->createMock(Foo::class); → createMock()はPHPUnit4では存在しない
  67. 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); } }
  68. PHPUnit 4 -> 5 • PHPUnit 4,5 両方で動くコードになった • composer

    update • 互換レイヤを削除
  69. PHPUnit 5 -> 6

  70. PHPUnit 5 -> 6 • 名前空間の導入 ◦ TestListenerのシグネチャ問題 • setExpectedException()

    廃止 • php-cs-fixer v1との共存不能
  71. PHPUnit 5 -> 6 名前空間の導入 PHPUnit_Framework_TestSuite PHPUnit\Framework\TestSuite ver 5に前方互換レイヤが用意されてるのでそれを使う https://github.com/sebastianbergmann/phpunit/tree/5.7.27/src/ForwardCompatibility

  72. 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
  73. 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ファイルとかに書く
  74. PHPUnit 5 -> 6 TestListenerのシグネチャ問題 use PHPUnit\Framework\TestListener ; use PHPUnit_Framework_TestSuite

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

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

    500); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage( 'something wrong'); $this->expectExceptionCode(500); もしくは expectExceptionXXX() を使う
  77. PHPUnit 5 -> 6 • PHPUnit 5,6 両方で動くようにテストコードを修正 • composer

    update
  78. 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がエラー
  79. 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で原因調査
  80. 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版を 使うことで解決
  81. PHPUnit 5 -> 6

  82. PHPUnit 6 -> 7

  83. PHPUnit 6 -> 7 • BaseTestListenerが廃止に • いくつかのクラスで戻り値型宣言が必要に • DbUnitを使っている場合、

    setUp(), tearDown()の :void 宣言が必要に
  84. PHPUnit 6 -> 7 abstract BaseTestListener が廃止に 代わりに interface と

    trait (TestListener, TestListenerDefaultImplementation) を使用する
  85. PHPUnit 6 -> 7

  86. PHPUnit 6 -> 7 class Constraint protected function matches($other): bool

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

    toString(): string 親 子
  88. PHPUnit 6 -> 7 前方互換したいが、これは動くのか? public function toString() public function

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

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

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

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

    US, UK, SET, SRE, その他 協力してくれた全ての同僚に感謝
  93. OSSへの貢献

  94. OSSへの貢献 最新のPHPUnitに追従できてないOSSは多い これまでのノウハウを適用可能 PRを送りつけた

  95. OSSへの貢献 マージ済みのPRを紹介します

  96. DietCube (弊社製のWAF) PHPUnit 5 > 6 > 7 https://github.com/mercari/dietcube/pull/26 https://github.com/mercari/dietcube/pull/29

  97. DietCake (弊社で大規模利用しているWAF) PHPUnit 4 > 5 > 6 > 7

    https://github.com/dietcake/dietcake/pull/27 https://github.com/dietcake/dietcake/pull/30
  98. Monolog PHPUnit 5 > 6 https://github.com/Seldaek/monolog/pull/1133

  99. PHPBench PHPUnit 6 > 7 https://github.com/phpbench/phpbench/pull/528

  100. AssertChain PHPUnit 4 > 5 > 6 https://github.com/gong023/assert_chain/pull/3

  101. Karen PHPUnit 5 > 6 https://github.com/brtriver/karen/pull/8

  102. Chronous PHPUnit 6 > 7 https://github.com/cakephp/chronos/pull/167

  103. 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
  104. 最後に みなさんも、ライブラリのアップグレードではまったり問題解決 したら、ぜひブログや勉強会で知見を共有してみてください。 そしてOSSに還元しましょう!

  105. ご清聴 ありがとう ございました