통합 테스트와 기능 테스트
실제 환경에서 구성 요소들이 함께 작동하는 방식을 테스트합니다.
통합 테스트와 기능 테스트은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
단위 테스트를 넘어서
단위 테스트는 모든 것을 모의 객체로 대체한 상태에서 클래스가 고립되어 작동한다는 것을 입증합니다. 하지만 모의 객체는 거짓된 결과를 낼 수 있습니다. 실제 데이터베이스, 프레임워크 라우터 또는 HTTP 계층은 다르게 동작하기 때문입니다. 통합 테스트는 둘 이상의 실제 구성 요소를 연결해 함께 검증하고, 기능 테스트는 공개 진입점(HTTP 요청, 콘솔 명령)을 통해 애플리케이션을 처음부터 끝까지 실행합니다. 이 레슨에서는 두 종류의 테스트를 적절히 사용하고 작성합니다.
테스트 피라미드
건강한 테스트 모음은 피라미드 형태입니다.
- 많은 단위 테스트 — 빠르고 고립되어 있으며 지속적으로 실행됩니다.
- 더 적은 통합 테스트 — 실제 데이터베이스/대기열/캐시를 사용하므로 더 느립니다.
- 소수의 기능/종단 간 테스트 — 전체 스택을 사용하고 가장 느리며 가장 불안정합니다.
이를 거꾸로 구성해 대부분 종단 간 테스트인 '아이스크림 콘' 형태로 만들면 느리고 불안정한 테스트 모음이 됩니다. 가능할 때마다 커버리지를 더 낮은 계층으로 내리십시오.
통합 테스트: 실제 데이터베이스를 대상으로 하는 저장소
저장소에 대한 통합 테스트는 모의 객체가 아닌 실제 데이터베이스 연결을 사용해 SQL, 스키마 및 매핑 오류를 찾아냅니다. 전용 테스트 데이터베이스를 사용하거나, 사용하는 데이터베이스 문법과 충분히 비슷한 메모리 내 SQLite를 사용하고 실제 PDO를 연결하십시오.
<?php
use PHPUnit\Framework\TestCase;
final class UserRepositoryTest extends TestCase
{
private \PDO $pdo;
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)');
}
public function test_persists_and_finds_a_user(): void
{
$repo = new UserRepository($this->pdo);
$id = $repo->create('ada@example.com');
self::assertSame('ada@example.com', $repo->find($id)->email);
}
}
격리: 트랜잭션 롤백
통합 테스트는 서로의 상태를 유출해서는 안 됩니다. 가장 빠른 패턴은 모든 테스트를 트랜잭션으로 감싼 뒤 tearDown()에서 롤백하는 것입니다. 그러면 다시 데이터를 채우지 않아도 데이터베이스가 깨끗한 상태로 돌아갑니다. (테스트 대상 코드가 커밋하거나 DDL을 사용하는 경우에는 테이블 비우기가 대안입니다.)
<?php
protected function setUp(): void
{
$this->pdo = TestDb::connection();
$this->pdo->beginTransaction();
}
protected function tearDown(): void
{
$this->pdo->rollBack(); // undo everything this test did
}
픽스처와 팩토리
테스트에는 데이터가 필요합니다. 깨지기 쉬운 거대한 SQL 덤프 대신, 테스트에 필요한 행만 생성하는 작은 팩토리를 사용하고 각 경우에 맞게 합리적인 기본값을 덮어쓰십시오. 이렇게 하면 각 테스트의 의도가 분명해지고 스키마가 확장되어도 잘 유지됩니다.
<?php
final class UserFactory
{
public static function create(\PDO $pdo, array $overrides = []): int
{
$data = array_merge(['email' => 'user'.uniqid().'@test.dev'], $overrides);
$stmt = $pdo->prepare('INSERT INTO users (email) VALUES (:email)');
$stmt->execute($data);
return (int) $pdo->lastInsertId();
}
}
기능 테스트: 클라이언트처럼 애플리케이션 호출하기
기능 테스트는 실제 애플리케이션 커널/라우터에 시뮬레이션한 요청을 보내고 상태 코드, 헤더, 본문과 같은 응답을 검증합니다. Symfony의 WebTestCase와 Laravel의 HTTP 테스트 도우미는 실제 웹 서버를 시작하지 않고도 정확히 이 작업을 수행합니다. 아래는 Symfony 방식입니다.
<?php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class HealthControllerTest extends WebTestCase
{
public function test_health_endpoint_returns_ok(): void
{
$client = static::createClient();
$client->request('GET', '/health');
self::assertResponseIsSuccessful(); // 2xx
self::assertJson($client->getResponse()->getContent());
}
}
응답 검증하기
기능 테스트는 경계에서 관찰 가능한 동작을 검증합니다. 상태, JSON 구조, 데이터베이스의 부수 효과가 그 예입니다. 아래에서는 POST로 리소스를 생성한 뒤 HTTP 응답과 레코드가 실제 저장소에 저장되었는지를 모두 확인해 전체 스택이 협력했음을 입증합니다.
<?php
public function test_creating_a_user_persists_it(): void
{
$client = static::createClient();
$client->request('POST', '/users', server: [
'CONTENT_TYPE' => 'application/json',
], content: json_encode(['email' => 'ada@example.com']));
self::assertResponseStatusCodeSame(201);
$repo = static::getContainer()->get(UserRepository::class);
self::assertNotNull($repo->findByEmail('ada@example.com'));
}
외부 경계만 선택적으로 가짜 처리하기
기능 테스트에서도 실제 결제 제공업체를 호출하거나 실제 이메일을 보내서는 안 됩니다. 가장 바깥쪽의 외부 서비스만 교체하십시오. 테스트 컨테이너에서 가짜 구현이나 메모리 내 구현으로 바꾸고, 자체 코드와 라우팅 및 데이터베이스는 실제 상태로 유지합니다. 이렇게 하면 불안정한 네트워크 호출 없이도 종단 간 동작의 충실도를 유지할 수 있습니다.
<?php
public function test_checkout_charges_via_gateway(): void
{
$client = static::createClient();
// Replace the real gateway binding with an in-memory fake:
static::getContainer()->set(PaymentGateway::class, new FakePaymentGateway());
$client->request('POST', '/checkout', content: json_encode(['cart' => 1]));
self::assertResponseIsSuccessful();
}
테스트 모음은 분리하고 실행 속도도 나누기
phpunit.xml을 테스트 모음으로 나누어 빠른 단위 테스트 모음은 저장할 때마다 실행하고, 느린 통합/기능 테스트 모음은 필요할 때나 지속적 통합 환경에서 실행하십시오. 디렉터리별로 그룹화하고 --testsuite로 선택하면 됩니다.
<phpunit>
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="functional">
<directory>tests/Functional</directory>
</testsuite>
</testsuites>
</phpunit>커버리지보다 결정성이 중요합니다
테스트 모음에 대한 신뢰를 가장 빠르게 잃게 만드는 것은 불안정성입니다. 통합/기능 테스트에서 항상 같은 결과가 나오도록 하십시오.
- 시간을 고정하고 (
time()을 호출하는 대신 시계를 주입합니다). - 무작위성의 시드를 설정하고, 정렬되지 않은 조회 결과에 순서 의존 검증을 하지 않습니다.
- 모든 테스트 후 데이터베이스 상태를 초기화합니다(트랜잭션 롤백 또는 테이블 비우기).
- 테스트 실행 순서에 절대 의존하지 않습니다.
테스트 데이터베이스를 운영 환경과 일치시키기
메모리 내 SQLite는 빠르지만 방언 차이(대소문자 구분, JSON 함수, 외래 키 적용, 자료형) 때문에 실제 데이터베이스 엔진에서만 드러나는 오류를 숨길 수 있습니다. 엔진별 SQL을 사용하는 코드라면 편리한 대체품 대신 운영 환경과 동일한 엔진을 대상으로 통합 테스트를 실행하십시오. 일반적으로 CI에서 일회용 Docker 컨테이너를 사용합니다.
<?php
// Read connection from environment so CI points at a real Postgres/MySQL container
$dsn = getenv('TEST_DATABASE_URL') ?: 'sqlite::memory:';
$pdo = new \PDO($dsn);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// In CI, TEST_DATABASE_URL targets the same engine as production.
빠른 확인
기능 테스트에서는 타사 결제 제공업체를 어떻게 다뤄야 할까요?
복습
구성 요소를 함께 테스트하는 방법을 배웠습니다.
- 통합 테스트는 실제 데이터베이스/서비스를 사용해 모의 객체가 숨기는 문제를 찾아내며, 트랜잭션 롤백으로 격리합니다.
- 기능 테스트는 시뮬레이션한 요청으로 실제 커널을 실행하고 응답과 부수 효과를 검증합니다.
- 팩토리는 최소한의 데이터를 생성하고, 가장 바깥쪽의 외부 경계만 가짜로 대체합니다.
- 테스트 모음을 속도별로 나누고 느린 테스트에서도 결정성을 유지합니다.
다음: 변이 테스트로 테스트 품질 측정하기.
자주 묻는 질문
“통합 테스트와 기능 테스트” 강의는 무료인가요?
네 — “통합 테스트와 기능 테스트” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“통합 테스트와 기능 테스트”에서 뭘 배우나요?
실제 환경에서 구성 요소들이 함께 작동하는 방식을 테스트합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
PHP Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“통합 테스트와 기능 테스트” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 테스트 주도 개발 워크플로
- Mockery로 모킹과 스터빙
- 통합 테스트와 기능 테스트
- Infection으로 변이 테스트