PHpullh

PHP · 심층 가이드

PHP 테스트 완전 정리

직접 만든 assert 헬퍼에서 시작해 PHPUnit 데이터 제공자, 익명 클래스 스텁, 메모리 SQLite 통합 테스트까지 12가지 예제로 실제 돌아가는 PHP 테스트를 짭니다.

주제 12개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

PHP의 테스트는 다른 언어와 출발점이 다릅니다. 요청 하나가 끝나면 프로세스 상태가 통째로 사라지는 실행 모델 덕분에, 개발자는 오래 살아 있는 객체 그래프를 신경 쓰지 않아도 되는 대신 전역 함수와 superglobal에 기대는 코드를 아주 쉽게 쓰게 됩니다. 그런 코드는 돌아갈 때는 멀쩡하지만 테스트에서는 손댈 구멍이 없습니다. 테스트 가능한 PHP를 쓴다는 것은 결국 입력을 인자로 받고 결과를 반환값으로 돌려주는 함수 경계를 만든다는 뜻이고, 이 가이드의 예제들은 대부분 그 경계를 어디에 그을지에 대한 이야기입니다.

순서대로 읽으면 도구가 아니라 구조가 쌓입니다. 간단한 assert 헬퍼는 테스트 프레임워크가 실제로 하는 일이 조건 확인과 실패 보고뿐임을 보여주고, PHPUnit 기본 테스트가 그 위에 라이프사이클과 리포팅을 얹습니다. 같은 검증 로직을 입력만 바꿔 반복해야 할 때는 데이터 제공자로 케이스를 표처럼 펼치고, 협력 객체가 끼어들면 무거운 목 라이브러리 대신 익명 클래스로 스텁 만들기로 그 자리에서 가짜를 만듭니다. 마지막에 임시 SQLite 통합 테스트가 붙으면 단위 테스트가 못 잡는 쿼리 오타까지 걸러집니다.

가장 많이 물리는 지점은 비교 연산입니다. PHPUnit의 assertEquals는 느슨한 비교라 "2"2, 빈 배열과 false 같은 조합을 통과시키므로, 타입까지 확인하려면 assertSame을 써야 합니다. PHP 8부터 숫자와 비숫자 문자열의 비교 규칙이 바뀌어 예전 코드의 0 == "abc" 같은 표현이 다르게 동작한다는 점도 기억해 둘 만합니다. 또 하나, 메모리 SQLite는 연결 단위로 만들어지므로 커넥션을 새로 열면 스키마가 없는 빈 DB를 만나게 됩니다.

01간단한 assert 헬퍼

테스트 프레임워크 없이도 가장 작은 검증 구조를 만들 수 있음을 보여주는 예제입니다.

PHP code

<?php
function assertSame(mixed $expected, mixed $actual): void {
    if ($expected !== $actual) {
        throw new RuntimeException('assert failed');
    }
}

assertSame(3, 1 + 2);
echo 'ok';
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

02PHPUnit 기본 테스트

PHPUnit의 기본 테스트 클래스와 assertion 메서드를 보는 예제입니다.

PHP code

<?php
use PHPUnit\Framework\TestCase;

final class SluggerTest extends TestCase {
    public function testItCreatesSlug(): void {
        $this->assertSame('hello-php', str_replace(' ', '-', strtolower('Hello PHP')));
    }
}
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

네임스페이스와 autoload 설정 없이 바로 실행하려 하면 PHPUnit이 클래스를 찾지 못할 수 있습니다.

03데이터 제공자

같은 테스트 로직을 여러 입력으로 돌리는 데이터 제공자 예제입니다.

PHP code

<?php
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class PriceFormatterTest extends TestCase {
    #[DataProvider('cases')]
    public function testFormat(int $input, string $expected): void {
        $this->assertSame($expected, number_format($input) . ' KRW');
    }

    public static function cases(): array {
        return [[1000, '1,000 KRW'], [12000, '12,000 KRW']];
    }
}
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

04익명 클래스로 스텁 만들기

복잡한 mocking 라이브러리 없이 가벼운 스텁을 만드는 예제입니다.

PHP code

<?php
interface Clock {
    public function now(): string;
}

$clock = new class implements Clock {
    public function now(): string { return '2026-03-28T09:00:00+09:00'; }
};

echo $clock->now();
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

05예외 테스트

특정 예외가 발생해야 성공하는 테스트를 작성하는 예제입니다.

PHP code

<?php
use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase {
    public function testInvalidEmailThrows(): void {
        $this->expectException(InvalidArgumentException::class);
        throw new InvalidArgumentException('invalid');
    }
}
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

06임시 SQLite 통합 테스트

외부 DB 없이 메모리 SQLite로 통합 테스트를 빠르게 돌리는 예제입니다.

PHP code

<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT)');
$pdo->exec("INSERT INTO posts (title) VALUES ('PHP')");

echo $pdo->query('SELECT COUNT(*) FROM posts')->fetchColumn();
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

07JSON 스냅샷 비교

응답 구조를 문자열 스냅샷처럼 비교하는 간단한 예제입니다.

PHP code

<?php
$actual = json_encode(['lang' => 'php', 'ok' => true], JSON_PRETTY_PRINT);
$expected = "{
    "lang": "php",
    "ok": true
}";

var_dump($actual === $expected);
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

08CLI 커맨드 테스트

커맨드라인 스크립트 결과를 프로세스로 실행해 검증하는 예제입니다.

PHP code

<?php
$process = proc_open('php -r "echo 42;"', [1 => ['pipe', 'w']], $pipes);
$output = trim(stream_get_contents($pipes[1]));
proc_close($process);

var_dump($output === '42');
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

09HTTP 핸들러 테스트 구조

프레임워크 없이도 요청 배열과 응답 배열을 분리해 핸들러를 테스트하는 예제입니다.

PHP code

<?php
function handle(array $request): array {
    return ['status' => 200, 'body' => ['path' => $request['path']]];
}

$response = handle(['path' => '/health']);
var_dump($response['status'] === 200);
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

10로거 테스트 더블

호출 여부를 메모리에 기록하는 테스트 더블 로거 예제입니다.

PHP code

<?php
interface Logger {
    public function info(string $message): void;
}

class ArrayLogger implements Logger {
    public array $messages = [];
    public function info(string $message): void { $this->messages[] = $message; }
}

$logger = new ArrayLogger();
$logger->info('saved');
var_dump($logger->messages === ['saved']);
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

11엄격 비교로 버그 막기

느슨한 비교가 테스트를 거짓 양성으로 만드는 상황을 보여주는 예제입니다.

PHP code

<?php
$payload = ['count' => '1'];
var_dump($payload['count'] === 1);
var_dump($payload['count'] == 1);
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

== 로 테스트를 쓰면 버그를 놓칠 가능성이 크므로 가능하면 === 를 기본값으로 두는 것이 좋습니다.

12간단한 랜덤 입력 테스트

고정 케이스 외에 랜덤 입력으로 기본 성질을 점검하는 가벼운 property 스타일 테스트 예제입니다.

PHP code

<?php
for ($i = 0; $i < 5; $i++) {
    $value = random_int(1, 100);
    if ($value < 1 || $value > 100) {
        throw new RuntimeException('out of range');
    }
}

echo 'ok';
알아두면 좋은 점

테스트 예제는 성공 케이스뿐 아니라 실패 케이스와 경계값도 함께 실행해 보는 것이 좋습니다.

자주 하는 실수

테스트가 외부 상태를 공유하면 실행 순서에 따라 통과 여부가 달라질 수 있습니다.

정리하며

  • 테스트가 어려운 코드는 대개 전역 상태에 의존하는 코드입니다. 먼저 인자와 반환값으로 경계를 만드세요.
  • 타입까지 검증해야 하는 자리에서는 assertEquals 대신 assertSame을 기본값으로 두는 편이 안전합니다.
  • 반복되는 케이스는 테스트 메서드를 복사하지 말고 데이터 제공자로 표처럼 정리하면 누락이 눈에 보입니다.
  • 메모리 SQLite 통합 테스트는 연결을 재사용해야 하며, 픽스처는 테스트마다 새로 만들어 격리를 유지합니다.

더 깊이 들어가고 싶다면 PHP 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.