Pestãã¹ãã£ã³ã°èŠåŸ
ãã¹ããããŠããªãããŸãã¯éå°ã«ã¢ãã¯åãããå®è£ 詳现ãã¢ãµãŒãããã ãã®ããã¹ãæžã¿ãæ©èœã¯ããããã¯ã·ã§ã³ã§éãã«å£ãããªãã¡ã¯ã¿ãªã³ã°ã®ãã³ã«è æããŸãããã®ã¹ãã«ã¯ãæ¯ãèãã蚌æããç°åžžç³»ãšèªå¯ãã«ããŒãããªãã¡ã¯ã¿ãªã³ã°ã«èãããPest 3/4ãã¹ããäœæããããšã匷å¶ããŸãã
èœãšã穎
2ã€ã®é害ã¢ãŒãããããã©ã¡ããã³ã¹ããããããŸãïŒ
- ãã¹ãããªãïŒããããŒãã¹ã®ã¿ã®ãã¹ãã ãã¢ã§ã¯æ©èœããŸããããããã¯ã·ã§ã³ã§ã²ã¹ãããšã³ããã€ã³ããå©ããããã³ããå¥ã®ããã³ãã®ããŒã¿ãèªã¿åããããªããŒã·ã§ã³ã®æã穎ããŽãããŒã¿ã蚱容ãã404ã500ã«ãªããŸããèªå¯ã®æã穎ïŒ
403/404ãã¹ãã¢ãµãŒããããŠããªãïŒã¯ãããŒã¿æŒæŽ©ãæ¬çªç°å¢ã«ãªãªãŒã¹ãããåå ã§ãã - èããéå°ã«ã¢ãã¯åããããã¹ãã èªåèªèº«ã®
UserRepository/ãµãŒãã¹ãshouldReceive('save')->once()ã§ã¢ãã¯åãããšãå®éã®ã¯ãšãªãééã£ãŠããŠããã¹ãã¯æåãã誰ããã¡ãœããåã倿Žããç¬éã«ççºããŸãããã¹ãã¹ã€ãŒãã¯ã°ãªãŒã³ã§èª€ã£ãèªä¿¡ãäžããæ¬¡ã®ãªãã¡ã¯ã¿ãªã³ã°ã§ã€ã©ã€ã©ããåé€ãããã¢ãŒã1ã«éæ»ãããŸãã
ä¿®æ£æ¹æ³ã¯ã©ã¡ãã®å ŽåãåãèŠåŸã§ãïŒèªåãææããªãå¢çããã§ã€ã¯ããèªåãææããã³ãŒããå®è¡ãã芳枬å¯èœãªçµæïŒDBè¡ãHTTPã¹ããŒã¿ã¹ããã£ã¹ãããããããžã§ã/ã¡ãŒã«ïŒãã¢ãµãŒããã â 倱æãšèªå¯ã®çµæãå«ããŠã
ã«ãŒã«
- ãã¹ãã¯æ©èœãšäžç·ã«æžãããåŸã§ãã§ã¯ãªãã ã³ã³ãããŒã©ãŒ/ãžã§ã/ã³ãã³ã/ããªã·ãŒã®å€æŽã¯ãPestãã¹ããå®äºãããŸã§äžå®å šã§ããæ©èœãå®äºãããšå ±åããåã«ããã®æ¯ãèããã¢ãµãŒããã察å¿ãããã¹ãããªããã°ãªããŸããã
- Pestã®æ
£çšå¥ã䜿çšããã ã±ãŒã¹ã¯
it('...')ãŸãã¯test('...')ã§å®çŸ©ããã¢ãµãŒãã¯æµæ¢ãªexpect(...)ã§è¡ããã»ããã¢ããã¯beforeEach()ã§å ±æããŸãããã¡ã€ã«ãæ¢ã«ãã®ã¹ã¿ã€ã«ã䜿çšããŠããå Žåãé€ããPHPUnitã®class FooTest extends TestCaseã¯èšè¿°ããªãã§ãã ããã - é©åãªã¿ã€ããéžã¶ã HTTP/ãšã³ãããŒãšã³ãã®æ¯ãèã â ãã£ãŒãã£ãŒãã¹ãïŒãã¬ãŒã ã¯ãŒã¯ãèµ·åãã
$this->get/post/...ã§ã«ãŒããå©ãïŒããã¬ãŒã ã¯ãŒã¯/DBãªãã®çŽç²ãªããžã㯠â ãŠããããã¹ããè¿·ã£ããããã©ã«ãã§ãã£ãŒãã£ãŒãã¹ãã â ããå€ãã®åé¡ãææããŸãã - ãã¬ã€ãã§ããŒã¿ããŒã¹ããªã»ããããïŒ
uses()ã䜿çšïŒãtests/Pest.phpã§RefreshDatabaseïŒãã©ã³ã¶ã¯ã·ã§ã³ã©ããïŒãŸãã¯LazilyRefreshDatabaseïŒãã¹ããDBã«è§Šãããšãã®ã¿ãã€ã°ã¬ãŒãïŒãFeatureãã£ã¬ã¯ããªå šäœã«é©çšãããããã¡ã€ã«ã®å é ã§uses(RefreshDatabase::class)ãé©çšããŸãã絶察ã«ãã¹ããæ®åç¶æ ã«äŸåãããªãã§ãã ããã - ããŒã¿ã¯ãã¡ã¯ããªã§æ§ç¯ããææžãã®ã€ã³ãµãŒãã¯ããªãã
User::factory()->create()/->make()ãšãªã¬ãŒã·ã§ã³ã·ãããã¡ã¯ããªã䜿çšããŸãããã¹ãå ã§ã®DB::table()->insert([...])ãnew Model([...])->save()ã®ãããªææžãã®ãã«ãã¯èãããã£ã¹ã/ããã©ã«ããã¹ãããããããçŠæ¢ããŸãã - ããŒã¿ã»ããã§éè€ãæé€ããã³ããŒããŒã¹ãã¯ããªãã åãã¢ãµãŒã·ã§ã³ãå€ãã®å
¥åïŒæå¹/ç¡å¹ãªãã€ããŒããããŒã«ãããªã¯ã¹ïŒã§å®è¡ãããå Žåã¯ãNåã®ã»ãŒåäžã®ãã¹ãã®ä»£ããã«
->with([...])ïŒã€ã³ã©ã€ã³ãŸãã¯tests/Datasets/å ã®ååä»ãããŒã¿ã»ããïŒã䜿çšããŸãã - èªåãææããªãå¢çããã§ã€ã¯ããã å€éšãŸãã¯éåæã®ãã®ã«ã€ããŠã¯ãã¢ã¯ã·ã§ã³ã®åã«ãã¡ãµãŒããã§ã€ã¯ã䜿çšããŸãïŒ
Http::fake()ãQueue::fake()ãBus::fake()ãMail::fake()ãNotification::fake()ãEvent::fake()ãStorage::fake()ãæéã«ã€ããŠã¯ã$this->travel(...)ã$this->travelTo(...)ããŸãã¯$this->freezeTime()ã䜿çšããŸã â åçµãããŠããªãnow()ã«å¯ŸããŠã¢ãµãŒãããªãã§ãã ããã - èªåã®ã¢ãã«/ãµãŒãã¹ãç¡æå³ã«ãªããŸã§ã¢ãã¯åããªãã ãã¹ã察象ã®ã³ãŒãã«å¯ŸããŠ
Mockery::mock(MyService::class)->shouldReceive(...)ãModel::shouldReceive(...)ãé¿ããŸããå®éã®ã³ãŒãããã¹ãDBãšãã§ã€ã¯ããå¢çã«å¯ŸããŠå®è¡ãããŸããèªåã®ã¯ã©ã¹ãã¢ãã¯åããã®ã¯ãå®éã«å®è¡ããŠããªãçã®ç¶ãç®ïŒäŸïŒé ããµãŒãããŒãã£SDKã©ãããŒïŒã§ã®ã¿ã«ããŠãã ããã - çµæãã¢ãµãŒãããå
éšå®è£
ã¯ã¢ãµãŒãããªãã
assertDatabaseHas/assertDatabaseMissing/assertModelExistsãassertStatus/assertOk/assertCreatedãassertJson/assertJsonPathãassertRedirectãMail::assertSentãNotification::assertSentToãQueue::assertPushedãBus::assertDispatchedãStorage::disk()->assertExistsãåªå ããŸããæ¯ãèãã®ä»£çãšããŠãã¡ãœããXãäžåºŠåŒã°ããããã¢ãµãŒãããªãã§ãã ããã - åžžã«ç°åžžç³»ãšèªå¯ããã¹ãããã ãã¹ãŠã®ãšã³ããã€ã³ã/ã¢ã¯ã·ã§ã³ã«ã¯ãæäœéãããããŒãã¹ãããªããŒã·ã§ã³/倱æãã¹ããããŠèªå¯ãã¹ â ã²ã¹ã â
401/ãªãã€ã¬ã¯ããééã£ããŠãŒã¶ãŒ/ããŒã« â403ãååšããªã/å€éšãªãœãŒã¹ â404ãå¿ èŠã§ããã»ãã¥ãªãã£ã«ææãªæ©èœã«ã¯ããå¥ã®ãŠãŒã¶ãŒãããã«ã¢ã¯ã»ã¹/倿Žã§ããªãããšãããã¹ããæç€ºçã«è¿œå ããŸããããã¯ãæææš©ãããã³ã·ãŒã«é¢ãããã®ã§ã¯äº€æžã®äœå°ããããŸããã - ã¢ãŒããã¯ãã£ãã¹ããå®äŸ¡ãªã°ããŒãã«ã¬ãŒãã¬ãŒã«ãšããŠè¿œå ããã é©åãªããªã»ãããå«ã
tests/Arch.phpïŒãŸãã¯Pest.phpå ã®ã¢ãŒããã¯ãã£ã±ãŒã¹ïŒãç¶æããŸãïŒarch()->preset()->php()ïŒdie/var_dump/ddã¹ã¿ã€ã«ã®ãããã°åºåãšéæšå¥šã®PHP颿°ãçŠæ¢ïŒãarch()->preset()->security()ïŒevalãextractãunserializeã匱ãããã·ã¥/md5/sha1ãå®å šã§ãªãã©ã³ãã æ§ãªã©ãçŠæ¢ïŒãarch()->preset()->laravel()ïŒLaravelã®èŠçŽãåŒ·å¶ â èšå®å€ã§ã®env()ãçŠæ¢ïŒãarch('controllers')->expect('App\Http\Controllers')->toExtend('App\Http\Controllers\Controller')ã®ãããªãããžã§ã¯ãäžå€æ¡ä»¶ã远å ããŸãã - ãã¹ãããšã«1ã€ã®æ¯ãèãã説æçãªååã
it('blocks guests from the dashboard')ãšèšè¿°ããit('works')ãšã¯èšè¿°ããŸããããã¹ãåã«ãandããå¿ èŠãªããåå²ããŸãã - CIã§ã«ãã¬ããžãã²ãŒãããã
./vendor/bin/pest --coverage --min=NNãå®è¡ããŠæå°å€ã匷å¶ãã./vendor/bin/pest --type-coverageïŒã¿ã€ãã«ãã¬ããžãã©ã°ã€ã³ãå¿ èŠïŒãå®è¡ããŠåã·ã°ããã£ã匷å¶ããŸããæ°ãããã¹ãã®ãªãè¡ãæ¬ é¥ãšããŠæ±ããŸãã - ãã©ãŠã¶/E2Eã¯æ§ããã«ïŒPest 4ïŒã Pest 4ã¯
visit('/')->...->assertSee(...)ïŒPlaywrightããŒã¹ïŒãä»ããŠãã©ãŠã¶ãã¹ããæäŸããŸããçã®ãã«ã¹ã¿ãã¯/JSãããŒã®ããã«äºçŽããHTTPå±€ã®ã¢ãµãŒã·ã§ã³ãé ããã©ãŠã¶ãã¹ããšããŠåå®è£ ããªãã§ãã ããã
è¯ãäŸãšæªãäŸ
// tests/Feature/UpdatePostTest.php
use App\Models\Post;
use App\Models\User;
// â éå°ã¢ãã¯ïŒäœãèµ·ãã£ããã§ã¯ãªããã¡ãœãããåŒã°ããããšããã¹ãã
// ééã£ãæçš¿ãæŽæ°ãããããèªèšŒãæ¬ èœããŠããŠãæåããã
it('updates a post', function () {
$service = Mockery::mock(App\Services\PostService::class);
$service->shouldReceive('update')->once()->andReturnTrue();
$this->app->instance(App\Services\PostService::class, $service);
$this->put('/posts/1', ['title' => 'New'])->assertOk();
});
// â
å®éã®ã³ãŒãããã¹ãDBã«å¯ŸããŠå®è¡ããçµæãšèªå¯ãã¢ãµãŒãã
use App\Models\Post;
use App\Models\User;
it('lets the owner update their post', function () {
$owner = User::factory()->create();
$post = Post::factory()->for($owner)->create(['title' => 'Old']);
$this->actingAs($owner)
->put("/posts/{$post->id}", ['title' => 'New'])
->assertRedirect();
$this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'New']);
});
it('forbids a non-owner from updating the post', function () {
$post = Post::factory()->create(['title' => 'Old']);
$attacker = User::factory()->create();
$this->actingAs($attacker)
->put("/posts/{$post->id}", ['title' => 'Hacked'])
->assertForbidden(); // 403
$this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Old']);
});
it('blocks guests', function () {
$post = Post::factory()->create();
$this->put("/posts/{$post->id}", ['title' => 'New'])
->assertRedirect('/login'); // ãŸãã¯APIã®å Žåã¯assertUnauthorized()
});
// â å®éã®ãããã¯ãŒã¯ãå©ãïŒäžå®å®ãé
ããããŒæŒæŽ©ã®å¯èœæ§ããïŒ; æçšãªãã®ã¯äœãã¢ãµãŒãããªãã
it('notifies slack', function () {
$resp = (new App\Services\Slack)->ping('deploy ok');
expect($resp)->toBeArray();
});
// â
èªåãææããªãå¢çããã§ã€ã¯ããéä¿¡ãããªã¯ãšã¹ããã¢ãµãŒãã
use Illuminate\Support\Facades\Http;
it('posts the deploy message to slack', function () {
Http::fake(['hooks.slack.com/*' => Http::response(['ok' => true])]);
(new App\Services\Slack)->ping('deploy ok');
Http::assertSent(fn ($request) =>
$request->url() === 'https://hooks.slack.com/services/T/B/x'
&& $request['text'] === 'deploy ok'
);
});
// â ã³ããŒããŒã¹ããããã»ãŒåäžã®ããªããŒã·ã§ã³ãã¹ãã
it('rejects empty email', function () {
$this->post('/register', ['email' => ''])->assertSessionHasErrors('email');
});
it('rejects bad email', function () {
$this->post('/register', ['email' => 'nope'])->assertSessionHasErrors('email');
});
// â
1ã€ã®ãã¹ããããŒã¿ã»ããã«ããããŒãã«é§åã
it('rejects invalid emails', function (string $email) {
$this->post('/register', ['email' => $email])
->assertSessionHasErrors('email');
})->with([
'empty' => '',
'no at-sign' => 'nope',
'no domain' => 'a@',
]);
// â
éåæ+æéå¢çããã§ã€ã¯; ãã£ã¹ããããšå¯äœçšãã¢ãµãŒãã
use App\Jobs\SendReminder;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
it('queues a reminder and welcomes the user on signup', function () {
Queue::fake();
Mail::fake();
$this->freezeTime();
$this->post('/register', [
'name' => 'Ada', 'email' => '[email protected]', 'password' => 'secret-pass',
])->assertRedirect();
Queue::assertPushed(SendReminder::class);
Mail::assertSent(WelcomeMail::class, fn ($m) => $m->hasTo('[email protected]'));
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
// tests/Arch.php â â
å®äŸ¡ãªã°ããŒãã«ã¬ãŒãã¬ãŒã«
arch()->preset()->php(); // die/var_dump/ddã¹ã¿ã€ã«ã®ãããã°åºåãšéæšå¥šPHP颿°ãçŠæ¢
arch()->preset()->security(); // evalãextractãunserializeãmd5/sha1ã匱ãã©ã³ãã æ§ãªã©ãçŠæ¢
arch()->preset()->laravel(); // Laravelã®èŠçŽã匷å¶ïŒèšå®å€ã§ã®env()ãçŠæ¢ïŒ
arch('controllers extend the base controller')
->expect('App\Http\Controllers')
->toExtend('App\Http\Controllers\Controller');
arch('models live in the right namespace')
->expect('App\Models')
->toExtend('Illuminate\Database\Eloquent\Model');
ç¢ºèªæ¹æ³
æ©èœãšãã®ãã¹ããå®äºãããšäž»åŒµããåã«ã以äžãå®è¡ããŠãã ããïŒ
# 1. ã¹ã€ãŒãå
šäœãã°ãªãŒã³ã§ããããšã
./vendor/bin/pest
# 2. ã€ãã¬ãŒã·ã§ã³äžã¯æ°ãã/倿Žããããã¡ã€ã«ã ããå®è¡ã
./vendor/bin/pest tests/Feature/UpdatePostTest.php
# 3. ã«ãã¬ããžã®äžéã匷å¶ïŒXdebugãŸãã¯PCOVãå¿
èŠïŒã
./vendor/bin/pest --coverage --min=80
# 4. ã¿ã€ãã«ãã¬ããžïŒpestphp/pest-plugin-type-coverageãå¿
èŠïŒã
./vendor/bin/pest --type-coverage --min=100
# 5. ã¹ã¿ã€ã«ããããã§ããããšã
./vendor/bin/pint --test
次ã«ãèœãšã穎ãåå°å ¥ããŠããªãããç£æ»ããŸãïŒ
# èªå¯ã«ãã¬ããžïŒä¿è·ãããåãã£ãŒãã£ãŒãã¹ãã¯401/403/404ãã¹ãã¢ãµãŒãããã¹ãã
grep -rEn "assertForbidden|assertUnauthorized|assertNotFound|->assertStatus\\((401|403|404)\\)" tests/
# éå°ã¢ãã¯ã®åãïŒèªåã®APPã¯ã©ã¹ãã¢ãã¯åããã®ã¯å±éºä¿¡å· â åãããã確èªã
grep -rEn "Mockery::|->shouldReceive\(|::partialMock\(|::spy\\(" tests/
# ãã¡ã¯ããªã®ä»£ããã«ææžãã®ã€ã³ãµãŒã â ->factory()ã«çœ®ãæãã
grep -rEn "DB::table\(.*\)->insert|new App\\\\Models" tests/
# å€éšå¢çã¯ãã§ã€ã¯ãããªããã°ãªããªã â ã³ãŒããå€éšãåŒã³åºãå Žæã«ãã§ã€ã¯ãååšããããšã確èªã
grep -rEn "Http::fake|Queue::fake|Bus::fake|Mail::fake|Notification::fake|Storage::fake|Event::fake" tests/
# ã»ãã¥ãªãã£+PHPã¢ãŒãããªã»ããïŒå®äŸ¡ãªã°ããŒãã«ã¬ãŒãã¬ãŒã«ïŒãã©ããã«ååšããããšã
grep -rEn "preset\(\)->(security|php|laravel)\(\)" tests/
ã³ãŒãã ãã§ãªããã¹ãã§ã¢ãµãŒãïŒæ°ãããšã³ããã€ã³ãããšã«ã(a) ããããŒãã¹ã®çµæã¢ãµãŒã·ã§ã³ïŒassertDatabaseHas/assertOk/assertJsonPathïŒã(b) å°ãªããšã1ã€ã®å€±æ/ããªããŒã·ã§ã³ã¢ãµãŒã·ã§ã³ã(c) ã²ã¹ã+ééã£ããŠãŒã¶ãŒã®èªå¯ã¢ãµãŒã·ã§ã³ãååšããããšã確èªããŸãããããããæ¬ ããŠããå Žåããã®æ©èœã¯å®äºããŠããŸããã
ã«ãŒã«ãæ²ããŠãããå Žå
- çã®å€éšã®ç¶ãç®ã§ã®èªåã®ã³ãŒãã®ã¢ãã¯åã HTTPå±€ã§ãã§ã€ã¯ã§ããªãææãµãŒãããŒãã£SDKïŒSMSãæ¯æãã²ãŒããŠã§ã€ïŒã®èãã©ãããŒã¯ãæ£åœãª
Mockeryã¿ãŒã²ããã§ã â ãã¹ãå¯Ÿè±¡ã®æ¯ãèãããã§ã€ã¯ããã®ã§ã¯ãªãããããã®äŸåé¢ä¿ãéé¢ããŠããŸãã - DBãªãã§ã®çŽç²ãªããžãã¯ã®ãŠããããã¹ãã å€ãªããžã§ã¯ãããã©ãŒããã¿ãŒãèšç®æ©ã¯
RefreshDatabaseããã¡ã¯ããªãå¿ èŠãšããŸãã â çŽæ¥ã€ã³ã¹ã¿ã³ã¹åããŠexpect()ã§çµæãã¢ãµãŒãããŸãããããããéããŠæ£ããã§ãã - ãã¹ããæ°žç¶åããªãå Žåã®
create()ã®ä»£ããã®make()âUser::factory()->make()ã¯çŽç²ããžãã¯ã®ã¢ãµãŒã·ã§ã³ã®ããã«DBæžã蟌ã¿ãåé¿ããŸãã - ã¬ã¬ã·ãŒã³ãŒãã§ã®äœãã«ãã¬ããžéŸå€ã çŸå®çãª
--minãèšå®ãããã¹ãŠã®äœæ¥ããããã¯ããã®ã§ã¯ãªããæéããããŠæ®µéçã«åŒãäžããŸããã100%ããŒãããããã¹ãã®ãªãªãŒã¹ããŒãã«ããå£å®ã«ãªã£ãŠã¯ãããŸããã - çŽç²ã«JSé§åã®ãããŒïŒLivewire/Inertia/Vueã®ã€ã³ã¿ã©ã¯ã·ã§ã³ããã¡ã€ã«ããã«ãŒïŒã®ãã©ãŠã¶ãã¹ã â ããã§ã¯ã
visit()ã¯ããéãã§ã¯ãªããé©åãªããŒã«ã§ãã
åèæç®
- Pest â ãã¹ãã®æžãæ¹ãæåŸ å€ãããã¯: https://pestphp.com/docs/writing-tests
- Pest â ããŒã¿ã»ãã: https://pestphp.com/docs/datasets
- Pest â ã¢ãŒããã¯ãã£ãã¹ããšããªã»ããïŒ
securityãlaravelãtoExtendïŒ: https://pestphp.com/docs/arch-testing - Pest â ã«ãã¬ããžãš
--min: https://pestphp.com/docs/coverage - Pest â ã¿ã€ãã«ãã¬ããžãã©ã°ã€ã³: https://pestphp.com/docs/type-coverage
- Pest v4 â ãã©ãŠã¶ãã¹ãïŒ
visit()ãããžã¥ã¢ã«ãªã°ã¬ãã·ã§ã³ïŒ: https://pestphp.com/docs/browser-testing - Laravel â HTTPãã¹ãïŒã¹ããŒã¿ã¹/JSON/ãªãã€ã¬ã¯ãã¢ãµãŒã·ã§ã³ïŒ: https://laravel.com/docs/testing
- Laravel â ããŒã¿ããŒã¹ãã¹ãïŒ
RefreshDatabaseããã¡ã¯ããªãassertDatabaseHasïŒ: https://laravel.com/docs/database-testing - Laravel â ã¢ããã³ã°ãšãã¡ãµãŒããã§ã€ã¯ïŒ
HttpãQueueãBusãMailãNotificationãEventãStorageãæéïŒ: https://laravel.com/docs/mocking