์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿงช PHP ํ…Œ์ŠคํŒ… ์ž…๋ฌธ๋ถ€ํ„ฐ PHPUnit ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ(TDD) ์‹ค์ „๊นŒ์ง€

๐Ÿงช PHP ํ…Œ์ŠคํŒ… ์ž…๋ฌธ๋ถ€ํ„ฐ PHPUnit ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ(TDD) ์‹ค์ „๊นŒ์ง€

"๋‚ด ์ฝ”๋“œ ์ž˜ ๋Œ์•„๊ฐ€๋Š”๋ฐ์š”?"๋ผ๊ณ  ๋งํ–ˆ๋‹ค๊ฐ€ ์ƒˆ๋ฒฝ 3์‹œ์— ์„œ๋ฒ„ ๋ถ™์žก๊ณ  ์šธ์–ด๋ณธ ๋ชจ๋“  PHP ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์ด์•ผ๊ธฐ

โ˜• ๋“ค์–ด๊ฐ€๋ฉฐ : ํ…Œ์ŠคํŠธ๋Š” ์™œ ํ•ญ์ƒ "๋‚˜์ค‘์—"๊ฐ€ ๋˜๋Š”๊ฐ€

์นœ๊ตฌ์•ผ, ์†”์งํ•˜๊ฒŒ ์–˜๊ธฐํ•ด๋ณด์ž.
์šฐ๋ฆฌ PHP ๊ฐœ๋ฐœ์ž๋“ค์€ ๋ธŒ๋ผ์šฐ์ € ์ƒˆ๋กœ๊ณ ์นจ์ด ๊ณง ํ…Œ์ŠคํŠธ์˜€๋˜ ์‹œ์ ˆ์„ ๋‹ค๋“ค ํ•œ ๋ฒˆ์”ฉ ๊ฒช์—ˆ์ž–์•„.

var_dump() ์ฐ๊ณ , die() ๋ฐ•๊ณ , ํ™”๋ฉด์— ๋ฐฐ์—ด ๋ฟŒ๋ ค๋ณด๊ณ ,
"์˜ค์ผ€์ด ๋‚˜์™”๋‹ค!" ํ•˜๊ณ  ์ปค๋ฐ‹. ๊ทธ๋ฆฌ๊ณ  ๋ฐฐํฌ. ๊ทธ๋ฆฌ๊ณ โ€ฆ ์žฅ์• .

์‚ฌ์‹ค ์ด๊ฑด ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ฒŒ์„๋Ÿฌ์„œ๊ฐ€ ์•„๋‹ˆ์•ผ.
PHP๋Š” ํƒœ์ƒ์ด "์š”์ฒญ ํ•˜๋‚˜ ๋ฐ›์•„์„œ HTML ๋ฑ‰๊ณ  ์ฃฝ๋Š”" ์–ธ์–ด๋ผ์„œ,
ํ•œ ๋ฒˆ ์‹คํ–‰ํ•˜๊ณ  ๋๋‚˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋„ˆ๋ฌด ์ž์—ฐ์Šค๋Ÿฌ์› ๊ฑฐ๋“ .
ํ”„๋กœ์„ธ์Šค๊ฐ€ ์‚ด์•„์žˆ์งˆ ์•Š์œผ๋‹ˆ "ํ…Œ์ŠคํŠธ๋ฅผ ๋”ฐ๋กœ ๋Œ๋ฆฐ๋‹ค"๋Š” ๊ฐœ๋… ์ž์ฒด๊ฐ€ ๋‚ฏ์„ค์—ˆ์ง€.

๊ทธ๋Ÿฐ๋ฐ ์ง€๊ธˆ์€ ์ƒํ™ฉ์ด ์™„์ „ํžˆ ๋‹ฌ๋ผ์กŒ์–ด.
Composer, PSR ํ‘œ์ค€, LaravelยทSymfony ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ,
๊ทธ๋ฆฌ๊ณ  PHPUnit์ด๋ผ๋Š” ์‚ฌ์‹ค์ƒ์˜ ํ‘œ์ค€ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๊นŒ์ง€.
PHP ์ƒํƒœ๊ณ„๋Š” ์ด์ œ ํ…Œ์ŠคํŠธ ์•ˆ ์งœ๋ฉด ์˜คํžˆ๋ ค ์ด์ƒํ•œ ๋™๋„ค๊ฐ€ ๋์–ด.

์‹œ๊ฐ„์ด ๊ฐˆ์ˆ˜๋ก ๋ฒŒ์–ด์ง€๋Š” ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ ํ…Œ์ŠคํŠธ ์—†๋Š” ์ฝ”๋“œ (์ˆ˜๋™ ๊ฒ€์ฆ) ํ…Œ์ŠคํŠธ ์žˆ๋Š” ์ฝ”๋“œ (์ž๋™ ๊ฒ€์ฆ) ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๊ธฐ๊ฐ„ โ†’ ๋น„์šฉ โ†‘

์ด ๊ธ€์—์„œ๋Š” PHPUnit์˜ ๊ธฐ๋ณธ๊ธฐ๋ถ€ํ„ฐ,
ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ(TDD)์˜ Red-Green-Refactor ์‚ฌ์ดํด,
๊ทธ๋ฆฌ๊ณ  ์‹ค๋ฌด์—์„œ ์ง„์งœ ์จ๋จน๋Š” ๋ชจํ‚นยท๋ฐ์ดํ„ฐ ํ”„๋กœ๋ฐ”์ด๋”ยท์ปค๋ฒ„๋ฆฌ์ง€๊นŒ์ง€
์นœ๊ตฌ๋ž‘ ์นดํŽ˜์—์„œ ์ˆ˜๋‹ค ๋–จ๋“ฏ ์ญ‰ ํ›‘์–ด๋ณผ๊ฒŒ.

๐Ÿงญ 1. PHPUnit์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€

xUnit ๊ฐ€๋ฌธ์˜ PHP ์žฅ๋‚จ

PHPUnit์€ Sebastian Bergmann์ด 2004๋…„๊ฒฝ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด์˜จ PHP์šฉ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ.
๋ฟŒ๋ฆฌ๋Š” Kent Beck์˜ SUnit / JUnit ๊ณ„์—ด, ์ฆ‰ xUnit ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋”ฐ๋ฅด๊ณ  ์žˆ์–ด.

๊ทธ๋ž˜์„œ Java์˜ JUnit์ด๋‚˜ Python์˜ unittest๋ฅผ ์จ๋ณธ ์‚ฌ๋žŒ์ด๋ผ๋ฉด
setUp(), tearDown(), assertEquals() ๊ฐ™์€ ์ด๋ฆ„์ด ์ต์ˆ™ํ•  ๊ฑฐ์•ผ.
์šฉ์–ด๊ฐ€ ๊ฑฐ์˜ ๊ณต์šฉ์–ด์ฒ˜๋Ÿผ ํ†ต์ผ๋ผ ์žˆ๊ฑฐ๋“ .

๋ฒ„์ „ ์ด์•ผ๊ธฐ (์ค‘์š”!)

PHPUnit์€ PHP ๋ฒ„์ „์— ๊ฐ•ํ•˜๊ฒŒ ๋ฌถ์—ฌ ์žˆ์–ด.
๋Œ€๋žต์ ์ธ ์ง€์› ๊ด€๊ณ„๋Š” ์ด๋Ÿฐ ์‹์ด์•ผ.

PHPUnit ๋ฉ”์ด์ €์š”๊ตฌ PHPํŠน์ง•
9.xPHP 7.3+๋ ˆ๊ฑฐ์‹œ ํ”„๋กœ์ ํŠธ์—์„œ ์—ฌ์ „ํžˆ ๋งŽ์ด ์‚ฌ์šฉ
10.xPHP 8.1+์ด๋ฒคํŠธ ์‹œ์Šคํ…œ ์žฌ์ž‘์„ฑ, ์„ค์ • ํŒŒ์ผ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ
11.xPHP 8.2+์–ดํŠธ๋ฆฌ๋ทฐํŠธ ์ค‘์‹ฌ ์ „ํ™˜ ๊ฐ€์†
12.xPHP 8.3+์–ด๋…ธํ…Œ์ด์…˜(์ฃผ์„) ๋ฐฉ์‹ ์‚ฌ์‹ค์ƒ ์ •๋ฆฌ, ์ตœ์‹  ๊ธฐ์ค€

โš ๏ธ ๊ฐ€์žฅ ๋งŽ์ด ๊ฒช๋Š” ํ•จ์ •
์˜ˆ์ „ ๋ธ”๋กœ๊ทธ ๊ธ€ ๋ณด๊ณ  /** @test */, @dataProvider ๊ฐ™์€
PHPDoc ์–ด๋…ธํ…Œ์ด์…˜์„ ์ผ๋Š”๋ฐ ์ตœ์‹  PHPUnit์—์„œ deprecated ๊ฒฝ๊ณ ๊ฐ€ ๋œจ๋Š” ๊ฒฝ์šฐ.
PHPUnit 10๋ถ€ํ„ฐ๋Š” PHP 8 ์–ดํŠธ๋ฆฌ๋ทฐํŠธ(#[Test], #[DataProvider])๊ฐ€ ๊ถŒ์žฅ ๋ฐฉ์‹์ด์•ผ.

์„ค์น˜๋Š” Composer ํ•œ ์ค„

composer require --dev phpunit/phpunit

# ์‹คํ–‰
./vendor/bin/phpunit

# ๋ฒ„์ „ ํ™•์ธ
./vendor/bin/phpunit --version

--dev ํ”Œ๋ž˜๊ทธ ๊ผญ ๋ถ™์—ฌ์ค˜.
ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ํ”„๋กœ๋•์…˜ ์˜์กด์„ฑ์— ๋„ฃ์œผ๋ฉด ๋ฐฐํฌ ์šฉ๋Ÿ‰๋„ ๋Š˜๊ณ ,
๋ณด์•ˆ ๊ด€์ ์—์„œ๋„ ์ข‹์„ ๊ฒŒ ํ•˜๋‚˜ ์—†์–ด.

๐Ÿ—๏ธ 2. ํ”„๋กœ์ ํŠธ ๋ผˆ๋Œ€ ์žก๊ธฐ

ํ…Œ์ŠคํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ์˜คํ† ๋กœ๋”ฉ์ด ๋จผ์ €์•ผ.
PSR-4 ๊ทœ์น™์œผ๋กœ composer.json์„ ์ด๋ ‡๊ฒŒ ์žก์•„๋ณด์ž.

{
  "autoload": {
    "psr-4": { "App\\": "src/" }
  },
  "autoload-dev": {
    "psr-4": { "Tests\\": "tests/" }
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  }
}

๊ทธ๋ฆฌ๊ณ  composer dump-autoload ํ•œ ๋ฒˆ ์‹คํ–‰.

phpunit.xml ์„ค์ • ํŒŒ์ผ

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         cacheDirectory=".phpunit.cache"
         failOnWarning="true"
         failOnRisky="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>

๐Ÿ’ก ํŒ
failOnWarning๊ณผ failOnRisky๋ฅผ ์ผœ๋‘๋ฉด
"ํ…Œ์ŠคํŠธ๋Š” ํ†ต๊ณผํ–ˆ์ง€๋งŒ ์‚ฌ์‹ค ์•„๋ฌด๊ฒƒ๋„ ๊ฒ€์ฆ ์•ˆ ํ•œ" ์œ ๋ น ํ…Œ์ŠคํŠธ๋ฅผ ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์–ด.
assertion์ด ํ•˜๋‚˜๋„ ์—†๋Š” ํ…Œ์ŠคํŠธ๋Š” risky๋กœ ๋ถ„๋ฅ˜๋˜๊ฑฐ๋“ .

๐Ÿ”ด๐ŸŸข๐Ÿ”ต 3. TDD์˜ ์‹ฌ์žฅ, Red-Green-Refactor

TDD๋Š” Kent Beck์ด ์ •๋ฆฌํ•œ ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•๋ก ์ธ๋ฐ,
ํ•ต์‹ฌ์€ ๋‹จ ํ•˜๋‚˜์•ผ. "ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์“ด๋‹ค."

๋ง์€ ๊ฐ„๋‹จํ•œ๋ฐ ์ฒ˜์Œ ๋“ค์œผ๋ฉด ์ง„์งœ ์ด์ƒํ•˜๊ฒŒ ๋А๊ปด์ง€์ง€.
"์•„๋‹ˆ, ์—†๋Š” ์ฝ”๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•ด?"
๊ทผ๋ฐ ๊ทธ๊ฒŒ ๋ฐ”๋กœ ํฌ์ธํŠธ์•ผ. ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์“ด๋‹ค๋Š” ๊ฑด ๊ณง "์„ค๊ณ„๋ฅผ ๋จผ์ € ํ•œ๋‹ค"๋Š” ๋œป์ด๊ฑฐ๋“ .

TDD ์‚ฌ์ดํด : Red โ†’ Green โ†’ Refactor RED ์‹คํŒจ ํ…Œ์ŠคํŠธ GREEN ์ตœ์†Œ ๊ตฌํ˜„ REFACTOR ๊ตฌ์กฐ ๊ฐœ์„  ํ•œ ์‚ฌ์ดํด : ๋ณดํ†ต ์ˆ˜ ๋ถ„ ์ด๋‚ด

๋‹จ๊ณ„๋ณ„๋กœ ๋œฏ์–ด๋ณด๊ธฐ

1๋‹จ๊ณ„ ยท RED ๐Ÿ”ด
์•„์ง ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์“ด๋‹ค.
๋‹น์—ฐํžˆ ์‹คํŒจํ•œ๋‹ค. ํด๋ž˜์Šค๊ฐ€ ์—†์œผ๋‹ˆ Fatal Error๊ฐ€ ๋‚  ์ˆ˜๋„ ์žˆ์–ด.
์‹คํŒจ๋ฅผ "ํ™•์ธ"ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ์‹คํŒจ๋ฅผ ์•ˆ ๋ณด๋ฉด ๊ทธ ํ…Œ์ŠคํŠธ๊ฐ€
์ง„์งœ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜๊ฐ€ ์—†๊ฑฐ๋“ .

2๋‹จ๊ณ„ ยท GREEN ๐ŸŸข
ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ์ฝ”๋“œ๋ฅผ ์“ด๋‹ค.
์‹ฌ์ง€์–ด ์ƒ์ˆ˜๋ฅผ ๊ทธ๋ƒฅ return ํ•ด๋ฒ„๋ ค๋„ ๋ผ. ๋ถ€๋„๋Ÿฌ์›Œํ•  ํ•„์š” ์—†์–ด.
์ด๊ฑธ "๊ฐ€์งœ๋กœ ํ†ต๊ณผ์‹œํ‚ค๊ธฐ(Fake It)"๋ผ๊ณ  ๋ถ€๋ฅด๋Š”๋ฐ, ์ •์‹ TDD ๊ธฐ๋ฒ•์ด์•ผ.

3๋‹จ๊ณ„ ยท REFACTOR ๐Ÿ”ต
ํ…Œ์ŠคํŠธ๊ฐ€ ์ดˆ๋ก๋ถˆ์ธ ์ƒํƒœ์—์„œ ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ•œ๋‹ค.
์ค‘๋ณต ์ œ๊ฑฐ, ์ด๋ฆ„ ๊ฐœ์„ , ๋ฉ”์„œ๋“œ ์ถ”์ถœ.
ํ…Œ์ŠคํŠธ๊ฐ€ ์•ˆ์ „๋ง์ด๋‹ˆ๊นŒ ๋งˆ์Œ๊ป ๋œฏ์–ด๊ณ ์น  ์ˆ˜ ์žˆ์–ด.

ํ•ต์‹ฌ ๊ทœ์น™ 3๊ฐ€์ง€ (Uncle Bob ๋ฒ„์ „)

1. ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์—†์œผ๋ฉด ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค.
2. ์‹คํŒจ์‹œํ‚ค๊ธฐ์— ์ถฉ๋ถ„ํ•œ ๋งŒํผ๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
3. ํ˜„์žฌ ์‹คํŒจ ์ค‘์ธ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ฌ ๋งŒํผ๋งŒ ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

๐Ÿ›’ 4. ์‹ค์ „ TDD : ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํ• ์ธ ๊ณ„์‚ฐ๊ธฐ ๋งŒ๋“ค๊ธฐ

๋ง๋กœ๋งŒ ํ•˜๋ฉด ๊ฐ์ด ์•ˆ ์˜ค๋‹ˆ๊นŒ, ์ง„์งœ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด๋ณด์ž.
์š”๊ตฌ์‚ฌํ•ญ์€ ์ด๋ž˜.

์š”๊ตฌ์‚ฌํ•ญ

ยท ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ(์ด๋ฆ„, ๊ฐ€๊ฒฉ, ์ˆ˜๋Ÿ‰)์„ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค.
ยท ์ด์•ก์„ ๊ณ„์‚ฐํ•œ๋‹ค.
ยท ์ด์•ก์ด 50,000์› ์ด์ƒ์ด๋ฉด 10% ํ• ์ธ.
ยท 100,000์› ์ด์ƒ์ด๋ฉด 15% ํ• ์ธ.
ยท ํ• ์ธ ๊ธˆ์•ก์€ ์› ๋‹จ์œ„ ์ ˆ์‚ฌ(๋‚ด๋ฆผ).

๐Ÿ”ด Step 1 : ์ฒซ ๋ฒˆ์งธ ์‹คํŒจ ํ…Œ์ŠคํŠธ

<?php
declare(strict_types=1);

namespace Tests\Unit;

use App\Cart;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;

final class CartTest extends TestCase
{
    #[Test]
    public function ๋นˆ_์žฅ๋ฐ”๊ตฌ๋‹ˆ์˜_์ด์•ก์€_0์›์ด๋‹ค(): void
    {
        $cart = new Cart();

        $this->assertSame(0, $cart->total());
    }
}

์‹คํ–‰ํ•˜๋ฉด?

Error: Class "App\Cart" not found

์™„๋ฒฝํ•ด. ์ด๊ฒŒ ์ •์ƒ์ ์ธ RED ์ƒํƒœ์•ผ.
๊ทธ๋ฆฌ๊ณ  ํ•œ๊ตญ์–ด ๋ฉ”์„œ๋“œ๋ช…, PHP์—์„œ ์•„๋ฌด ๋ฌธ์ œ ์—†์ด ์ž˜ ๋Œ์•„๊ฐ€.
ํŒ€ ์ปจ๋ฒค์…˜๋งŒ ๋งž์œผ๋ฉด ํ…Œ์ŠคํŠธ ๊ฐ€๋…์„ฑ์ด ํ™• ์˜ฌ๋ผ๊ฐ€์„œ ์€๊ทผํžˆ ์ธ๊ธฐ ์žˆ๋Š” ๋ฐฉ์‹์ด์•ผ.

๐ŸŸข Step 2 : ์ตœ์†Œ ๊ตฌํ˜„

<?php
declare(strict_types=1);

namespace App;

final class Cart
{
    public function total(): int
    {
        return 0;
    }
}

"์ด๊ฒŒ ๋ฌด์Šจ ๊ตฌํ˜„์ด์•ผ?" ์‹ถ์ง€? ๊ทผ๋ฐ ์ด๊ฒŒ ๋งž์•„.
ํ˜„์žฌ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ์ตœ์†Œ ์ฝ”๋“œ๋‹ˆ๊นŒ.
๋‹ค์Œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ด ๊ฑฐ์ง“๋ง์„ ๊นจ๋œจ๋ ค ์ค„ ๊ฑฐ์•ผ.

๐Ÿ”ด Step 3 : ๋‹ค์Œ ํ…Œ์ŠคํŠธ๋กœ ์••๋ฐ•ํ•˜๊ธฐ

#[Test]
public function ์ƒํ’ˆ์„_๋‹ด์œผ๋ฉด_๊ฐ€๊ฒฉ์˜_ํ•ฉ์ด_์ด์•ก์ด_๋œ๋‹ค(): void
{
    $cart = new Cart();
    $cart->add('์•„๋ฉ”๋ฆฌ์นด๋…ธ', 4500, 2);
    $cart->add('ํฌ๋กœ์™€์ƒ', 3800, 1);

    $this->assertSame(12800, $cart->total());
}

์ด์ œ return 0;์€ ๋ชป ๋ฒ„ํ…จ. ์ง„์งœ ๊ตฌํ˜„์„ ํ•ด์•ผ ํ•ด.

<?php
declare(strict_types=1);

namespace App;

use InvalidArgumentException;

final class Cart
{
    /** @var array<int, array{name:string, price:int, qty:int}> */
    private array $items = [];

    public function add(string $name, int $price, int $qty = 1): void
    {
        if ($price < 0) {
            throw new InvalidArgumentException('๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
        }
        if ($qty < 1) {
            throw new InvalidArgumentException('์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
        }

        $this->items[] = ['name' => $name, 'price' => $price, 'qty' => $qty];
    }

    public function total(): int
    {
        $sum = 0;
        foreach ($this->items as $item) {
            $sum += $item['price'] * $item['qty'];
        }
        return $sum;
    }
}

๐ŸŸข Step 4 : ํ• ์ธ ๊ทœ์น™ ์ถ”๊ฐ€

์ด์ œ ํ• ์ธ์ด์•ผ. ์—ฌ๋Ÿฌ ์ผ€์ด์Šค๋ฅผ ํ•œ ๋ฒˆ์— ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์œผ๋‹ˆ
Data Provider๋ฅผ ์จ๋ณด์ž. ์ด๊ฒŒ PHPUnit์˜ ์ง„์งœ ๋งค๋ ฅ ํฌ์ธํŠธ ์ค‘ ํ•˜๋‚˜์•ผ.

use PHPUnit\Framework\Attributes\DataProvider;

#[Test]
#[DataProvider('ํ• ์ธ_์‹œ๋‚˜๋ฆฌ์˜ค')]
public function ์ด์•ก์—_๋”ฐ๋ผ_ํ• ์ธ์ด_์ ์šฉ๋œ๋‹ค(
    int $์›๊ธˆ, int $๊ธฐ๋Œ€๊ฐ’
): void {
    $cart = new Cart();
    $cart->add('ํ…Œ์ŠคํŠธ์ƒํ’ˆ', $์›๊ธˆ, 1);

    $this->assertSame($๊ธฐ๋Œ€๊ฐ’, $cart->payable());
}

public static function ํ• ์ธ_์‹œ๋‚˜๋ฆฌ์˜ค(): array
{
    return [
        'ํ• ์ธ ์—†์Œ ๊ฒฝ๊ณ„ ์•„๋ž˜' => [49_999, 49_999],
        '10% ํ• ์ธ ์‹œ์ž‘์ '     => [50_000, 45_000],
        '10% ๊ตฌ๊ฐ„ ์ƒ๋‹จ'       => [99_999, 89_999],  // 89999.1 โ†’ ๋‚ด๋ฆผ
        '15% ํ• ์ธ ์‹œ์ž‘์ '     => [100_000, 85_000],
        '๊ณ ์•ก ๊ฒฐ์ œ'           => [250_000, 212_500],
    ];
}

์ด๋ ‡๊ฒŒ ๋ฐฐ์—ด ํ‚ค์— ์ด๋ฆ„์„ ๋ถ™์—ฌ๋‘๋ฉด
์‹คํŒจํ–ˆ์„ ๋•Œ ์ด์•ก์—_๋”ฐ๋ผ_ํ• ์ธ์ด_์ ์šฉ๋œ๋‹ค with data set "10% ๊ตฌ๊ฐ„ ์ƒ๋‹จ"
์ด๋Ÿฐ ์‹์œผ๋กœ ๋”ฑ ์ง‘์–ด์„œ ์•Œ๋ ค์ค˜. ๋””๋ฒ„๊น… ์‹œ๊ฐ„์ด ํ™• ์ค„์–ด๋“ค์–ด.

public function payable(): int
{
    $total = $this->total();
    $rate = match (true) {
        $total >= 100_000 => 0.15,
        $total >=  50_000 => 0.10,
        default           => 0.0,
    };

    return (int) floor($total * (1 - $rate));
}

๐Ÿ’ธ ๋ถ€๋™์†Œ์ˆ˜์  ์ฃผ์˜๋ณด
๋ˆ ๊ณ„์‚ฐ์— float๋ฅผ ์“ฐ๋ฉด 0.1 + 0.2 !== 0.3 ๊ฐ™์€ ๊ณ ์ „์ ์ธ ์ง€์˜ฅ์ด ์—ด๋ ค.
์‹ค๋ฌด์—์„œ๋Š” ๊ธˆ์•ก์„ ์ •์ˆ˜(์› ๋‹จ์œ„)๋กœ ๋‹ค๋ฃจ๊ฑฐ๋‚˜
BCMath / brick/money ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด.
ํ…Œ์ŠคํŠธ์—์„œ float๋ฅผ ๋น„๊ตํ•ด์•ผ ํ•œ๋‹ค๋ฉด assertEqualsWithDelta()๋ฅผ ์“ฐ์ž.

๐Ÿ”ต Step 5 : ๋ฆฌํŒฉํ„ฐ๋ง

์ง€๊ธˆ Cart๊ฐ€ "๋‹ด๊ธฐ + ๊ณ„์‚ฐ + ํ• ์ธ์ •์ฑ…"๊นŒ์ง€ ๋‹ค ํ•˜๊ณ  ์žˆ์–ด.
๋‹จ์ผ ์ฑ…์ž„ ์›์น™(SRP) ๊ด€์ ์—์„œ ํ• ์ธ ์ •์ฑ…์„ ๋ถ„๋ฆฌํ•ด๋ณด์ž.

interface DiscountPolicy
{
    public function rateFor(int $total): float;
}

final class TieredDiscountPolicy implements DiscountPolicy
{
    public function rateFor(int $total): float
    {
        return match (true) {
            $total >= 100_000 => 0.15,
            $total >=  50_000 => 0.10,
            default           => 0.0,
        };
    }
}

final class Cart
{
    public function __construct(
        private readonly DiscountPolicy $policy = new TieredDiscountPolicy()
    ) {}
    // ...
}

์—ฌ๊ธฐ์„œ ๋งˆ๋ฒ•์ด ์ผ์–ด๋‚˜.
๋ฆฌํŒฉํ„ฐ๋ง ํ›„์—๋„ ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ๋Œ๋ฆฌ๋ฉด ์ „๋ถ€ ์ดˆ๋ก๋ถˆ์ด์•ผ.
๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ํ†ต์งธ๋กœ ๋ฐ”๊ฟจ๋Š”๋ฐ ๋™์ž‘์€ ๊ทธ๋Œ€๋กœ๋ผ๋Š” ๊ฑธ 1์ดˆ ๋งŒ์— ์ฆ๋ช…ํ•œ ๊ฑฐ์ง€.

์ด๊ฒŒ TDD๋ฅผ ํ•˜๋Š” ์ง„์งœ ์ด์œ ์•ผ.
ํ…Œ์ŠคํŠธ๋Š” ๋ฒ„๊ทธ ์žก๋Š” ๋„๊ตฌ์ด๊ธฐ ์ด์ „์— ๋ณ€๊ฒฝ์„ ๋‘๋ ค์›Œํ•˜์ง€ ์•Š๊ฒŒ ํ•ด์ฃผ๋Š” ์•ˆ์ „๋ฒจํŠธ๊ฑฐ๋“ .

๐ŸŽญ 5. ํ…Œ์ŠคํŠธ ๋”๋ธ” : ๋ชจํ‚น์˜ ์„ธ๊ณ„

์‹ค๋ฌด ์ฝ”๋“œ๋Š” ์ˆœ์ˆ˜ ๊ณ„์‚ฐ๋งŒ ํ•˜์ง€ ์•Š์ง€.
DB์— ๋ถ™๊ณ , API ํ˜ธ์ถœํ•˜๊ณ , ๋ฉ”์ผ ๋ณด๋‚ด๊ณ , ํŒŒ์ผ ์“ฐ๊ณ โ€ฆ.

์ด๋Ÿฐ ๊ฑธ ํ…Œ์ŠคํŠธ์—์„œ ์ง„์งœ๋กœ ์‹คํ–‰ํ•˜๋ฉด?
๋А๋ฆฌ๊ณ , ๋ถˆ์•ˆ์ •ํ•˜๊ณ , ์ง„์งœ ๊ณ ๊ฐํ•œํ…Œ ๋ฉ”์ผ ๋‚ ์•„๊ฐ€๋Š” ์ฐธ์‚ฌ๊ฐ€ ์ƒ๊ฒจ.
๊ทธ๋ž˜์„œ ํ…Œ์ŠคํŠธ ๋”๋ธ”(Test Double), ํ”ํžˆ ๋งํ•˜๋Š” "๊ฐ€์งœ ๊ฐ์ฒด"๋ฅผ ์“ฐ๋Š” ๊ฑฐ์•ผ.

์ข…๋ฅ˜์—ญํ• ๋น„์œ 
Dummy์ž๋ฆฌ๋งŒ ์ฑ„์›€, ์‹ค์ œ๋กœ ์•ˆ ์“ฐ์ž„๋งˆ๋„คํ‚น
Stub๋ฏธ๋ฆฌ ์ •ํ•ด์ง„ ๊ฐ’์„ ๋ฐ˜ํ™˜๋Œ€๋ณธ ์ฝ๋Š” ๋ฐฐ์šฐ
Spyํ˜ธ์ถœ ๊ธฐ๋ก์„ ๋‚จ๊น€CCTV
Mockํ˜ธ์ถœ ๊ธฐ๋Œ€์น˜๋ฅผ ๊ฒ€์ฆ๊น๊นํ•œ ๊ฐ๋…๊ด€
Fake๋‹จ์ˆœํ•˜์ง€๋งŒ ๋™์ž‘ํ•˜๋Š” ๊ตฌํ˜„์ธ๋ฉ”๋ชจ๋ฆฌ DB

PHPUnit์—์„œ Stub ๋งŒ๋“ค๊ธฐ

#[Test]
public function ํ™˜์œจ_API๊ฐ€_์‹คํŒจํ•˜๋ฉด_๊ธฐ๋ณธํ™˜์œจ์„_์‚ฌ์šฉํ•œ๋‹ค(): void
{
    $api = $this->createStub(ExchangeRateApi::class);
    $api->method('fetch')
        ->willThrowException(new \RuntimeException('timeout'));

    $converter = new CurrencyConverter($api, fallbackRate: 1300);

    $this->assertSame(130_000, $converter->toKrw(100));
}

Mock์œผ๋กœ "ํ˜ธ์ถœ๋๋Š”์ง€" ๊ฒ€์ฆํ•˜๊ธฐ

#[Test]
public function ์ฃผ๋ฌธ_์™„๋ฃŒ์‹œ_์•Œ๋ฆผ์ด_์ •ํ™•ํžˆ_ํ•œ๋ฒˆ_๋ฐœ์†ก๋œ๋‹ค(): void
{
    $notifier = $this->createMock(Notifier::class);

    $notifier->expects($this->once())
             ->method('send')
             ->with(
                 $this->equalTo('user@example.com'),
                 $this->stringContains('์ฃผ๋ฌธ์ด ์™„๋ฃŒ')
             );

    $service = new OrderService($notifier);
    $service->complete(new Order('user@example.com'));
}

๐Ÿง  ๋ชจํ‚น ๋‚จ์šฉ ๊ฒฝ๊ณ 
๋ชจ๋“  ๊ฑธ ๋‹ค ๋ชจํ‚นํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ์— ๋ชป ๋ฐ•ํ˜€๋ฒ„๋ ค.
๋ฆฌํŒฉํ„ฐ๋ง๋งŒ ํ•ด๋„ ํ…Œ์ŠคํŠธ๊ฐ€ ์šฐ์ˆ˜์ˆ˜ ๊นจ์ง€๋Š” "๊นจ์ง€๊ธฐ ์‰ฌ์šด ํ…Œ์ŠคํŠธ(Fragile Test)"๊ฐ€ ๋˜์ง€.

์›์น™์€ ์ด๊ฑฐ์•ผ โ†’ ๋‚ด๊ฐ€ ์†Œ์œ ํ•˜์ง€ ์•Š์€ ๊ฒƒ(์™ธ๋ถ€ API, ์‹œ๊ฐ„, ๋žœ๋ค, ํŒŒ์ผ์‹œ์Šคํ…œ)๋งŒ ๋ชจํ‚นํ•œ๋‹ค.
๋‚ด ๋„๋ฉ”์ธ ๋กœ์ง๋ผ๋ฆฌ๋Š” ์›ฌ๋งŒํ•˜๋ฉด ์ง„์งœ ๊ฐ์ฒด๋กœ ๋ถ™์—ฌ์„œ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒŒ ๋” ๊ฒฌ๊ณ ํ•ด.

์‹œ๊ฐ„ยท๋žœ๋ค ๋‹ค๋ฃจ๊ธฐ

time()์ด๋‚˜ rand()๋ฅผ ์ฝ”๋“œ์— ์ง์ ‘ ๋ฐ•์œผ๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ด์ ธ.
"์ž์ •์—๋งŒ ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ" ๊ฐ™์€ ๊ดด๋‹ด์ด ์—ฌ๊ธฐ์„œ ๋‚˜์™€.

interface Clock { public function now(): \DateTimeImmutable; }

final class FrozenClock implements Clock
{
    public function __construct(private \DateTimeImmutable $t) {}
    public function now(): \DateTimeImmutable { return $this->t; }
}

// ํ…Œ์ŠคํŠธ์—์„œ
$clock = new FrozenClock(new \DateTimeImmutable('2025-01-01 00:00:00'));
$coupon = new Coupon(expiresAt: '2024-12-31', clock: $clock);
$this->assertTrue($coupon->isExpired());

์ฐธ๊ณ ๋กœ PSR-20์— ClockInterface๊ฐ€ ํ‘œ์ค€์œผ๋กœ ์ •์˜๋ผ ์žˆ์–ด์„œ,
์ƒˆ ํ”„๋กœ์ ํŠธ๋ผ๋ฉด ๊ทธ๊ฑธ ๊ทธ๋Œ€๋กœ ์“ฐ๋Š” ๊ฒŒ ์ข‹์•„.

๐Ÿ” 6. Assertion, ์ œ๋Œ€๋กœ ๊ณจ๋ผ ์“ฐ๊ธฐ

PHPUnit์—” assertion์ด ์ˆ˜์‹ญ ๊ฐœ ์žˆ์–ด. ๊ทผ๋ฐ ์‹ค๋ฌด์—์„œ 90%๋Š” ์ด ์ •๋„๋กœ ๋๋‚˜.

๋ฉ”์„œ๋“œ์šฉ๋„
assertSame()๊ฐ’ + ํƒ€์ž…๊นŒ์ง€ ์—„๊ฒฉ ๋น„๊ต (===). ๊ธฐ๋ณธ์œผ๋กœ ์ด๊ฑฐ ์“ฐ์ž
assertEquals()๋А์Šจํ•œ ๋น„๊ต. ๊ฐ์ฒด๋Š” ์†์„ฑ ๋‹จ์œ„ ๋น„๊ต
assertTrue/False()๋ถˆ๋ฆฌ์–ธ ๊ฒ€์ฆ
assertNull()null ์—ฌ๋ถ€
assertCount()๋ฐฐ์—ด/Countable ๊ฐœ์ˆ˜
assertInstanceOf()ํƒ€์ž… ๊ฒ€์ฆ
assertArrayHasKey()๋ฐฐ์—ด ํ‚ค ์กด์žฌ
assertStringContainsString()๋ฌธ์ž์—ด ํฌํ•จ
expectException()์˜ˆ์™ธ ๋ฐœ์ƒ ๊ฒ€์ฆ

โš ๏ธ assertEquals์˜ ํ•จ์ •
assertEquals(0, 'hello')๋Š” PHP 7๊นŒ์ง€๋Š” ํ†ต๊ณผํ–ˆ์–ด(๋А์Šจํ•œ ๋น„๊ต ๋•Œ๋ฌธ์—).
PHP 8์—์„œ ๋น„๊ต ๊ทœ์น™์ด ๋ฐ”๋€Œ์–ด ์ง€๊ธˆ์€ ์•„๋‹ˆ์ง€๋งŒ,
assertEquals('1', 1)์€ ์—ฌ์ „ํžˆ ํ†ต๊ณผํ•ด.
์˜๋„ํ•œ ๊ฒŒ ์•„๋‹ˆ๋ผ๋ฉด ๋ฌด์กฐ๊ฑด assertSame์„ ์“ฐ๋Š” ์Šต๊ด€์„ ๋“ค์ด์ž.

์˜ˆ์™ธ ํ…Œ์ŠคํŠธ

#[Test]
public function ์Œ์ˆ˜_๊ฐ€๊ฒฉ์€_๊ฑฐ๋ถ€๋œ๋‹ค(): void
{
    $this->expectException(InvalidArgumentException::class);
    $this->expectExceptionMessage('๊ฐ€๊ฒฉ์€ ์Œ์ˆ˜์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');

    (new Cart())->add('์ด์ƒํ•œ์ƒํ’ˆ', -1000);
}

expectException()์€ ๋ฐ˜๋“œ์‹œ ์˜ˆ์™ธ๊ฐ€ ํ„ฐ์ง€๋Š” ์ฝ”๋“œ ๋ฐ”๋กœ ์•ž์— ์จ์•ผ ํ•ด.
๊ทธ๋ฆฌ๊ณ  ๊ทธ ๋’ค์— ์ฝ”๋“œ๋ฅผ ๋” ์“ฐ๋ฉด ์‹คํ–‰๋˜์ง€ ์•Š์œผ๋‹ˆ,
ํ•œ ํ…Œ์ŠคํŠธ์—๋Š” ์˜ˆ์™ธ ํ•˜๋‚˜๋งŒ ๊ฒ€์ฆํ•˜๋Š” ๊ฒŒ ์›์น™์ด์•ผ.

๐Ÿ“Š 7. ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€, ์ˆซ์ž์— ์†์ง€ ๋ง์ž

์ปค๋ฒ„๋ฆฌ์ง€๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰ํ•œ ์ฝ”๋“œ์˜ ๋น„์œจ์ด์•ผ.
์ธก์ •ํ•˜๋ ค๋ฉด Xdebug(coverage ๋ชจ๋“œ) ๋˜๋Š” PCOV ํ™•์žฅ์ด ํ•„์š”ํ•ด.
์†๋„๋งŒ ๋†“๊ณ  ๋ณด๋ฉด PCOV๊ฐ€ ์••๋„์ ์œผ๋กœ ๋น ๋ฅด๋‹ˆ CI์—์„œ๋Š” PCOV๋ฅผ ์ถ”์ฒœํ• ๊ฒŒ.

# HTML ๋ฆฌํฌํŠธ ์ƒ์„ฑ
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html build/coverage

# ํ„ฐ๋ฏธ๋„์—์„œ ์š”์•ฝ๋งŒ
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text
์ปค๋ฒ„๋ฆฌ์ง€์˜ ์„ธ ๊ฐ€์ง€ ์ธต์œ„ Line Coverage ์ด ์ค„์ด ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋๋Š”๊ฐ€ โ€” ๊ฐ€์žฅ ๋А์Šจํ•จ Branch Coverage if์˜ true/false ์–‘์ชฝ์„ ๋ชจ๋‘ ์ง€๋‚˜๊ฐ”๋Š”๊ฐ€ Path Coverage ๊ฐ€๋Šฅํ•œ ์‹คํ–‰ ๊ฒฝ๋กœ ์กฐํ•ฉ์„ ๋ชจ๋‘ ๋ฐŸ์•˜๋Š”๊ฐ€ โ€” ๊ฐ€์žฅ ์—„๊ฒฉ ์•„๋ž˜๋กœ ๊ฐˆ์ˆ˜๋ก ์‹ ๋ขฐ๋„ โ†‘ ยท ๋‹ฌ์„ฑ ๋‚œ์ด๋„ โ†‘

๐Ÿšจ ์ปค๋ฒ„๋ฆฌ์ง€ 100%์˜ ๊ฑฐ์ง“๋ง

public function divide(int $a, int $b): float
{
    return $a / $b;   // ์ด ์ค„์€ 100% ์ปค๋ฒ„๋จ
}

// ํ…Œ์ŠคํŠธ
$this->assertSame(2.0, $calc->divide(4, 2));  // ํ†ต๊ณผ!

๋ผ์ธ ์ปค๋ฒ„๋ฆฌ์ง€ 100%. ๊ทผ๋ฐ divide(4, 0)์€? DivisionByZeroError ๐Ÿ’ฅ
์ปค๋ฒ„๋ฆฌ์ง€๋Š” "ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์€ ๊ณณ"์„ ์ฐพ๋Š” ์ง€๋„์ผ ๋ฟ,
"ํ…Œ์ŠคํŠธ๊ฐ€ ์ถฉ๋ถ„ํ•˜๋‹ค"๋Š” ์ฆ๋ช…์„œ๊ฐ€ ์•„๋‹ˆ์•ผ.

ํ˜„์‹ค์ ์ธ ๋ชฉํ‘œ์น˜๋Š” ๋ณดํ†ต ์ด๋ž˜.

ยท ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๋กœ์ง : 85~95% ์ง€ํ–ฅ
ยท ์ „์ฒด ํ”„๋กœ์ ํŠธ : 70~80%๋ฉด ๊ฑด๊ฐ•ํ•œ ํŽธ
ยท ๋ทฐ/ํ…œํ”Œ๋ฆฟ/์„ค์ • ํŒŒ์ผ : ์ปค๋ฒ„๋ฆฌ์ง€ ๋Œ€์ƒ์—์„œ ์ œ์™ธ

๐Ÿงฑ 8. ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ์™€ ํ˜„์‹ค

Mike Cohn์ด ์ œ์•ˆํ•œ ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ๋Š” ์ด๋Ÿฐ ๊ตฌ์กฐ์•ผ.

ํ…Œ์ŠคํŠธ ํ”ผ๋ผ๋ฏธ๋“œ E2E Integration Unit ๋А๋ฆผ ยท ๋น„์Œˆ ๋ถˆ์•ˆ์ • ๋น ๋ฆ„ ยท ์ €๋ ด ์•ˆ์ •์ 

PHP ํ”„๋กœ์ ํŠธ์—์„œ์˜ ์‹ค์ œ ๊ตฌ๋ถ„

Unit ํ…Œ์ŠคํŠธ โ€” DB๋„, ํŒŒ์ผ๋„, ๋„คํŠธ์›Œํฌ๋„ ์•ˆ ๊ฑด๋“œ๋ฆผ.
์ˆœ์ˆ˜ PHP ๊ฐ์ฒด๋งŒ. ์ˆ˜์ฒœ ๊ฐœ๋ฅผ ๋ช‡ ์ดˆ ๋งŒ์— ๋Œ๋ฆด ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด.

Integration ํ…Œ์ŠคํŠธ โ€” ์‹ค์ œ DB(๊ฐ€๋Šฅํ•˜๋ฉด SQLite in-memory๋‚˜
๋„์ปค๋กœ ๋„์šด MySQL)์— ๋ถ™์–ด์„œ Repository๋‚˜ Query๊ฐ€ ์ž˜ ๋„๋Š”์ง€ ํ™•์ธ.
ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๊ฐ์‹ธ๊ณ  ํ…Œ์ŠคํŠธ ๋๋‚˜๋ฉด ๋กค๋ฐฑํ•˜๋Š” ํŒจํ„ด์ด ๊ตญ๋ฃฐ์ด์•ผ.

protected function setUp(): void
{
    $this->pdo = new PDO('sqlite::memory:');
    $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
}

protected function tearDown(): void
{
    $this->pdo = null;   // ์ธ๋ฉ”๋ชจ๋ฆฌ DB๋Š” ์—ฐ๊ฒฐ ๋Š๊ธฐ๋ฉด ์ž๋™ ์†Œ๋ฉธ
}

E2E / Feature ํ…Œ์ŠคํŠธ โ€” HTTP ์š”์ฒญ์„ ์‹ค์ œ๋กœ ์ด์„œ
์ƒํƒœ ์ฝ”๋“œ์™€ ์‘๋‹ต ๋ณธ๋ฌธ์„ ํ™•์ธ. Laravel์˜ $this->get('/api/users')๋‚˜
Symfony์˜ WebTestCase๊ฐ€ ์ด ์˜์—ญ์ด์•ผ.

โš™๏ธ 9. CI์— ๋ฌผ๋ฆฌ๊ณ , ์Šต๊ด€์œผ๋กœ ๋งŒ๋“ค๊ธฐ

ํ…Œ์ŠคํŠธ๋Š” ์ž๋™์œผ๋กœ ๋Œ์•„์•ผ ์‚ด์•„๋‚จ์•„.
์‚ฌ๋žŒ์ด ๊ธฐ์–ตํ•ด์„œ ๋Œ๋ฆฌ๋Š” ํ…Œ์ŠคํŠธ๋Š” ๊ฒฐ๊ตญ ์•ˆ ๋Œ๊ฒŒ ๋˜๊ฑฐ๋“ .

# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]

jobs:
  phpunit:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.2', '8.3', '8.4']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: pcov
      - run: composer install --prefer-dist --no-progress
      - run: vendor/bin/phpunit --coverage-text

๊ฐ™์ด ์“ฐ๋ฉด ์ข‹์€ ๋„๊ตฌ๋“ค

๋„๊ตฌ์—ญํ• 
PHPStan / Psalm์ •์  ๋ถ„์„. ์‹คํ–‰ ์—†์ด ํƒ€์ž… ์˜ค๋ฅ˜ ํƒ์ง€
PestPHPUnit ์œ„์— ์–นํžŒ ํ‘œํ˜„๋ ฅ ์ข‹์€ ๋ฌธ๋ฒ• ๋ ˆ์ด์–ด
Infection๋ฎคํ…Œ์ด์…˜ ํ…Œ์ŠคํŒ…. ํ…Œ์ŠคํŠธ์˜ "ํ’ˆ์งˆ"์„ ์ธก์ •
PHP-CS-Fixer์ฝ”๋“œ ์Šคํƒ€์ผ ์ž๋™ ์ •๋ฆฌ

ํŠนํžˆ Infection์€ ์ง„์งœ ํฅ๋ฏธ๋กœ์›Œ.
์ฝ”๋“œ๋ฅผ ์ผ๋ถ€๋Ÿฌ ๋ณ€ํ˜•(>๋ฅผ >=๋กœ ๋ฐ”๊พธ๋Š” ์‹)์‹œํ‚จ ๋’ค
ํ…Œ์ŠคํŠธ๊ฐ€ ๊ทธ๊ฑธ ์žก์•„๋‚ด๋Š”์ง€ ํ™•์ธํ•ด.
์žก์•„๋‚ด์ง€ ๋ชปํ•˜๋ฉด "๊ทธ ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ์‹ค ์•„๋ฌด๊ฒƒ๋„ ์ง€ํ‚ค์ง€ ๋ชปํ•˜๊ณ  ์žˆ๋‹ค"๋Š” ๋œป์ด์ง€.
์ปค๋ฒ„๋ฆฌ์ง€ ์ˆซ์ž๋ณด๋‹ค ํ›จ์”ฌ ์ •์งํ•œ ์ง€ํ‘œ์•ผ.

๐Ÿงฏ 10. ํ”ํ•œ ์‹ค์ˆ˜์™€ ํ•ด๊ฒฐ์ฑ…

โ‘  ํ…Œ์ŠคํŠธ๋ผ๋ฆฌ ์„œ๋กœ ์˜์กดํ•œ๋‹ค
A ํ…Œ์ŠคํŠธ๊ฐ€ ๋งŒ๋“  ๋ฐ์ดํ„ฐ๋ฅผ B๊ฐ€ ์“ฐ๋Š” ๊ตฌ์กฐ. ์ˆœ์„œ ๋ฐ”๋€Œ๋ฉด ์™€์žฅ์ฐฝ.
โ†’ ๊ฐ ํ…Œ์ŠคํŠธ๋Š” setUp()์—์„œ ์ž๊ธฐ ์ƒํƒœ๋ฅผ ์Šค์Šค๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•ด.
--order-by=random ์˜ต์…˜์œผ๋กœ ์ˆœ์„œ๋ฅผ ์„ž์–ด ๋Œ๋ ค๋ณด๋ฉด ๋ฐ”๋กœ ๋“คํ†ต๋‚˜.

โ‘ก ํ•œ ํ…Œ์ŠคํŠธ์—์„œ 20๊ฐ€์ง€๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค
์ฒซ ๋ฒˆ์งธ assertion์ด ๊นจ์ง€๋ฉด ๋‚˜๋จธ์ง€๋Š” ์‹คํ–‰๋„ ์•ˆ ๋ผ.
โ†’ ํ…Œ์ŠคํŠธ ํ•˜๋‚˜๋‹น ๊ฐœ๋… ํ•˜๋‚˜. ์ด๋ฆ„๋งŒ ๋ด๋„ ๋ญ˜ ๊ฒ€์ฆํ•˜๋Š”์ง€ ์•Œ๊ฒŒ.

โ‘ข ํ…Œ์ŠคํŠธ๊ฐ€ 5๋ถ„์”ฉ ๊ฑธ๋ฆฐ๋‹ค
โ†’ ์‹ญ์ค‘ํŒ”๊ตฌ Unit ํ…Œ์ŠคํŠธ์ธ ์ฒ™ํ•˜๋Š” Integration ํ…Œ์ŠคํŠธ์•ผ.
DB ๋ถ™๋Š” ๋†ˆ๋“ค์€ testsuite๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ , ํ‰์†Œ์—” Unit๋งŒ ๋Œ๋ฆฌ์ž.

โ‘ฃ private ๋ฉ”์„œ๋“œ๋ฅผ Reflection์œผ๋กœ ์–ต์ง€๋กœ ํ…Œ์ŠคํŠธํ•œ๋‹ค
โ†’ ๊ทธ๊ฑด ์‹ ํ˜ธ์•ผ. "์ด private ๋กœ์ง์ด ์‚ฌ์‹ค์€ ๋ณ„๋„ ํด๋ž˜์Šค์—ฌ์•ผ ํ•œ๋‹ค"๋Š”.
public ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ๊ฐ„์ ‘์ ์œผ๋กœ ๊ฒ€์ฆ๋˜๋Š” ๊ฒŒ ์ •์ƒ์ด์•ผ.

โ‘ค ๋ฒ„๊ทธ ๊ณ ์น  ๋•Œ ํ…Œ์ŠคํŠธ๋ฅผ ์•ˆ ์“ด๋‹ค
โ†’ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ๋ฅผ ๋ฐ›์œผ๋ฉด ๋จผ์ € ๊ทธ ๋ฒ„๊ทธ๋ฅผ ์žฌํ˜„ํ•˜๋Š” ์‹คํŒจ ํ…Œ์ŠคํŠธ๋ฅผ ์“ฐ์ž.
๊ทธ๊ฒŒ ํšŒ๊ท€ ๋ฐฉ์ง€(Regression) ํ…Œ์ŠคํŠธ๊ฐ€ ๋˜๊ณ ,
๊ฐ™์€ ๋ฒ„๊ทธ๊ฐ€ ๋‘ ๋ฒˆ ๋‚˜์˜ค๋Š” ๊ฑธ ์˜๊ตฌํžˆ ๋ง‰์•„์ค˜.

๐ŸŒฑ 11. ๊ทธ๋ž˜์„œ, ์–ด๋–ป๊ฒŒ ์‹œ์ž‘ํ• ๊นŒ

"๊ธฐ์กด ๋ ˆ๊ฑฐ์‹œ ํ”„๋กœ์ ํŠธ์ธ๋ฐ ํ…Œ์ŠคํŠธ๊ฐ€ 0๊ฐœ์˜ˆ์š”. ์–ด๋””๋ถ€ํ„ฐ ์†๋Œ€์ฃ ?"
๊ฐ€์žฅ ๋งŽ์ด ๋“ฃ๋Š” ์งˆ๋ฌธ์ด์•ผ. ๋‹ต์€ ๋ช…ํ™•ํ•ด.

์ „๋ถ€ ๋‹ค ์งœ๋ ค๊ณ  ํ•˜์ง€ ๋งˆ.

1๏ธโƒฃ ์ƒˆ๋กœ ์“ฐ๋Š” ์ฝ”๋“œ๋ถ€ํ„ฐ ํ…Œ์ŠคํŠธ๋ฅผ ๋ถ™์—ฌ. ๊ณผ๊ฑฐ๋Š” ์ผ๋‹จ ๋†”๋‘ฌ.
2๏ธโƒฃ ๋ฒ„๊ทธ๊ฐ€ ๋‚œ ์ง€์ ์— ํ…Œ์ŠคํŠธ๋ฅผ ์‹ฌ์–ด. ์•„ํ”ˆ ๊ณณ์ด ๊ณง ์ค‘์š”ํ•œ ๊ณณ์ด์•ผ.
3๏ธโƒฃ ๊ณ„์‚ฐ ๋กœ์งยท๊ฒ€์ฆ ๋กœ์ง์ฒ˜๋Ÿผ ์ž…์ถœ๋ ฅ์ด ๋ช…ํ™•ํ•œ ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋ถ€ํ„ฐ. ๊ฐ€์„ฑ๋น„ ์ตœ๊ณ .
4๏ธโƒฃ ๋ฆฌํŒฉํ„ฐ๋งํ•  ์ฝ”๋“œ ์ฃผ๋ณ€์— ํŠน์„ฑํ™” ํ…Œ์ŠคํŠธ(Characterization Test)๋ฅผ ๋จผ์ € ๊น”์•„.
ย ย ย ํ˜„์žฌ ๋™์ž‘์ด ์˜ณ๋“  ๊ทธ๋ฅด๋“  ์ผ๋‹จ "์ง€๊ธˆ ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ•œ๋‹ค"๋ฅผ ๊ณ ์ •์‹œํ‚ค๋Š” ๊ฑฐ์ง€.

๊ทธ๋ฆฌ๊ณ  ํ•˜๋‚˜ ๋”.
TDD๋Š” ์ข…๊ต๊ฐ€ ์•„๋‹ˆ๋ผ ๋„๊ตฌ์•ผ.
ํ”„๋กœํ† ํƒ€์ž… ๋งŒ๋“ค ๋•Œ, UI ๋งŒ๋“ค ๋•Œ, ํ•œ ๋ฒˆ ์“ฐ๊ณ  ๋ฒ„๋ฆด ์Šคํฌ๋ฆฝํŠธ์—๋Š”
๊ตณ์ด TDD๋ฅผ ๊ฐ•์š”ํ•  ํ•„์š” ์—†์–ด.

๋Œ€์‹  ๋ˆ์ด ์˜ค๊ฐ€๋Š” ๋กœ์ง, ๊ถŒํ•œ ๊ฒ€์‚ฌ, ๋ณต์žกํ•œ ์ƒํƒœ ์ „์ด,
์—ฌ๋Ÿฌ ์‚ฌ๋žŒ์ด ๊ณ„์† ๊ฑด๋“œ๋ฆด ์ฝ”์–ด ๋ชจ๋“ˆโ€”
์—ฌ๊ธฐ์—” ๋ฐ˜๋“œ์‹œ ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์–ด์•ผ ํ•ด. ๊ทธ๊ฒŒ ํ”„๋กœ๋‹ค์›€์ด์•ผ.

์š”์ฆ˜์€ PHP ํ…Œ์ŠคํŒ…์ด๋‚˜ TDD ๊ฐ™์€ ์ฃผ์ œ๋„ ํ˜ผ์ž ํŒŒ๊ธฐ๋ณด๋‹ค๋Š”
์‹ค๋ฌด ๊ฒฝํ—˜์ž์—๊ฒŒ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›๊ฑฐ๋‚˜ ์งง์€ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›๋Š” ๊ฒŒ ํ›จ์”ฌ ๋นจ๋ผ.
์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ๊ฐœ๋ฐœ ๋ถ„์•ผ ์žฌ๋Šฅ์„ ์ฃผ๊ณ ๋ฐ›๋Š” ํ”Œ๋žซํผ์—์„œ๋Š”
PHPUnit ์„ค์ •๋ถ€ํ„ฐ ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ ํ…Œ์ŠคํŠธ ์ „๋žต๊นŒ์ง€ ์ง์ ‘ ๋ฌผ์–ด๋ณผ ์ˆ˜ ์žˆ์–ด์„œ,
๋ฌธ์„œ๋งŒ ์ฝ์„ ๋•Œ ์ƒ๊ธฐ๋Š” "๊ทธ๋ž˜์„œ ์‹ค๋ฌด์—์„ ์š”?" ๊ฐˆ์ฆ์„ ๊ฝค ์ž˜ ์ฑ„์›Œ์ฃผ๋”๋ผ.

๐Ÿ“Œ ๋งˆ๋ฌด๋ฆฌ : ์ดˆ๋ก๋ถˆ์˜ ์•ˆ๋„๊ฐ

์ฒ˜์Œ TDD๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด ๋ฌด์กฐ๊ฑด ๋А๋ ค.
"๊ธฐ๋Šฅ ํ•˜๋‚˜ ๋งŒ๋“œ๋Š” ๋ฐ ์ฝ”๋“œ๋ฅผ ์™œ ๋‘ ๋ฐฐ๋กœ ์“ฐ์ง€?" ์‹ถ๊ณ , ๋‹ต๋‹ตํ•ด.

๊ทผ๋ฐ 3๊ฐœ์›”์ฏค ์ง€๋‚˜์„œ
์•„๋ฌด๋„ ๊ฑด๋“œ๋ฆฌ๊ธฐ ๋ฌด์„œ์›Œํ•˜๋˜ ๊ฒฐ์ œ ๋ชจ๋“ˆ์„ ๋ฆฌํŒฉํ„ฐ๋งํ•˜๊ณ ,
ํ„ฐ๋ฏธ๋„์— OK (247 tests, 891 assertions)๊ฐ€ ์ดˆ๋ก์ƒ‰์œผ๋กœ ๋œจ๋Š” ์ˆœ๊ฐ„โ€”

๊ทธ ์•ˆ๋„๊ฐ์€ ์ง„์งœ ๊ฒช์–ด๋ด์•ผ ์•Œ์•„.
๋ฐฐํฌ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋Š” ์†์ด ์•ˆ ๋–จ๋ฆฌ๋Š” ๊ฒฝํ—˜, ๊ทธ๊ฑฐ ๊ฝค ์ค‘๋…์„ฑ ์žˆ๊ฑฐ๋“ . ๐Ÿ˜Œ

์˜ค๋Š˜์˜ ์š”์•ฝ ๐ŸŽ’

ยท PHPUnit์€ PHP์˜ ์‚ฌ์‹ค์ƒ ํ‘œ์ค€ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ, Composer๋กœ --dev ์„ค์น˜
ยท ์ตœ์‹  ๋ฒ„์ „์—์„  ์–ด๋…ธํ…Œ์ด์…˜ ๋Œ€์‹  PHP 8 ์–ดํŠธ๋ฆฌ๋ทฐํŠธ(#[Test]) ์‚ฌ์šฉ
ยท TDD์˜ ๋ฆฌ๋“ฌ์€ Red โ†’ Green โ†’ Refactor, ์‚ฌ์ดํด์€ ์งง๊ฒŒ
ยท assertEquals๋ณด๋‹ค assertSame์„ ๊ธฐ๋ณธ์œผ๋กœ
ยท Data Provider๋กœ ๊ฒฝ๊ณ„๊ฐ’์„ ํ•œ ๋ฒˆ์— ๊ฒ€์ฆ
ยท ๋ชจํ‚น์€ "๋‚ด๊ฐ€ ์†Œ์œ ํ•˜์ง€ ์•Š์€ ๊ฒƒ"์—๋งŒ
ยท ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์ง€๋„์ผ ๋ฟ, ๋ชฉ์ ์ง€๊ฐ€ ์•„๋‹ˆ๋‹ค
ยท ์‹œ์ž‘์€ ์ƒˆ ์ฝ”๋“œ์™€ ๋ฒ„๊ทธ ์ง€์ ๋ถ€ํ„ฐ, ์ „๋ถ€ ๋‹ค ํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋ง ๊ฒƒ

์ž, ์ด์ œ ํ„ฐ๋ฏธ๋„ ์—ด๊ณ  composer require --dev phpunit/phpunit ํ•œ ์ค„๋งŒ ์ณ๋ณด์ž.
์ฒซ ๋ฒˆ์งธ ๋นจ๊ฐ„๋ถˆ์ด ๋‹น์‹ ์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ์–ด. ๐Ÿ”ด โ†’ ๐ŸŸข

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.