Contents

PHPUnit Annotation 정리

Contents

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


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


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

각 테스트가 끝난 뒤 트랜잭션 커밋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
```

모든 테스트가 끝난 후, 공유된 자원들을 정리하기 위해 호출할 정적 메소드를 지정 할 수 있습니다.

해당 부분에서 테스트 실행시 만든 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
```

모든 글로벌 변수를 각 테스트 전에 백업하고, 각 테스트 이후 해당 백업을 복원시킵니다.

메소드 레벨에서 재정의가 가능합니다.

해당 설명만으로는 이해가 잘 되지 않아서 직접 예제 코드를 만들어 보았습니다. 클래스 스코프 밖에 정의된 글로벌 변수인 $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");
    }
}
```

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

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

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

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

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

아래와 같이 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
```

해당 클래스에서 테스트가 실행되기전 공유 하기 위한 정보를 설정하기 위해 호출 할 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
 */
class Foo
{
    public function bar()
    {
    }
}

class Bar
{
    /**
     * @codeCoverageIgnore
     */
    public function foo()
    {
    }
}

if (false) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}

exit; // @codeCoverageIgnore
```

어떤 영역을 테스트 하고자 하는지 명시하고자 할 때 사용합니다.

이와 같이 명시 하면 IDE(PHPStorm)에서 연결되어 있어 ctrl+shift+T 를 이용해 테스트로 바로 이동이 가능해지고, usage로 찾을 수 있어 메소드명 수정시 같이 반영됩니다.


기본 네임스페이스나 클래스명을 명시하는데 사용할 수 있어, @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();
    }
}
```

클래스나 메소드레벨에서 사용할 수 있고 @covers 어노테이션을 덮어 씁니다.


@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 clone 를 이용하고, PHP에서 clone으로 불리는 얕은 복사를 원할 경우 @depends shallowClone 를 이용하면 됩니다.


아래와 같이 테스트를 수행하지 않을 경우 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어노테이션을 이용하여, 테스트 코드에 1개 이상의 태그와 같이 묶음 필터를 추가할 수 있습니다.

XML 설정 파일 주입을 이용 하거나 CLI에서 실행시 --group--exclude-group 를 이용해서 테스트 실행 대상 또는 제외그룹을 설정할 수 있습니다.


@group large의 별칭으로,PHP_Invoker 패키지가 설치되어 있고, strict mode가 실행되어 있으면 60초 이상 실행 될 경우 실패 처리됩니다. 해당 타임아웃에 관한 정보는 설정 정보 XML의 timeoutForLargeTests속성을 통해 설정 할 수 있습니다.


@group medium의 별칭으로, PHP_Invoker 패키지가 설치되어 있고, strict mode가 실행되어 있으면 10초 이상 실행 될 경우 실패 처리됩니다. 해당 타임아웃에 관한 정보는 설정 정보 XML의 timeoutForMediumTests속성을 통해 설정 할 수 있습니다.

Medium 테스트는 @large 테스트에 의존적이여서는 안됩니다.


테스트가 별도의 프로세스에서 실행될 때, PHPUnit은부모 프로세스에서 글로벌 state를 직렬화 한 값을 자식 프로세스에서 역직렬화하여 상태를 보존합니다.

부모 프로세스에서 직렬화 할 수 없는 글로벌 state가 있는 경우, 해당 옵션을 disable 처리하여 방지할 수 있습니다.


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 설치 여부 및 버전 체크
/**
 * @requires extension mysqli
 */
class DatabaseTest extends TestCase
{
    /**
     * @requires PHP >= 5.3
     */
    public function testConnection()
    {
                // 해당 테스트는 mysqli 확장프로그램이 설치되어 있고, PHP 버전이 5.3 이상일때 실행됩니다.
    }

    // 추가적인 테스트를 작성하였을때에도 mysqli 확장프로그램이 필요합니다.
}
```

해당 테스트 클래스 내의 모든 테스트 메소드들이 별도의 PHP 프로세스에서 실행되어야 함을 표시 할 때 사용합니다.

PHPUnit은 직렬화를 통해 Global state를 유지하려 하기 때문에, 직렬화가 불가능한 부분은 @preserveGlobalState를 참조하세요.


해당 테스트 메소드가 별도의 PHP 프로세스에서 실행되어야 함을 표시 할 때 사용합니다.

PHPUnit은 직렬화를 통해 Global state를 유지하려 하기 때문에, 직렬화가 불가능한 부분은 @preserveGlobalState를 참조하세요.


@group small의 별칭으로, PHP_Invoker 패키지가 설치되어 있고, strict mode가 실행되어 있으면 1초 이상 실행 될 경우 실패 처리됩니다. 해당 타임아웃에 관한 정보는 설정 정보 XML의 timeoutForSmallTests속성을 통해 설정 할 수 있습니다.

Medium 테스트는 @large@medium로 마킹된 테스트에 의존적이여서는 안됩니다.

테스트의 실행 시간 제어를 하고자 할 때, @small, @medium, @large 와 같은 어노테이션을 명시적으로 사용해야합니다.


테스트 메소드는 메소드명의 prefix로 test를 사용합니다.

테스트 메소드명의 prefix로 test를 사용하지 않는 대안으로, 주석에 @test 어노테이션을 사용하면 테스트 메소드라고 인식됩니다.

/images/phpunit-annotations/eeb49a85-fdd7-40ee-99df-60579993b1a3.png

/images/phpunit-annotations/cbdae160-7e46-49e4-9d93-0a3584597625.png


--testdox를 옵션으로 주었을때, 메소드의 이름으로부터 만들어진 설명을 오버라이딩 할 수 있습니다.

클래스 또는 메소드의 설명을 더 명확히 만들어 agile document를 만들 수 있습니다.

주의할 점으로는 PHPUnit v7.0까지는 어노테이션 파싱 오류로, @test로 인식되어 동작합니다.


@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"]]
        ];
}
```

@group 어노테이션의 별칭. ticket ID를 이용하여 테스트를 필터링 할 수 있도록 하여줍니다.


테스트에 의해 실행될 코드를 지정합니다.

좋은 예제는 아래와 같이 유닛 테스트 코드에 필요한 Object 값 입니다.

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