Pest ํ ์คํธ ๊ท์จ
ํ ์คํธ๋์ง ์์ ๊ธฐ๋ฅ, ํน์ ๊ณผ๋ํ๊ฒ ๋ชจํน(mocking)๋์ด ๊ตฌํ ์ธ๋ถ ์ฌํญ์ ๋จ์ธ(assert)ํ๋ ํ ์คํธ ์ค์ํธ๋ ํ๋ก๋์ ์์ ์กฐ์ฉํ ๋ง๊ฐ์ง๊ณ ๋ชจ๋ ๋ฆฌํฉํ ๋ง ๋๋ง๋ค ์ฉ์ด๊ฐ๋๋ค. ์ด ์คํฌ์ ํ๋์ ์ฆ๋ช ํ๊ณ , ์คํจ ๊ฒฝ๋ก(unhappy path)์ ๊ถํ ๋ถ์ฌ(authorization)๋ฅผ ๋ค๋ฃจ๋ฉฐ, ๋ฆฌํฉํ ๋ง์๋ ์ด์๋จ๋ Pest 3/4 ํ ์คํธ๋ฅผ ์์ฑํ๋๋ก ํฉ๋๋ค.
ํจ์ (The footgun)
๋ ๊ฐ์ง ์คํจ ๋ชจ๋๊ฐ ์์ผ๋ฉฐ, ๋ ๋ค ๋น์ฉ์ด ๋ง์ด ๋ญ๋๋ค:
- ํ
์คํธ ์์ / ํดํผ ํจ์ค(happy-path) ์ ์ฉ ํ
์คํธ. ๋ฐ๋ชจ์์๋ ๊ธฐ๋ฅ์ด ์๋ํ์ง๋ง, ํ๋ก๋์
์์ ๊ฒ์คํธ๊ฐ ์๋ํฌ์ธํธ์ ์ ๊ทผํ๊ฑฐ๋, ํ
๋ํธ๊ฐ ๋ค๋ฅธ ํ
๋ํธ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฑฐ๋, ๊ฒ์ฆ ๊ณต๋ฐฑ์ผ๋ก ์ฐ๋ ๊ธฐ ๋ฐ์ดํฐ๊ฐ ๋ค์ด๊ฐ๊ฑฐ๋, 404๊ฐ 500์ด ๋ฉ๋๋ค. ๊ถํ ๋ถ์ฌ ๊ตฌ๋ฉ(
403/404๊ฒฝ๋ก๊ฐ ๋จ์ธ๋์ง ์์)์ ๋ฐ์ดํฐ ๋์ถ์ด ํ๋ก๋์ ์ผ๋ก ๋ฐฐ์ก๋๋ ๋ฐฉ์์ ๋๋ค. - ์ทจ์ฝํ๊ณ ๊ณผ๋ํ๊ฒ ๋ชจํน๋ ํ
์คํธ. ์์ฒด
UserRepository/์๋น์ค๋ฅผshouldReceive('save')->once()๋ก ๋ชจํนํ๋ฉด ์ค์ ์ฟผ๋ฆฌ๊ฐ ์๋ชป๋์ด๋ ํ ์คํธ๋ ํต๊ณผํ๊ณ , ๋๊ตฐ๊ฐ ๋ฉ์๋ ์ด๋ฆ์ ๋ฐ๊พธ๋ ์ฆ์ ํญ๋ฐํฉ๋๋ค. ์ค์ํธ๋ ๋ น์์ด๊ณ ๊ฑฐ์ง๋ ์์ ๊ฐ์ ์ฃผ๋ฉฐ, ๋ค์ ๋ฆฌํฉํ ๋ง์์ ์ข์ ๊ฐ์ ์ญ์ ๋ฉ๋๋ค โ ๋ค์ ๋ชจ๋ 1๋ก ๋์๊ฐ๋๋ค.
์ด๋ ์ชฝ์ด๋ ํด๊ฒฐ์ฑ ์ ๋์ผํ ๊ท์จ์ ๋๋ค: ์์ ํ์ง ์์ ๊ฒฝ๊ณ๋ ๊ฐ์ง๋ก(fake) ๋ง๋ค๊ณ , ์์ ํ ์ฝ๋๋ ์ค์ ๋ก ์คํํ๋ฉฐ, ๊ด์ฐฐ ๊ฐ๋ฅํ ๊ฒฐ๊ณผ(DB ํ, HTTP ์ํ, ๋์คํจ์น๋ ์์ /๋ฉ์ผ)๋ฅผ ๋จ์ธํ์ธ์ โ ์คํจ ๋ฐ ๊ถํ ๋ถ์ฌ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ์ฌ.
๊ท์น (Rules)
- ๊ธฐ๋ฅ๊ณผ ํจ๊ป ํ ์คํธ๋ฅผ ์์ฑํ๊ณ , "๋์ค์" ํ์ง ๋ง์ธ์. ์ปจํธ๋กค๋ฌ/์์ /๋ช ๋ น/์ ์ฑ ๋ณ๊ฒฝ์ Pest ํ ์คํธ๊ฐ ์์ ๋๊น์ง ์๋ฃ๋์ง ์์ ๊ฒ์ ๋๋ค. ๊ธฐ๋ฅ์ ํด๋น ๋์์ ๋จ์ธํ๋ ํ ์คํธ ์์ด ์๋ฃ๋์๋ค๊ณ ๋ณด๊ณ ํ์ง ๋ง์ธ์.
- Pest ๊ด์ฉ๊ตฌ๋ฅผ ์ฌ์ฉํ์ธ์. ์ผ์ด์ค๋ฅผ
it('...')๋๋test('...')๋ก ์ ์ํ๊ณ ,expect(...)๋ก ์ ์ฐฝํ๊ฒ ๋จ์ธํ๋ฉฐ,beforeEach()๋ก ์ค์ ์ ๊ณต์ ํ์ธ์. ํ์ผ์ด ์ด๋ฏธ PHPUnit ์คํ์ผ์ด ์๋๋ผ๋ฉดclass FooTest extends TestCase๋ฅผ ์์ฑํ์ง ๋ง์ธ์. - ์ฌ๋ฐ๋ฅธ ์ ํ์ ์ ํํ์ธ์. HTTP/์๋ ํฌ ์๋ ๋์ โ ๊ธฐ๋ฅ ํ
์คํธ(Feature test, ํ๋ ์์ํฌ๋ฅผ ๋ถํ
ํ๊ณ
$this->get/post/...๋ก ๋ผ์ฐํธ๋ฅผ ํธ์ถ). ํ๋ ์์ํฌ/DB๊ฐ ์๋ ์์ ๋ก์ง โ ๋จ์ ํ ์คํธ(Unit test). ์์ฌ์ค๋ฌ์ฐ๋ฉด ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๊ธฐ๋ฅ ํ ์คํธ๋ฅผ ์ฌ์ฉํ์ธ์ โ ๋ ๋ง์ ๊ฒ์ ์ก์๋ ๋๋ค. - ํธ๋ ์ดํธ(trait)์
uses()๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ฆฌ์ ํ์ธ์. ์ ์ฒดFeature๋๋ ํ ๋ฆฌ์ ๋ํดtests/Pest.php์์RefreshDatabase(ํธ๋์ญ์ ๋ํ) ๋๋LazilyRefreshDatabase(ํ ์คํธ๊ฐ DB์ ๋ฟ์ ๋๋ง ๋ง์ด๊ทธ๋ ์ด์ )๋ฅผ ์ ์ฉํ๊ฑฐ๋, ํ์ผ ์๋จ์uses(RefreshDatabase::class)๋ฅผ ์ฌ์ฉํ์ธ์. ์ ๋ DB ํ ์คํธ๋ฅผ ์์ฌ ์ํ( leftover state)์ ์์กดํ๊ฒ ๋์ง ๋ง์ธ์. - ๋ฐ์ดํฐ๋ฅผ ํฉํ ๋ฆฌ(factory)๋ก ๊ตฌ์ถํ๊ณ , ์๋ ์ฝ์
์ ์ ๋ ์ฌ์ฉํ์ง ๋ง์ธ์.
User::factory()->create()/->make()๋ฐ ๊ด๊ณ ํฉํ ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ธ์. ํ ์คํธ์์ ์๋์ผ๋กDB::table()->insert([...])๋๋new Model([...])->save()๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ทจ์ฝํ๊ณ ์บ์คํธ/๊ธฐ๋ณธ๊ฐ์ ๊ฑด๋๋๋๋ค โ ๊ธ์งํฉ๋๋ค. - ๋ฐ์ดํฐ์
(datasets)์ผ๋ก ์ค๋ณต ์ ๊ฑฐํ๊ณ , ๋ณต์ฌ-๋ถ์ฌ๋ฃ๊ธฐํ์ง ๋ง์ธ์. ๋์ผํ ๋จ์ธ์ด ๋ง์ ์
๋ ฅ(์ ํจ/๋ฌดํจ ํ์ด๋ก๋, ์ญํ ๋งคํธ๋ฆญ์ค)์ ๋ํด ์คํ๋ ๋๋ N๊ฐ์ ๊ฑฐ์ ๋์ผํ ํ
์คํธ ๋์
->with([...])(์ธ๋ผ์ธ ๋๋tests/Datasets/์ ๋ช ๋ช ๋ ๋ฐ์ดํฐ์ )์ ์ฌ์ฉํ์ธ์. - ์์ ํ์ง ์์ ๊ฒฝ๊ณ๋ ๊ฐ์ง๋ก ๋ง๋์ธ์(fake). ์ธ๋ถ ๋๋ ๋น๋๊ธฐ์ ์ธ ๊ฒ์ ๋ํด, ์์
์ ์ ํ์ฌ๋ ํ์ดํฌ๋ฅผ ์ฌ์ฉํ์ธ์:
Http::fake(),Queue::fake(),Bus::fake(),Mail::fake(),Notification::fake(),Event::fake(),Storage::fake(). ์๊ฐ(time)์ ๋ํด์๋$this->travel(...),$this->travelTo(...), ๋๋$this->freezeTime()์ ์ฌ์ฉํ์ธ์ โ ์ ๋ ๊ณ ์ ๋์ง ์์now()๋ฅผ ๋จ์ธํ์ง ๋ง์ธ์. - ์์ฒด ๋ชจ๋ธ/์๋น์ค๋ฅผ ๋ฌด์๋ฏธํ๊ฒ ๋ชจํนํ์ง ๋ง์ธ์. ํ
์คํธ ๋์ ์ฝ๋์ ๋ํด
Mockery::mock(MyService::class)->shouldReceive(...)๋ฐModel::shouldReceive(...)๋ฅผ ํผํ์ธ์. ์ค์ ์ฝ๋๊ฐ ํ ์คํธ DB์ ๊ฐ์ง ๊ฒฝ๊ณ์ ๋ํด ์คํ๋๋๋ก ๋์ธ์. ์์ฒด ํด๋์ค๋ฅผ ๋ชจํนํ๋ ๊ฒ์ ์ค์ ๋ก ์คํํ์ง ์๋ ์ง์ ํ ์ ํฉ์ (์: ๋๋ฆฐ ํ์ฌ SDK ๋ํผ)์์๋ง ๊ฐ๋ฅํฉ๋๋ค. - ๊ฒฐ๊ณผ(outcome)๋ฅผ ๋จ์ธํ๊ณ , ๋ด๋ถ(internals)๋ฅผ ๋จ์ธํ์ง ๋ง์ธ์.
assertDatabaseHas/assertDatabaseMissing/assertModelExists,assertStatus/assertOk/assertCreated,assertJson/assertJsonPath,assertRedirect,Mail::assertSent,Notification::assertSentTo,Queue::assertPushed,Bus::assertDispatched,Storage::disk()->assertExists๋ฅผ ์ ํธํ์ธ์. ํ๋์ ๋๋ฆฌ์๋ก์ "๋ฉ์๋ X๊ฐ ํ ๋ฒ ํธ์ถ๋์๋ค"๋ฅผ ๋จ์ธํ์ง ๋ง์ธ์. - ํญ์ ์คํจ ๊ฒฝ๋ก(unhappy path)์ ๊ถํ ๋ถ์ฌ(authorization)๋ฅผ ํ
์คํธํ์ธ์. ๋ชจ๋ ์๋ํฌ์ธํธ/์์
์๋ ์ต์ํ ๋ค์์ด ํ์ํฉ๋๋ค: ํดํผ ํจ์ค, ๊ฒ์ฆ/์คํจ ๊ฒฝ๋ก, ๋ฐ ์ธ์ฆ ๊ฒฝ๋ก โ ๊ฒ์คํธ โ
401/๋ฆฌ๋ค์ด๋ ํธ, ์๋ชป๋ ์ฌ์ฉ์/์ญํ โ403, ๋๋ฝ๋/์ธ๋ถ ๋ฆฌ์์ค โ404. ๋ณด์์ ๋ฏผ๊ฐํ ๊ธฐ๋ฅ์๋ ๋ช ์์ ์ธ "๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ์ด์ ์ ๊ทผ/์์ ํ ์ ์์" ํ ์คํธ๋ฅผ ์ถ๊ฐํ์ธ์. ์ด๋ ์์ ๊ถ ๋๋ ํ ๋์์ ๊ด๋ จ๋ ๋ชจ๋ ๊ฒ์ ๋ํด ์๋ณดํ ์ ์์ต๋๋ค. - ์ ๋ ดํ ์ ์ญ ์์ ๋๊ฐ์ผ๋ก ์ํคํ
์ฒ ํ
์คํธ๋ฅผ ์ถ๊ฐํ์ธ์.
tests/Arch.php(๋๋Pest.php์ arch ์ผ์ด์ค)์ ์ ํฉํ ํ๋ฆฌ์ ์ ์ ์งํ์ธ์: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')์ ๊ฐ์ ํ๋ก์ ํธ ๋ถ๋ณ ์กฐ๊ฑด(invariants)์ ์ถ๊ฐํ์ธ์. - ํ
์คํธ๋น ํ๋์ ๋์, ์ค๋ช
์ ์ธ ์ด๋ฆ.
it('blocks guests from the dashboard'), ์ ๋it('works')๊ฐ ์๋๋๋ค. ํ ์คํธ ์ด๋ฆ์ "and"๊ฐ ํ์ํ๋ฉด ๋ถํ ํ์ธ์. - CI์์ ๊ฒ์ดํธ ์ปค๋ฒ๋ฆฌ์ง(gate coverage).
./vendor/bin/pest --coverage --min=NN์ ์คํํ์ฌ ์ต์๊ฐ์ ๊ฐ์ ํ๊ณ ,./vendor/bin/pest --type-coverage(type-coverage ํ๋ฌ๊ทธ์ธ ํ์)๋ฅผ ์คํํ์ฌ ํ์ ์๊ทธ๋์ฒ๋ฅผ ๊ฐ์ ํ์ธ์. ์๋ก์ด ํ ์คํธ๋์ง ์์ ์ค์ ๊ฒฐํจ์ผ๋ก ์ฒ๋ฆฌํ์ธ์. - ๋ธ๋ผ์ฐ์ /E2E๋ ๋๋ฌผ๊ฒ ์ฌ์ฉํ์ธ์ (Pest 4). Pest 4๋ ๋ธ๋ผ์ฐ์ ํ
์คํธ๋ฅผ
visit('/')->...->assertSee(...)(Playwright ๊ธฐ๋ฐ)๋ก ์ ๊ณตํฉ๋๋ค. ์ง์ ํ ํ์คํ/JS ํ๋ฆ์ ์ํด ์์ฝํ์ธ์; HTTP ๋ ์ด์ด ๋จ์ธ์ ๋๋ฆฐ ๋ธ๋ผ์ฐ์ ํ ์คํธ๋ก ๋ค์ ๊ตฌํํ์ง ๋ง์ธ์.
์ข์ ์ vs ๋์ ์
// 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');
});
// โ
ํ๋์ ํ
์คํธ, ๋ฐ์ดํฐ์
์ ์ฌ์ฉํ ํ
์ด๋ธ ๊ธฐ๋ฐ.
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');
ํ์ธ ๋ฐฉ๋ฒ (How to verify)
๊ธฐ๋ฅ๊ณผ ํ ์คํธ๊ฐ ์๋ฃ๋์๋ค๊ณ ์ฃผ์ฅํ๊ธฐ ์ ์ ๋ค์์ ์คํํ์ธ์:
# 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
๊ทธ๋ฐ ๋ค์ ํจ์ ์ ๋ค์ ๋์ ํ์ง ์์๋์ง ๊ฐ์ฌํ์ธ์:
# Authz ์ปค๋ฒ๋ฆฌ์ง: ๊ฐ ๋ณดํธ๋ ๊ธฐ๋ฅ ํ
์คํธ๋ 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) ์ต์ ํ๋์ ์คํจ/๊ฒ์ฆ ๋จ์ธ, (c) ๊ฒ์คํธ + ์๋ชป๋ ์ฌ์ฉ์ ๊ถํ ๋ถ์ฌ ๋จ์ธ์ด ์๋์ง ํ์ธํ์ธ์. ํ๋๋ผ๋ ๋น ์ง๋ฉด ๊ธฐ๋ฅ์ด ์๋ฃ๋ ๊ฒ์ด ์๋๋๋ค.
๊ท์น์ ์์ธ์ ์ผ๋ก ์ ์ฉํ ์ ์๋ ๊ฒฝ์ฐ (When it's OK to bend the rule)
- ์ง์ ํ ์ธ๋ถ ์ ํฉ์ ์์ ์์ฒด ์ฝ๋ ๋ชจํน. HTTP ๊ณ์ธต์์ ๊ฐ์ง๋ก ๋ง๋ค ์ ์๋ ์ ๋ฃ ํ์ฌ SDK(SMS, ๊ฒฐ์ ๊ฒ์ดํธ์จ์ด)๋ฅผ ๊ฐ์ผ ์์ ๋ํผ๋ ํฉ๋ฒ์ ์ธ
Mockery๋์์ ๋๋ค โ ํ ์คํธ ๋์ ๋์์ ๊ฐ์ง๋ก ๋ง๋๋ ๊ฒ์ด ์๋๋ผ ๊ทธ๋ค์ ์์กด์ฑ์ ๊ฒฉ๋ฆฌํ๋ ๊ฒ์ ๋๋ค. - DB ์์ด ์์ ๋ก์ง ๋จ์ ํ
์คํธ. ๊ฐ ๊ฐ์ฒด, ํฌ๋งทํฐ, ๊ณ์ฐ๊ธฐ๋
RefreshDatabase์ ํฉํ ๋ฆฌ๊ฐ ํ์ ์์ต๋๋ค โ ์ง์ ์ธ์คํด์คํํ๊ณexpect()๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฉํ์ธ์; ๋ ๋น ๋ฅด๊ณ ์ ํํฉ๋๋ค. - ํ
์คํธ๊ฐ ์ง์๋์ง ์์ ๋
create()๋์make()โUser::factory()->make()๋ ์์ ๋ก์ง ๋จ์ธ์ ์ํด DB ์ฐ๊ธฐ๋ฅผ ํผํฉ๋๋ค. - ๋ ๊ฑฐ์ ์ฝ๋์ ๋ํ ๋ฎ์ ์ปค๋ฒ๋ฆฌ์ง ์๊ณ๊ฐ. ํ์ค์ ์ธ
--min์ ์ค์ ํ๊ณ ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ์ ์ฐจ ๋์ฌ ์์ ์ ์ฐจ๋จํ์ง ๋ง์ธ์; "100% ์๋๋ฉด ์ ๋๋ค"๊ฐ ํ ์คํธ๋ฅผ ์ ํ ๋ฐฐ์กํ์ง ์๋ ํ๊ณ๊ฐ ๋์ด์๋ ์ ๋ฉ๋๋ค. - ์ง์ ํ JS ๊ธฐ๋ฐ ํ๋ฆ( Livewire/Inertia/Vue ์ํธ์์ฉ, ํ์ผ ์ ํ๊ธฐ)์ ๋ํ ๋ธ๋ผ์ฐ์ ํ
์คํธ โ ์ฌ๊ธฐ์๋
visit()์ด ์ฌ๋ฐ๋ฅธ ๋๊ตฌ์ด๋ฉฐ, ๊ณผ๋ํ ์ฌ์ฉ์ด ์๋๋๋ค.
์ฐธ๊ณ ์๋ฃ (References)
- Pest โ Writing Tests, expectations, hooks: https://pestphp.com/docs/writing-tests
- Pest โ Datasets: https://pestphp.com/docs/datasets
- Pest โ Architecture Testing & presets (
security,laravel,toExtend): https://pestphp.com/docs/arch-testing - Pest โ Coverage &
--min: https://pestphp.com/docs/coverage - Pest โ Type Coverage plugin: https://pestphp.com/docs/type-coverage
- Pest v4 โ Browser Testing (
visit(), visual regression): https://pestphp.com/docs/browser-testing - Laravel โ HTTP Tests (status/JSON/redirect assertions): https://laravel.com/docs/testing
- Laravel โ Database Testing (
RefreshDatabase, factories,assertDatabaseHas): https://laravel.com/docs/database-testing - Laravel โ Mocking & facade fakes (
Http,Queue,Bus,Mail,Notification,Event,Storage, time): https://laravel.com/docs/mocking