프로젝트 규모가 커질수록 기존 코드를 수정할 때 예기치 않은 버그가 발생할까 봐 불안해했던 경험이 있을 것이다.
다만 대부분의 개발자들은 테스트 코드 작성 방법을 제대로 모른 채 일일이 브라우저에서 직접 입력값을 넣어가며 수동 테스트를 진행하곤 한다.
이번에는 PHPUnit을 이용한 단위 테스트(Unit Test)의 기본 개념부터 왜 필요한지, 그리고 외부 API나 DB 연동 없이 비즈니스 로직을 독립적으로 검증하는 Mock 객체 활용법까지 완벽하게 정리해서 소개하겠다.

 

1. 단위 테스트(Unit Test)와 Mock 객체의 개념

단위 테스트는 애플리케이션에서 검증 가능한 가장 작은 단위(주로 클래스나 메서드)를 독립적으로 테스트하는 기법이다.
개발자가 코드를 변경했을 때 기존 기능이 고장 나지 않았음을 몇 초 만에 확신할 수 있게 해준다.
그러나 실무 코드는 데이터베이스 접속, 외부 API 호출, 파일 시스템 등 수많은 외부 의존성을 가지고 있다.
이때 진짜 객체 대신 가짜 동작을 정의할 수 있는 'Mock 객체'를 만들어 사용하면, 외부 환경에 영향받지 않는 빠르고 안정적인 테스트를 작성할 수 있다.

 

테스트 방식 비교
구분수동 테스트단위 테스트 (Unit Test)통합 테스트 (Integration Test)
실행 속도매우 큼 (분 단위)매우 빠름 (밀리초 단위)보통 (초 단위)
외부 의존성실제 DB 및 API 연동Mock 객체로 완전 분리실제 DB 또는 테스트 DB 사용
자동화 여부불가 (사람이 직접 수행)CI/CD 파이프라인 자동화 가능자동화 가능
버그 탐지 시점QA 또는 배포 후코드 작성 직후 (개발 단계)기능 통합 및 배포 전

 

2. PHPUnit 환경 구성 및 기초 테스트 작성을 위한 방법

PHP 프로젝트에서 PHPUnit을 도입하는 방법은 아주 간단하다.
Composer를 통해 개발용 의존성(dev dependency)으로 설치한 뒤, 루트 디렉토리에 설정 파일인 phpunit.xml을 추가하면 된다.

 

Composer를 이용한 설치 명령어
composer require --dev phpunit/phpunit ^10.0

설치가 완료되면 프로젝트 루트에 phpunit.xml 파일을 생성하여 테스트 디렉토리 위치와 기본 동작 설정을 지정한다.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

 

3. 실전 예제: 결제 서비스 테스트와 Mock 객체 적용

외부 PG사 API를 호출하여 결제를 처리하는 PaymentService 클래스를 예로 들어보자.
진짜 PG API를 호출하는 테스트를 작성하면 실제 돈이 결제되거나, 네트워크 상태에 따라 테스트 실패가 발생한다.

 

✗ 잘못된 코드 (실제 외부 API에 의존하는 테스트)

✗ 외부 서비스를 직접 호출하면 테스트 실행 속도가 느려지고, 네트워크 장애 시 테스트가 실패하게 된다.

<?php
use PHPUnitFrameworkTestCase;

class PaymentServiceTest extends TestCase
{
    public function testProcessPayment()
    {
        // 실제 PG사 API 클라이언트를 그대로 사용 (테스트 환경에서 결제 승인 요청이 전송됨)
        $pgClient = new RealPgApiClient('API_SECRET_KEY');
        $paymentService = new PaymentService($pgClient);

        $result = $paymentService->pay(10000);

        // 네트워크 상태나 API 키 유효성에 따라 결과가 달라짐
        $this->assertTrue($result);
    }
}

 

✓ 올바른 코드 (PHPUnit Mock 객체 활용)

✓ createMock() 함수를 사용해 PgApiClient의 모의 객체를 만들고, 원하는 반환값을 지정(stubbing)하여 외부 환경과 완벽히 격리한다.

<?php
use PHPUnitFrameworkTestCase;

class PaymentServiceTest extends TestCase
{
    public function testProcessPaymentWithMock()
    {
        // 1. PgApiClient 인터페이스 또는 클래스의 Mock 객체 생성
        $pgClientMock = $this->createMock(PgApiClient::class);

        // 2. Mock 객체의 동작 정의: pay() 메서드가 10000 인자로 1회 호출되면 true를 반환하도록 설정
        $pgClientMock->expects($this->once())
                     ->method('requestPay')
                     ->with(10000)
                     ->willReturn(true);

        // 3. 의존성 주입(DI)을 통해 Mock 객체를 서비스에 전달
        $paymentService = new PaymentService($pgClientMock);

        // 4. 테스트 실행 및 결과 검증
        $result = $paymentService->pay(10000);
        $this->assertTrue($result);
    }
}

 

테스트 실행 결과 (터미널 출력)
./vendor/bin/phpunit tests/PaymentServiceTest.php

PHPUnit 10.5.0 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.0
Configuration: /path/to/project/phpunit.xml

.                                                                   1 / 1 (100%)

Time: 00:00.015, Memory: 6.00 MB

OK (1 test, 2 assertions)

 

4. 실무에서 흔히 하는 실수와 주의사항

단위 테스트를 작성할 때 자주犯하는 실수들을 미리 파악해두면 훨씬 깔끔한 테스트 코드를 유지할 수 있다.

  • private 메서드를 직접 테스트하려는 시도: private 메서드는 구현 세부사항이다. public 메서드를 테스트하면서 자연스럽게 함께 검증하는 것이 올바른 구조다.
  • 하나의 테스트 메서드에서 너무 많은 로직 검증: 테스트 메서드 하나당 하나의 시나리오(Assert)만 집중 검증해야 실패 원인을 즉시 파악할 수 있다.
  • 과도한 Mocking: 모든 객체를 Mock으로 만들어버리면 실제 코드가 동작하는 방식과 달라져 테스트의 신뢰성이 떨어진다. 순수 단일 값이나 VO(Value Object)는 진짜 객체를 사용하는 것이 좋다.

 

잘못된 패턴 vs 올바른 패턴

✗ 잘못된 예: private 메서드를 검증하려고 ReflectionClass를 이용해 강제로 접근 제한을 푸는 경우
✓ 올바른 예: public 메서드의 입력과 출력을 통해 내부 비즈니스 로직을 간접 검증하는 경우

 

5. 마무리 및 요약

PHPUnit을 활용한 단위 테스트와 Mocking 기법은 안정적인 웹 서비스를 유지하기 위해 선택이 아닌 필수다.
작은 단위의 자동화된 테스트 코드 작성이 모여서 배포 후 발생하는 대형 장애를 미연에 방지한다는 점을 잊지 말자.
이 글의 Mock 객체 활용 예제를 참고해 프로젝트에서 가장 핵심이 되는 비즈니스 로직 하나부터 테스트 코드를 작성해 보면, 코드 리팩토링과 새로운 기능 추가가 한결 두렵지 않아질 것이다.