Testo: 독립형 PHP 테스트 및 벤치마킹 프레임워크
새로운 Testo 프레임워크가 베타 테스트를 위해 공개되었습니다. 이 프레임워크는 PHPUnit에 대한 의존성이 전혀 없으며, PHP 8.2 이상을 지원하고 플러그인, 인라인 테스트, 벤치마킹 기능을 모두 제공합니다. 중급에서 고급 수준의 PHP 개발자를 대상으로 설계되었으며, 유연성, 엄격한 타이핑, 최소한의 부가 코드(boilerplate)를 핵심 가치로 삼습니다.
PHPUnit 및 기타 대안과의 주요 차이점
Testo는 PHPUnit의 근본적인 한계 — 즉, 유연성 부족, 자주 발생하는 호환성 깨짐, 그리고 기본 PHP 버전 지원을 넘어서는 발전 정체 — 를 해결하기 위해 개발되었습니다. 본 프레임워크의 개발자는 PHPUnit 이슈 트래커와 PR에도 기여했으나, 제안된 개선 사항은 채택되지 않았습니다. PEST와 Codeception 역시 PHPUnit의 구조적 제약을 그대로 계승하면서 오히려 복잡하고 어색한 문법을 추가했습니다.
Testo는 완전히 독립적입니다:
nikic/php-parser를 사용하지 않아 파서 관련 충돌을 완전히 배제합니다.- PHP 8.2 이상만을 요구합니다.
llms.txt라는 공식 문서 기반의 구조화된 사양을 통해 AI 에이전트가 자동으로 테스트 코드를 생성할 수 있습니다.
모듈식 플러그인 시스템을 통해 프로젝트별 맞춤화가 가능합니다. 모든 기능은 선택형 플러그인으로 제공되며, 각 테스트 스위트가 필요에 따라 자체적으로 플러그인을 구성합니다. 예를 들어, #[Retry] 플러그인은 불안정한 테스트(flaky test)에 대한 내성을 부여하며, 단 두 개의 가벼운 파일로 구현됩니다.
파이프라인, 미들웨어, 이벤트를 활용해 동작을 확장하세요. #[TestInline] 어트리뷰트를 사용해 src/ 디렉터리 내 실제 코드에 직접 테스트를 작성하고, 어트리뷰트를 통해 테스트 로직을 깔끔하게 통합할 수 있습니다.
설치 및 설정
Composer로 설치하세요:
composer require --dev testo/testo
프로젝트 루트에 testo.php 파일을 생성하세요:
<?php
declare(strict_types=1);
use Testo/Application/Config/ApplicationConfig;
use Testo/Application/Config/SuiteConfig;
return new ApplicationConfig(
suites: [
new SuiteConfig(
name: 'Sources',
location: ['src'],
),
new SuiteConfig(
name: 'Tests',
location: ['tests'],
),
],
);
이 파일은 ApplicationConfig 인스턴스를 반환해야 합니다. 파일이 없으면 Testo는 기본값(예: tests/ 디렉터리 스캔)으로 자동 전환됩니다. 기본적으로 두 개의 테스트 스위트가 정의되어 있으며, Sources는 프로덕션 코드 내에 포함된 인라인 테스트 및 벤치마킹을 위한 것이고, Tests는 전통적인 유닛 테스트를 위한 것입니다.
테스트 실행은 ./vendor/bin/testo 명령어로 수행하거나, 공식 PHPStorm 플러그인을 사용해 IDE와 원활하게 연동할 수 있습니다.
유닛 테스트 예시
상속이 필요 없습니다. 메서드에 #[Test] 어트리뷰트만 붙이면 됩니다:
final class OrderTest
{
#[Test]
public function calculatesTotal(): void
{
$order = new Order();
$order->addItem('Book', price: 15.0, quantity: 2);
$order->addItem('Pen', price: 3.0, quantity: 5);
Assert::same($order->total(), 45.0);
}
#[Test]
#[DataSet([100.0, 10, 90.0], '10% off')]
#[DataSet([100.0, 0, 100.0], 'no discount')]
#[DataSet([0.0, 50, 0.0], 'zero price')]
public function appliesDiscount(float $price, int $percent, float $expected): void
{
$result = Order::applyDiscount($price, $percent);
Assert::same($result, $expected);
}
#[Test]
#[ExpectException(InsufficientFundsException::class)]
public function cannotOverdraw(): never
{
new Account(balance: 100)->withdraw(200);
}
}
클래스 전체에 #[Test] 어트리뷰트를 적용하면, 해당 클래스 내 모든 void 또는 never 반환 타입 메서드가 자동으로 테스트로 인식됩니다. 클래스 외부의 독립 함수도 생명주기 어트리뷰트 없이 바로 실행됩니다.
어설션은 직관적인 순서를 따릅니다: 실제 값(actual)을 먼저, 기대 값(expected)을 다음에 입력합니다. 체이닝 가능하고 타입 안전한 어설션:
Assert::string($email)->contains('@');
Assert::int($age)->greaterThan(0)->lessThan(150);
Assert::array($items)
->hasKeys('id', 'name')
->isList()
->notEmpty();
Assert::json($response->body())
->isObject()
->hasKeys('data', 'meta');
인라인 테스트 및 벤치마킹
src/ 내부에서 #[TestInline]을 사용해 비공개 또는 내부 메서드를 직접 검증하세요:
// src/Money.php
final class Money
{
#[TestInline(['price' => 100.0, 'discount' => 0.1, 'tax' => 0.2], 108.0)]
#[TestInline(['price' => 50.0, 'discount' => 0.0, 'tax' => 0.1], 55.0)]
private static function calculateFinalPrice(
float $price,
float $discount,
float $tax,
): float {
return $price * (1 - $discount) * (1 + $tax);
}
}
#[Bench] 어트리뷰트로 성능 벤치마킹을 수행하세요:
#[Bench(
callables: [
'multiply' => 'viaMultiply',
'shift' => 'viaShift',
],
arguments: [1, 5_000],
calls: 2_000_000,
)]
function viaDivision(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d / 2) + $a * $d;
}
function viaMultiply(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d * 0.5) + $a * $d;
}
function viaShift(int $a, int $b): int
{
$d = $b - $a + 1;
return ((($d - 1) * $d) >> 1) + $a * $d;
}
샘플 출력:
+---+----------+-------+---------+------------------+--------+
| # | Name | Iters | Calls | Avg Time | RStDev |
+---+----------+-------+---------+------------------+--------+
| 2 | current | 10 | 2000000 | 75.890µs | ±0.79% |
| 3 | multiply | 10 | 2000000 | 78.821µs (+3.9%) | ±0.47% |
| 1 | shift | 10 | 2000000 | 70.559µs (-7.0%) | ±0.70% |
+---+----------+-------+---------+------------------+--------+
왜 중요한가?
- 진정한 독립형 프레임워크: PHPUnit 의존성 제로, 조합 가능한 플러그인으로 확장 가능.
- 인라인 테스트(
#[TestInline])와 벤치마킹(#[Bench])이src/내 프로덕션 코드 바로 옆에 함께 존재합니다. - 타입 안전하고 유창한 어설션 체인: 부가 코드 없음, 상속 없음, 마법의 전역 변수 없음.
- 순수 PHP 파일 기반 설정: 다중 테스트 스위트 프로젝트에 완벽히 대응.
- 일류 PHPStorm 플러그인 및 CLI 도구 지원: 실무 개발 현장에서 바로 사용 가능.
— Editorial Team
아직 댓글이 없습니다.