PHPUnit Annotation 정리
서론
PHP Unit을 사용하면서도 잘 모르는 어노테이션을 PHPUnit v8.4 기준(2019-11-19 최신버전)으로 정리하였습니다.
@author
테스트를 작성자별 그룹화 필터링 할 때
@group
어노테이션의 별칭으로, 테스트를 작성자별로 그룹화하여 필터링 하는데 사용할 수 있습니다.
@after
각 테스트가 끝난 뒤 실행 하려 할 때
각 테스트 메소드들이 실행 된 후, 특정 메소드를 실행하고자 할때 사용할 수 있습니다.
각 테스트가 끝난 뒤 트랜잭션 커밋or롤백을 한다거나, 생성된 파일을 삭제하는 등의 처리를 하는데 사용하면 좋을 것 같습니다.
namespace Tests;
use PHPUnit\Framework\TestCase;
class MyTest extends TestCase
{
/**
* @after
*/
public function afterDo()
{
echo "After Method Called" . PHP_EOL;
}
public function test1()
{
echo "test1 Method Called" . PHP_EOL;
self::assertTrue(true);
}
public function test2()
{
echo "test2 Method Called" . PHP_EOL;
self::assertTrue(true);
}
}
// 출력 결과
test1 Method Called
After Method Called
test2 Method Called
After Method Called
```
@afterClass
모든 테스트가 끝난 후 실행 하려 할 때
모든 테스트가 끝난 후, 공유된 자원들을 정리하기 위해 호출할 정적 메소드를 지정 할 수 있습니다.
해당 부분에서 테스트 실행시 만든 DB 커넥션을 회수하거나, 전체 트랜잭션을 처리 하거나, 소켓을 닫는 등의 처리를 할 수 있을것 같습니다.
namespace Tests;
use PHPUnit\Framework\TestCase;
class MyTest extends TestCase
{
/**
* @afterClass
*/
public static function afterClassDo()
{
echo "After Class Method Called" . PHP_EOL;
}
public function test1()
{
echo "test1 Method Called" . PHP_EOL;
self::assertTrue(true);
}
public function test2()
{
echo "test2 Method Called" . PHP_EOL;
self::assertTrue(true);
}
}
// 출력 결과
test1 Method Called
test2 Method Called
After Class Method Called
```
@backupGlobals
글로벌 변수를 유지하고 싶다면
모든 글로벌 변수를 각 테스트 전에 백업하고, 각 테스트 이후 해당 백업을 복원시킵니다.
메소드 레벨에서 재정의가 가능합니다.
해당 설명만으로는 이해가 잘 되지 않아서 직접 예제 코드를 만들어 보았습니다.
클래스 스코프 밖에 정의된 글로벌 변수인 $className
을 @backupGlobals
어노테이션이 enabled 되어 있는 테스트 코드에서는 실행이전 값을 백업하여두고 테스트가 끝나면 복원이 되어,
두번째 테스트코드에서도 “MyTest"라는 값을 가지고 있게됩니다.
namespace Tests;
use PHPUnit\Framework\TestCase;
$className = "MyTest";
/**
* @backupGlobals enabled
*/
class MyTest extends TestCase
{
public function test_글로벌변수를_백업하고_변경()
{
global $className;
$this->assertEquals($className, "MyTest");
$className = "Foo";
}
/**
* @backupGlobals disabled
*/
public function test_글로벌변수를_백업하지_않고_변경()
{
global $className;
$this->assertEquals($className, "MyTest");
$className = "Bar";
}
public function test_글로벌변수를_백업되어있는지_체크()
{
global $className;
$this->assertEquals($className, "Bar");
}
}
```
@backupStaticAttributes
정적 속성을 사용하려 할 때
선언된 클래스들 안의 모든 정적 속성을 각 테스트 전에 백업하고, 각 테스트 후에 해당 백업을 복원 시킵니다.
클래스 레벨에도 선언 가능하며, 각 테스트 메소드에서 추가 제어 가능합니다.
use PHPUnit\Framework\TestCase;
/**
* @backupStaticAttributes enabled
*/
class MyTest extends TestCase
{
public function test_정적속성을_사용하는_테스트()
{
// ...
}
/**
* @backupStaticAttributes disabled
*/
public function test_정적속성을_사용하지_않는_테스트()
{
// ...
}
}
```
@before
각 테스트 실행전에 실행 하려 할 때
각 테스트 메소드가 호출되기 전에 실행할 메소드를 지정할 수 있습니다.
아래와 같이 beforeMethod는 각 메소드 호출전에 실행되지만, users 배열의 값이 증가되지는 않습니다.
class MyTest extends TestCase
{
protected $users = [];
/**
* @before
*/
public function beforeMethod()
{
echo "Before Method Called" . PHP_EOL;
$this->users[] = [
'name' => '홍길동'
];
}
public function test1()
{
echo "test1 Method Called" . PHP_EOL;
self::assertCount(1, $this->users);
}
public function test2()
{
echo "test2 Method Called" . PHP_EOL;
self::assertCount(1, $this->users);
}
}
// 출력 결과
Before Method Called
test1 Method Called
Before Method Called
test2 Method Called
```
@beforeClass
테스트 실행전 공유 속성을 만들 때
해당 클래스에서 테스트가 실행되기전 공유 하기 위한 정보를 설정하기 위해 호출 할 static 메소드에 지정하여 사용할 수 있습니다.
class MyTest extends TestCase
{
protected $users = [];
/**
* @beforeClass
*/
public static function beforeClass()
{
echo "Before Class Called" . PHP_EOL;
}
/**
* @before
*/
public function beforeMethod()
{
echo "Before Method Called" . PHP_EOL;
$this->users[] = [
'name' => '홍길동'
];
}
public function test1()
{
echo "test1 Method Called" . PHP_EOL;
self::assertCount(1, $this->users);
}
public function test2()
{
echo "test2 Method Called" . PHP_EOL;
self::assertCount(1, $this->users);
}
}
// 출력 결과
Before Class Called
Before Method Called
test1 Method Called
Before Method Called
test2 Method Called
```
@codeCoverageIgnore*
코드 커버리지 분석시 제외할 라인에 사용할 수 있습니다.
/**
* @codeCoverageIgnore
*/
class Foo
{
public function bar()
{
}
}
class Bar
{
/**
* @codeCoverageIgnore
*/
public function foo()
{
}
}
if (false) {
// @codeCoverageIgnoreStart
print '*';
// @codeCoverageIgnoreEnd
}
exit; // @codeCoverageIgnore
```
@covers
테스트 영역을 명시하려 할 때
어떤 영역을 테스트 하고자 하는지 명시하고자 할 때 사용합니다.
이와 같이 명시 하면 IDE(PHPStorm)에서 연결되어 있어 ctrl+shift+T 를 이용해 테스트로 바로 이동이 가능해지고, usage로 찾을 수 있어 메소드명 수정시 같이 반영됩니다.
@coversDefaultClass
너무 긴 네임스페이스와 클래스명을 반복해서 쓰고 싶지 않을 때
기본 네임스페이스나 클래스명을 명시하는데 사용할 수 있어, @covers
어노테이션에 긴 네임스페이스나, 클래스명을 반복해서 사용할 필요가 없어집니다.
해당 어노테이션에는 정규화 된 클래스명을 사용해야하기때문에,
모호하지 않도록 클래스명 맨 앞에 \
로 시작하는것을 추천합니다.
아래 예제 코드와 같이 @covers \Foo\CoveredClass::publicMethod
를 @covers ::publicMethod
로 줄여 쓸 수 있는 이점을 얻게 됩니다.
/**
* @coversDefaultClass \Foo\CoveredClass
*/
class CoversDefaultClassTest extends TestCase
{
/**
* @covers ::publicMethod
*/
public function testSomething()
{
$o = new Foo\CoveredClass;
$o->publicMethod();
}
}
```
@coversNothing
작성예정
클래스나 메소드레벨에서 사용할 수 있고 @covers
어노테이션을 덮어 씁니다.
@DataProvider
메소드를 이용해 파라미터를 주입하고 싶을때
@dataProvider
를 사용하면 메소드의 파라미터로 전달할 수 있습니다.
Java Junit 패키지에서 JunitParams를 이용하여 @Parameters
어노테이션을 사용하는것과 동일한 효과를 얻을 수 있습니다.
- 예제 코드
아래 예제 코드와 같은 테스트는 배열의 각 값 들이
$a
,$b
,$expected
로 바인딩 되며, 총 4개의 배열이 자동 주입되어 테스트가 4회 수행됩니다.<?php use PHPUnit\Framework\TestCase; class DataTest extends TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertSame($expected, $a + $b); } public function additionProvider() { return [ [0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 3] ]; } }
아래와 같이 이름이 정의된 dataset을 사용할 수도 있습니다.
<?php use PHPUnit\Framework\TestCase; class DataTest extends TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertSame($expected, $a + $b); } public function additionProvider() { return [ 'adding zeros' => [0, 0, 0], 'zero plus one' => [0, 1, 1], 'one plus zero' => [1, 0, 1], 'one plus one' => [1, 1, 3] ]; } }
@depends
테스트 코드간의 종속성 정의
@depends
어노테이션 사용시 테스트 코드간의 종속성을 선언 할 수 있습니다.
실행순서를 정의하는것은 아니지만, @depends
에 정의된 테스트의 리턴값의 레퍼런스를 전달합니다.
레퍼런스 전달이 아닌 값의 깊은 복사를 원할 경우 @depends clone
를 이용하고,
PHP에서 clone으로 불리는 얕은 복사를 원할 경우 @depends shallowClone
를 이용하면 됩니다.
@doesNotPerformAssertions
값에 대한 assertion 없이 테스트 코드를 실행만 하고자 할때
아래와 같이 테스트를 수행하지 않을 경우 This test did not perform any assertions
와 같은 Warning이 발생됩니다.
해당 어노테이션을 사용하면 Risky 없이 OK (1 test, 0 assertions)
로 성공 처리됩니다.
namespace Tests;
use PHPUnit\Framework\TestCase;
class MyTest extends TestCase
{
public function testAddSlashes()
{
echo addslashes("name='1'");
}
}
// 출력 결과
This test did not perform any assertions
/opt/project/tests/MyTest.php:9
name=\'1\'
OK, but incomplete, skipped, or risky tests!
Tests: 1, Assertions: 0, Risky: 1.
<?php
namespace Tests;
use PHPUnit\Framework\TestCase;
class MyTest extends TestCase
{
/**
* @doesNotPerformAssertions
*/
public function testAddSlashes()
{
echo addslashes("name='1'");
}
}
```
@group
테스트 코드에 태그를 달고 싶을때
@group
어노테이션을 이용하여, 테스트 코드에 1개 이상의 태그와 같이 묶음 필터를 추가할 수 있습니다.
XML 설정 파일 주입을 이용 하거나 CLI에서 실행시 --group
과 --exclude-group
를 이용해서 테스트 실행 대상 또는 제외그룹을 설정할 수 있습니다.
@large
60초 이상 실행 되면 실패 처리 하고자 할 때
@group large
의 별칭으로,PHP_Invoker
패키지가 설치되어 있고, strict mode가 실행되어 있으면 60초 이상 실행 될 경우 실패 처리됩니다.
해당 타임아웃에 관한 정보는 설정 정보 XML의 timeoutForLargeTests
속성을 통해 설정 할 수 있습니다.
@medium
10초 이상 실행 되면 실패 처리 하고자 할 때
@group medium
의 별칭으로, PHP_Invoker
패키지가 설치되어 있고, strict mode가 실행되어 있으면 10초 이상 실행 될 경우 실패 처리됩니다.
해당 타임아웃에 관한 정보는 설정 정보 XML의 timeoutForMediumTests
속성을 통해 설정 할 수 있습니다.
Medium 테스트는 @large
테스트에 의존적이여서는 안됩니다.
@preserveGlobalState
테스트가 별도의 프로세스에서 실행될때 직렬화 오류 방지
테스트가 별도의 프로세스에서 실행될 때, PHPUnit은부모 프로세스에서 글로벌 state를 직렬화 한 값을 자식 프로세스에서 역직렬화하여 상태를 보존합니다.
부모 프로세스에서 직렬화 할 수 없는 글로벌 state가 있는 경우, 해당 옵션을 disable
처리하여 방지할 수 있습니다.
@requires
특정 조건일때만 테스트를 수행하고자 할 때
PHP의 버전이나 extensions 설치여부 등 전제 조건을 체크하여 테스트를 건너뛸 수 있습니다.
<
, <=
, >
, >=
, =
, ==
, !=
, <>
등의 비교 연산자를 사용하여 버전을 비교할 수 있습니다.
해당 어노테이션을 이용해 체크 가능한 항목은 아래와 같습니다.
- 체크 가능한 조건
- PHP
- PHP 버전
- PHPUnit
- PHP Unit 버전
- OS
PHP_OS
상수와 정규식으로 매칭되는 값입니다. ex) WIN32|WINNT
- OSFAMILY
PHP_OS_FAMILY
상수와 매칭되는 값으로 PHP 7.2.0부터 사용가능합니다. ex) Windows
- function
- 함수 존재 여부 → function_exists()
- extension
- extension 설치 여부 및 버전 체크
- PHP
/**
* @requires extension mysqli
*/
class DatabaseTest extends TestCase
{
/**
* @requires PHP >= 5.3
*/
public function testConnection()
{
// 해당 테스트는 mysqli 확장프로그램이 설치되어 있고, PHP 버전이 5.3 이상일때 실행됩니다.
}
// 추가적인 테스트를 작성하였을때에도 mysqli 확장프로그램이 필요합니다.
}
```
@runTestsInSeparateProcesses
테스트 클래스 내의 모든 테스트 메소드가 별도 PHP프로세스에서 테스트코드를 실행 하는것을 명시할 때
해당 테스트 클래스 내의 모든 테스트 메소드들이 별도의 PHP 프로세스에서 실행되어야 함을 표시 할 때 사용합니다.
PHPUnit은 직렬화를 통해 Global state를 유지하려 하기 때문에, 직렬화가 불가능한 부분은 @preserveGlobalState
를 참조하세요.
@runInSeparateProcess
해당 테스트 메소드가 별도의 PHP 프로세스에서 실행되어야 함을 표시 할 때 사용합니다.
PHPUnit은 직렬화를 통해 Global state를 유지하려 하기 때문에, 직렬화가 불가능한 부분은 @preserveGlobalState
를 참조하세요.
@small
@group small
의 별칭으로, PHP_Invoker
패키지가 설치되어 있고, strict mode가 실행되어 있으면 1초 이상 실행 될 경우 실패 처리됩니다.
해당 타임아웃에 관한 정보는 설정 정보 XML의 timeoutForSmallTests
속성을 통해 설정 할 수 있습니다.
Medium 테스트는 @large
와 @medium
로 마킹된 테스트에 의존적이여서는 안됩니다.
※ 테스트의 실행 시간 제어를 하고자 할 때, @small
, @medium
, @large
와 같은 어노테이션을 명시적으로 사용해야합니다.
@test
테스트 메소드명을 test로 시작하고싶지 않을 때
테스트 메소드는 메소드명의 prefix로 test를 사용합니다.
테스트 메소드명의 prefix로 test
를 사용하지 않는 대안으로, 주석에 @test
어노테이션을 사용하면 테스트 메소드라고 인식됩니다.
@testdox
testdox 옵션으로 생성되는 문서의 설명을 대체하려 할 때
--testdox
를 옵션으로 주었을때, 메소드의 이름으로부터 만들어진 설명을 오버라이딩 할 수 있습니다.
클래스 또는 메소드의 설명을 더 명확히 만들어 agile document를 만들 수 있습니다.
주의할 점으로는 PHPUnit v7.0까지는 어노테이션 파싱 오류로, @test로 인식되어 동작합니다.
@testWith
주석을 이용해 파라미터를 주입하고 싶을때
@dataProvider
는 호출될 메소드를 필요로 하지만, 주석만을 이용해 테스트하고자 할 때에는
@testWith
를 사용할 수 있습니다.
JSON 포맷은 연관배열로 주입됩니다.
주의 할 점은 여러개의 dataset을 정의할 때에는 라인당 하나씩 지정해야합니다.
아래의 두개의 코드는 동일하게 동작합니다.
/**
* @param string $input
* @param int $expectedLength
*
* @testWith ["test", 4]
* ["longer-string", 13]
*/
public function testStringLength(string $input, int $expectedLength)
{
$this->assertSame($expectedLength, strlen($input));
}
/**
* @param string $input
* @param int $expectedLength
*
* @dataProvider additionProvider
*/
public function testStringLengthWithDataProvider(string $input, int $expectedLength)
{
$this->assertSame($expectedLength, strlen($input));
}
public function additionProvider()
{
return [
["test", 4],
["longer-string", 13]
];
}
/**
* @param array $array
* @param array $keys
*
* @testWith [{"day": "monday", "conditions": "sunny"}, ["day", "conditions"]]
*/
public function testArrayKeys($array, $keys)
{
$this->assertSame($keys, array_keys($array));
}
/**
* @param array $array
* @param array $keys
*
* @dataProvider additionProvider
*/
public function testArrayKeysWithDataProvider($array, $keys)
{
$this->assertSame($keys, array_keys($array));
}
public function additionProvider()
{
return [
[["day" => "monday", "conditions" => "sunny"], ["day", "conditions"]]
];
}
```
@ticket
Ticket ID(JIRA 이슈 코드와 같은)로 테스트를 필터링 할 때
@group
어노테이션의 별칭. ticket ID를 이용하여 테스트를 필터링 할 수 있도록 하여줍니다.
@uses
테스트에 의해 실행될 코드를 지정합니다.
좋은 예제는 아래와 같이 유닛 테스트 코드에 필요한 Object 값 입니다.
해당 어노테이션에는 정규화 된 클래스명을 사용해야하기때문에,
모호하지 않도록 클래스명 맨 앞에 \
로 시작하는것을 추천합니다.