#phperkaigi 2019でのLT内容です。 cakephp/chronosを利用して強いテスタビリティを手に入れる話
࣌Λࢧ͢ΔPHP#phperkaigi 2019.03.31Hideki Kinjyotwitter: @o0h_ / github: o0h
View Slide
ಥͰ͕͢
Έͳ͞Μࢥ͍·ͤΜ͔
͋ʙɺ͕࣌ؒͱ·Ε͍͍ͷʹɻ
Ͱ͢ΑͶʁ
ͦΕɺPHPͰͰ͖ΔΑʂ
ຊͷ͝հ
cakephp/chronos
ࣗݾհ• ίωώτגࣜձࣾ• αʔόʔαΠυΤϯδχΞ• ओʹCakePHPͳͲ• ࠷ۙɺ{ॳ}ॻ੶Λࣥච͠·ͨ͠͡Ί·ͯ͠ʂ
TL;DR• cakephp/chronos A standalone DateTime library originally based off of Carbon• Date(calendar) / DateTimeͷศརͳॲཧΛఏڙ͢Δ• ࣌ॲཧΛҕৡ͢Δ͜ͱͰɺ γεςϜ࣌ʹґଘ͠ͳ͍ʮݱࡏ࣌ʯΛॲཧ͕Մೳʹ
Α͋͘Γͦ͏ͳ
དྷ݄ɺզ͕͕ࣾECαΠτʮଠ܅ͷ͓หശʯΛ࢝Ίͯɺ7पͩɻײँͷؾ࣋ͪΛ͜ΊͨɺεϖγϟϧηʔϧΛΖ͏Αࣾͷਓ
͕ࣾɺϫλγʹͱͬͯΤϞ͍ࣄΛͤͯ͘Εͨͧɻ͜ΕઈରʹࣦഊͰ͖ͳ͍ɺ͕ΜΔ͔͠ͳ͍ʂ ৽ೖࣾһϫλγ
͋ʙΒΑͬʂͱɻ$template = 'itsumono_hanbai_page';if (time() >=strtotime(START_SALE_DATETIME_STR)) {$template = 'special_taro_sale';}render($template);
͜ΕͰɺʹͳͬͨΒϖʔδ͕ΓସΘΔɻྑ͔ͬͨɺྑ͍ײ͡ʹࣄ͕Ͱ͖ͨͧɻ৽ೖࣾһϫλγ
͊ɺ͕ΜͬͨͶʂ͏Ұਓલ͍͔ۙͳʁ(স)͜ΕͳΒɺΈΜͳେتͼʹҧ͍ͳ͍ɻ͘͠ͳΔͧʙʙʂཔΕΔઌഐ
ͱ͜ΖͰ͞ʂ͜Εͬͯɺ͏CI௨͔ͨ͠ͳʁͲ͏ͩͬͨʁཔΕΔઌഐ
͋ɺઌഐʂςετͬͯ͜ͱͰ͔͢ʁϩʔΧϧͰɺγεςϜλΠϜΛมߋͯ͠ಈ࡞֬ೝ͠·ͨ͠ɻ ৽ೖࣾһϫλγ
ͰɺΉΉΉɺ͔֬ʹςετίʔυɾɾɾrunkitͱ͔ೖΕͪΌ͍͍ͬͯͰ͔͢Ͷʁ৽ೖࣾһϫλγ
͏Μ͏Μɺͦ͏ͩΑͶɻͦΜͳ࣌ʹɺͱͬͯศརͳΜͷ͕͋ΔΜͩɻཔΕΔઌഐ
cakephp/chronos͞ʂ
৭ʑͱػೳ͕͋ΔΑཔΕΔઌഐ
ԼͷΒͷؔ৺ͱͯ͠ɺʮtestingaidʯػೳ͕ʹཱͪͦ͏͔ͩΒɺհ͢ΔͶɻཔΕΔઌഐ
setTestNow()
before$template = 'itsumono_hanbai_page';if (time() >=strtotime(START_SALE_DATETIME_STR)) {$template = 'special_taro_sale';}render($template);
·ͣɺʮݱࡏʯʹ͍ͭͯ ͯ͢Chronosܦ༝ͰऔΓग़͢Α͏ʹॻ͖͑ΔΑuse Cake\Chronos\Chronos;$template = 'itsumono_hanbai_page';if (Chronos::now()>= strtotime(START_SALE_DATETIME_STR)) {$template = 'special_taro_sale';}render($template);
͍ͭͰʹɺ ૬खํॻ͖͑ͯ͠·͓͏use Cake\Chronos\Chronos;$template = 'itsumono_hanbai_page';if (Chronos::now()>= new Chronos(START_SALE_DATETIME_STR)) {$template = 'special_taro_sale';}render($template);
ൺֱϝιουΛ͏ͱ ܕνΣοΫͰ͖ΔͷͰɺ͓ΈͰɻuse Cake\Chronos\Chronos;$template = 'itsumono_hanbai_page';if (Chronos::now()->gte(newChronos(START_SALE_DATETIME_STR))) {$template = 'special_taro_sale';}render($template);
ϓϩμΫτίʔυͷ४උͰ͖ͨɻͯ͞ɺ͜ΕͰςετ͕ॻ͚ΔΜͩΑʂཔΕΔઌഐ
testίʔυΛॻ͍ͯΈΔ/*** @test*/public function セール開始前(){Chronos::setTestNow('2019-03-10');$targetPageUrl = '/';$actual = get($targetPageUrl);$this->assertTitleIs('太郎君のお弁当箱',$actual);}
testίʔυΛॻ͍ͯΈΔ/*** @test*/public function セール開始前(){Chronos::setTestNow('2019-03-10');$targetPageUrl = '/';$actual = get($targetPageUrl);$this->assertTitleIs('太郎君のお弁当箱',$actual);}͜͜ʹೖΕ͕ͨ A$ISPOPTOPX AʹͳΔ
testίʔυΛॻ͍ͯΈΔ/*** @test*/public function セール期間中(){Chronos::setTestNow('2019-04-01');$targetPageUrl = '/';$actual = get($targetPageUrl);$this->assertTitleIs('太郎君のお弁当箱〜7周年ありがとう!〜',$actual);}֘ظؒʹ͓͍ͯ ϖʔδίϯςϯπ͕ ࠩ͠ସΘΔ͜ͱΛظ͢Δ
testίʔυΛॻ͍ͯΈΔ/*** @test*/public function セール終了後(){Chronos::setTestNow('2019-05-01 06:59:59’);$targetPageUrl = '/';$actual = get($targetPageUrl);$this->assertTitleIs(‘太郎君のお弁当箱',$actual);}࣌ࠁϨϕϧ·ͰݻఆՄೳ
͓ͬͱɺऴྃ࣌ͷઃఆΛͯ͠ͳ͔͔ͬͨͶʂͪΖΜɺ͙͢ʹඞཁͳ͍͚Ͳɺࠓͷ͏ͪʹ͓͍ͬͯͪΌ͑҆৺ͩͶʂཔΕΔઌഐ
Θɺ͋Ϳͳ͍ʂઌഐ͋Γ͕ͱ͏͍͟͝·͢ʂͬͺΓɺςετ͕(͔͠؆୯ʹ)ॻ͚Δͱ҆৺ײ͕͋Δͳʙ ৽ೖࣾһϫλγ
ͪͳΈʹɺ`setTestNow(null)` ͯ͋͛͠ΕɺϑΣΠΫ࣌Ͱͳ͘ݱ࣮ͷ͕࣌ฦͬͯ͘ΔͷͰ҆৺͞ɻςετͰ͏࣌ͱ͔ɺtearDown()ʹڬΜͰ͓͖͍ͨͶɻཔΕΔઌഐ
ଞʹศརͳ͜ͱ(࣌ؒ͋ΕͶ!)
৭ʑͱϔϧύʔͳͲ• DateTimeInterfaceDateTimeImmutableͷػೳͦͷ··͑ΔͷͰɺҠߦͦΜͳʹ͠ΜͲ͘ͳ͍ͣ• add/sub/diffͳͲͳͲ• ࣌ૢ࡞ʹɺʮࣗવจͬΆ͍දݱͰಡΈ͍͢ʯAPI͕૿͑Δҹ• ge/gte/lt/lte/between/wasWithinLastͳͲͷൺֱػೳ• addDays(int $value) / subMonths(int $value)ͳͲͷߋ৽ػೳ• Chronos::yesterday() / Chronos::instance(DatetimeInterface $dt)ͳͲͷϑΝΫτϦ
ΞάϨογϒͳ͍ํuse Cake\Chronos\Chronos;if (!function_exists('now')) {function now() {return Chronos::now();}}// usageif (now()->isSunday()) {goto doNidone();}
(ͪͳΈʹ)cakephpͷ߹• I18nܥύοέʔδͰܧঝͨ͠ɺTime/FrozenTime͕ར༻͞Ε͍ͯΔ• timezonelocaleͷใΛ͍࣋ͬͯΔ• https://github.com/cakephp/i18n/blob/master/FrozenTime.php• ORMܦ༝ͰRDBMS͔ΒऔΓग़ͨ͠datetimeྻͷɺࣗಈతʹίΠπʹϚʔγϟϧ͞ΕΔ
·ͱΊ
·ͱΊ(1/2)• ςετ = ҆৺ײ• ςελϒϧ = ઃܭͷɺςεςΟϯάϑϨʔϜϫʔΫ(etc)ଆͷදݱྗͷ• ࣌Λࢧ͢Δͱͱͬͯศར ಛʹςετɺ͜ΕͰTDDΓ͍ͧ͢ʂ
·ͱΊ(2/2)• cakephpνʔϜͷϝϯς͚ͩͲɺελϯυΞϩϯͰ͑ΔͷͰɺͲ͜ʹͰ࿈Ε͍͚ͯΔʂ• ʮਂ0࣌ʙਂ2࣌ͷؒ௨Βͳ͍ʯ͔Βͷଔۀɾɾɾʂ
͓͖߹͍͍͖ͨͩ ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ