PHPUnit Annotation 정리

서론


PHP Unit을 사용하면서도 잘 모르는 어노테이션을 PHPUnit v8.4 기준(2019-11-19 최신버전)으로 정리하였습니다.

@author

테스트를 작성자별 그룹화 필터링 할 때


@group 어노테이션의 별칭으로, 테스트를 작성자별로 그룹화하여 필터링 하는데 사용할 수 있습니다.

@after

각 테스트가 끝난 뒤 실행 하려 할 때


각 테스트 메소드들이 실행 된 후, 특정 메소드를 실행하고자 할때 사용할 수 있습니다.

각 테스트가 끝난 뒤 트랜잭션 커밋or롤백을 한다거나, 생성된 파일을 삭제하는 등의 처리를 하는데 사용하면 좋을 것 같습니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

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 커넥션을 회수하거나, 전체 트랜잭션을 처리 하거나, 소켓을 닫는 등의 처리를 할 수 있을것 같습니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

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”라는 값을 가지고 있게됩니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

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

정적 속성을 사용하려 할 때


선언된 클래스들 안의 모든 정적 속성을 각 테스트 전에 백업하고, 각 테스트 후에 해당 백업을 복원 시킵니다.

클래스 레벨에도 선언 가능하며, 각 테스트 메소드에서 추가 제어 가능합니다.

예제 코드 **아래 코드는 정상 동작 안 합니다.**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

use PHPUnit\Framework\TestCase;

/**
* @backupStaticAttributes enabled
*/
class MyTest extends TestCase
{
public function test_정적속성을_사용하는_테스트()
{
// ...
}

/**
* @backupStaticAttributes disabled
*/
public function test_정적속성을_사용하지_않는_테스트()
{
// ...
}
}

@before

각 테스트 실행전에 실행 하려 할 때


각 테스트 메소드가 호출되기 전에 실행할 메소드를 지정할 수 있습니다.

아래와 같이 beforeMethod는 각 메소드 호출전에 실행되지만,
users 배열의 값이 증가되지는 않습니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
use PHPUnit\Framework\TestCase;

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 메소드에 지정하여 사용할 수 있습니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
use PHPUnit\Framework\TestCase;

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*


코드 커버리지 분석시 제외할 라인에 사용할 수 있습니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
use PHPUnit\Framework\TestCase;

/**
* @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로 찾을 수 있어 메소드명 수정시 같이 반영됩니다.

예제 코드
1
2
3
4
5
6
7
/**
* @covers \App\Services\Member::getMember
*/
public function test_회원정보에_나이_정보가_있는지_체크()
{
$this->assertArrayHasKey('age', $this->member->getMember(1));
}

@coversDefaultClass

너무 긴 네임스페이스와 클래스명을 반복해서 쓰고 싶지 않을 때


기본 네임스페이스나 클래스명을 명시하는데 사용할 수 있어, @covers 어노테이션에 긴 네임스페이스나, 클래스명을 반복해서 사용할 필요가 없어집니다.

해당 어노테이션에는 정규화 된 클래스명을 사용해야하기때문에,
모호하지 않도록 클래스명 맨 앞에 \ 로 시작하는것을 추천합니다.

아래 예제 코드와 같이 @covers \Foo\CoveredClass::publicMethod@covers ::publicMethod로 줄여 쓸 수 있는 이점을 얻게 됩니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
use PHPUnit\Framework\TestCase;

/**
* @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 assertSame($expected, $a + $b); } public function additionProvider() { return [ [0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 3] ]; } }
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26

        - 아래와 같이 이름이 정의된 dataset을 사용할 수도 있습니다.
        - ```php
        <?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)로 성공 처리됩니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

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 설치 여부 및 버전 체크
예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
use PHPUnit\Framework\TestCase;

/**
* @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을 정의할 때에는 라인당 하나씩 지정해야합니다.

아래의 두개의 코드는 동일하게 동작합니다.

예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php

/**
* @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 값 입니다.

예제 코드
1
2
3
4
5
6
7
8
/**
* @covers \BankAccount
* @uses \Money
*/
public function testMoneyCanBeDepositedInAccount()
{
// ...
}

해당 어노테이션에는 정규화 된 클래스명을 사용해야하기때문에,
모호하지 않도록 클래스명 맨 앞에 \ 로 시작하는것을 추천합니다.

Author

Hodory

Posted on

2019-11-25

Updated on

2022-08-11

Licensed under

댓글