Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介 @DQNEO (どきゅねお) US版メルカリのバックエンドエンジニア PHP: Contributed to Ethna, DietCube, DietCake, Symfony Go: Go compiler by Go https://github.com/DQNEO/minigo

Slide 3

Slide 3 text

メルカリについて

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

ミッションとバリュー 新たな価値を生みだす 世界的なマーケットプレイスを創る Create value in a global marketplace where anyone can buy & sell Be Professional プロフェッショナルであれ All for One 全ては成功のために Go Bold 大胆にやろう 5

Slide 6

Slide 6 text

株式会社メルカリ 2013年2月1日 東京、仙台、福岡、 Palo Alto、Portland、 London 約1,350名(連結) 従業員数 オフィス 会社設立日 6

Slide 7

Slide 7 text

メルカリ日本事業 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

Slide 8

Slide 8 text

海外展開 2014年にUS、2017年にUKでサービスを開始。 USでの成功が、重要なマイルストーンであると認識し注力しています。 Mercari US Mercari UK 8

Slide 9

Slide 9 text

PHPとメルカリとOSS

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

$ 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 最初のコミット

Slide 12

Slide 12 text

https://speakerdeck.com/ttsuruoka/merukarifalsechao-gao-su-kai-fa-wozhi-eruphp-phpcon2014 メルカリの超高速開発を支えるPHP (PHPCon2014)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

ありがとうPHP

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

PHPUnitの紹介

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

新しい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

Slide 24

Slide 24 text

新しいPHPUnitの利点 コードがモダンになり、可読性や拡張性が向上 ○ 名前空間導入 ○ self::assertThat() → static::assertThat() ○ Exception → Throwable ○ 型宣言導入 ○ Abstract → Interface + Trait 細かい新機能やパフォーマンスの向上

Slide 25

Slide 25 text

どうやって バージョンを上げるか

Slide 26

Slide 26 text

どうやってPHPUnitのバージョンをあげるか 1. composer.jsonを編集 "phpunit/phpunit": "^4.0" → "^5.0" 2. composer update --with-dependencies phpunit/phpunit 3. こけたテストを修正 基本的にはこれだけ

Slide 27

Slide 27 text

大規模プロジェクトで 起きた問題

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

大規模環境で遭遇した問題点 ● テストコード修正が大量 ➔ 並列ブランチ・他の開発者への影響 ● 本番環境に影響あるかも疑惑 ● composer updateがエラー (バージョン制約の問題)

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

並列ブランチの問題 ● composer update ● テスト大量修正 phpunit5 平行する 数十ブランチ master (PHPUnit 4)

Slide 35

Slide 35 text

並列ブランチの問題 FeatureA 開発 PHPUnit 4 向けのテストコードを追加 phpunit5 平行する 数十ブランチ master (PHPUnit 4) ● composer update ● テスト大量修正

Slide 36

Slide 36 text

並列ブランチの問題 FeatureB 開発 composer.lockを変更 ・・・ phpunit5 平行する 数十ブランチ FeatureA 開発 PHPUnit 4 前提のテストコードを追加 master (PHPUnit 4) ● composer update ● テスト大量修正

Slide 37

Slide 37 text

ジレンマ 他の開発者は旧PHPUnit前提でテストを書くので、 ● masterに旧スタイルのテストが追加され続ける ● 新phpunitブランチをmasterにマージすると、他の開発者の テストを壊してしまう

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

具体的な手順 1. 互換レイヤを作る 2. テストコードの前方互換を達成する (PHPUnit N と N+1でパス) 3. master で前方互換状態が崩れたら、都度修正 4. PHPUnit N+1 用のテストが他の開発者のブランチに浸透するまで 2-3 を繰り返す 5. composer updateして PHPUnitのバージョンをあげる

Slide 47

Slide 47 text

PHPUnit更新に応用 テストコードの修正 PHPUnitのバージョン ver 4 ver 5 ver 6

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Master 平行ブランチ問題を解決 composer update (PHPUnit 4 ➔ 5)

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

本番環境へ影響がでるかも疑惑 ● 一部の本番環境で、composer install --no-dev つけていなかった ● 本番サーバに "require-dev"パッケージがインストールされている! ● composer update --with-dependenciesすると、本番アプリケーションの 挙動が変わる可能性 ➔ 気軽にPHPUnitのバージョンをあげられない

Slide 56

Slide 56 text

どういうこと? 例 ● 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

Slide 57

Slide 57 text

devパッケージが本番で使われてる? require-devパッケージのクラスがプロダクションコードから呼 ばれてないか、全件調査した。 結果、呼ばれていなかった。

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

これにより require-devパッケージのupdateは気軽にできるようになった。 検証方法: comopser update 前後のcomposer.lockを使って composer install --no-devしてみて、./vendor に変更がなければOK 本番への影響を簡単に検証可能

Slide 60

Slide 60 text

ここまでのまとめ ● TickTock Model 「テストコードの修正」と「PHPUnitバージョンアップ」を分け る ● "Forward Compatible" (前方互換)を達成する ● 本番環境とQA環境では composer install --no-dev

Slide 61

Slide 61 text

その他のTips ● 実行したコマンドをコミットメッセージに入れる "php ./composer.phar update --with-dependencies phpunit/phpunit phpunit/dbunit" ● 大量のテストコード修正はスクリプトで自動化

Slide 62

Slide 62 text

ver 4 -> 5 -> 6 -> 7の 各アップグレードで 具体的にやったこと

Slide 63

Slide 63 text

PHPUnit 4 -> 5

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

PHPUnit 4 -> 5 引数付きgetMock() はgetMockBuilder() に置き換える $mock = $this->getMock(Foo::class, ['methodA'], ['val']); $mock = $this->getMockBuilder(Foo::class) ->setMethods(['methodA']) ->setConstructorArgs(['val']) ->getMock();

Slide 66

Slide 66 text

PHPUnit 4 -> 5 引数なしgetMock() をcreateMock() に置き換えたい $mock = $this->getMock(Foo::class); $mock = $this->createMock(Foo::class); → createMock()はPHPUnit4では存在しない

Slide 67

Slide 67 text

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); } }

Slide 68

Slide 68 text

PHPUnit 4 -> 5 ● PHPUnit 4,5 両方で動くコードになった ● composer update ● 互換レイヤを削除

Slide 69

Slide 69 text

PHPUnit 5 -> 6

Slide 70

Slide 70 text

PHPUnit 5 -> 6 ● 名前空間の導入 ○ TestListenerのシグネチャ問題 ● setExpectedException() 廃止 ● php-cs-fixer v1との共存不能

Slide 71

Slide 71 text

PHPUnit 5 -> 6 名前空間の導入 PHPUnit_Framework_TestSuite PHPUnit\Framework\TestSuite ver 5に前方互換レイヤが用意されてるのでそれを使う https://github.com/sebastianbergmann/phpunit/tree/5.7.27/src/ForwardCompatibility

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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ファイルとかに書く

Slide 74

Slide 74 text

PHPUnit 5 -> 6 TestListenerのシグネチャ問題 use PHPUnit\Framework\TestListener ; use PHPUnit_Framework_TestSuite as TestSuite; class MyTestListener implements TestListener { public function startTestSuite (TestSuite $suite) 前方互換レイヤーを使おうとするとシグネチャ不一致エラー。 引数の反変(contravariant)に違反するため。 前方互換レイヤーを使わずにお茶を濁した。

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

PHPUnit 5 -> 6 setExpectedException() 廃止 $this->setExpectedException( \RuntimeException::class, 'something wrong', 500); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage( 'something wrong'); $this->expectExceptionCode(500); もしくは expectExceptionXXX() を使う

Slide 77

Slide 77 text

PHPUnit 5 -> 6 ● PHPUnit 5,6 両方で動くようにテストコードを修正 ● composer update

Slide 78

Slide 78 text

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がエラー

Slide 79

Slide 79 text

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で原因調査

Slide 80

Slide 80 text

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版を 使うことで解決

Slide 81

Slide 81 text

PHPUnit 5 -> 6

Slide 82

Slide 82 text

PHPUnit 6 -> 7

Slide 83

Slide 83 text

PHPUnit 6 -> 7 ● BaseTestListenerが廃止に ● いくつかのクラスで戻り値型宣言が必要に ● DbUnitを使っている場合、 setUp(), tearDown()の :void 宣言が必要に

Slide 84

Slide 84 text

PHPUnit 6 -> 7 abstract BaseTestListener が廃止に 代わりに interface と trait (TestListener, TestListenerDefaultImplementation) を使用する

Slide 85

Slide 85 text

PHPUnit 6 -> 7

Slide 86

Slide 86 text

PHPUnit 6 -> 7 class Constraint protected function matches($other): bool interface SelfDescribing public function toString(): string いくつかのクラスで戻り値型宣言が追加

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

PHPUnit 6 -> 7 前方互換したいが、これは動くのか? public function toString() public function toString(): string 親 子 ➔ 動く。戻り値の共変(covariant)はOK

Slide 89

Slide 89 text

PHPUnit 6 -> 7 DbUnit ver 3 -> 4 protected function setUp(): void protected function tearDown(): void 全TestCaseに影響 → 面倒なので自動化 https://gist.github.com/DQNEO/5471032715ada025dee51be0b6568932

Slide 90

Slide 90 text

(ちなみにPHPUnit 8) protected function setUp(): void protected function tearDown(): void voidがつく予定 https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/TestCase.php#L407

Slide 91

Slide 91 text

PHPUnit 6 -> 7 ● PHPUnit 6, 7 両方で動くテストコードになった ➔ 特別な互換レイヤは不要 ● composer update

Slide 92

Slide 92 text

PHPUnit 4 -> 5 -> 6 -> 7 完了! JP, US, UK, SET, SRE, その他 協力してくれた全ての同僚に感謝

Slide 93

Slide 93 text

OSSへの貢献

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

DietCake (弊社で大規模利用しているWAF) PHPUnit 4 > 5 > 6 > 7 https://github.com/dietcake/dietcake/pull/27 https://github.com/dietcake/dietcake/pull/30

Slide 98

Slide 98 text

Monolog PHPUnit 5 > 6 https://github.com/Seldaek/monolog/pull/1133

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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