【WIP】PHPUnit로 나 취향의 어서션을 더해 「읽기 쉬운 쓰기 쉬운」테스트를

12891 단어 PHPUnit
@todo 제대로 동작 검증
@todo 각 클래스의 상세·역할 분담에 대해서 쓴다

현재 업무로 API 애플리케이션을 개발하고 있습니다.
API라는 것으로, 「에러의 형식이 정해져」 있습니다.
그 때, _view에 들어 있는 변수를 취해 그 안의 $codeがXXX $sub_codeがYYY $messageがZZZ 세 가지? 그게・・・
별로 온화하지 않다고 느낀 것입니다.
※실제로는 좀 더 복잡. 끝이 접혀 있습니다만, 대체로 이런 이미지입니다.

「같은 처리라면」 「같은 쓰는 방법을」 「게다가 가능한 한 간결·일목요연하게」하는 것이 아름답지 않습니까?
그렇다고 해서, 「에러시의 응답의 어설션」을 둥글게 정리해 버리면 편하지 않습니까! ! 라고 생각한 것입니다.

만들고 싶은 것



예를 들면, Twitter의 REST API의 응답의 이미지로, 그 내용을 검사하는 물건! 를 만들어 보자. 라고 하는 상정으로 추천합니다.
{
    "errors":   [
        {"message":"Sorry, that page does not exist","code":34},
        {"message":"User not found.","code":50},
        {"message":"Sorry, you are not authorized to see this status","code": 179}
    ]
}

※어디까지나 「예」예요! 실제로는 이것들의 에러가, 하물며 동시에 복수개 돌아올까? 라든지 돌진하는 것은 야생입니다.
cf : Error Codes & Responses — Twitter Developers

이것에 대해,
$this->assertErrorCode(34, $errors, '存在しないはずのページの状態を検知できていない');
$this->assertErrorCode(50, $errors, '存在しないはずのユーザーの状態を検知できていない');
$this->assertErrorCode(179, $errors, 'アクセス資格のないリソースにアクセスできてしまっている');

라든지 할 수 있으면 = 응답을 일일이 디코드해 key/value 체크를 해···라고 쓰는 것보다 슛으로 하기 때문에 「테스트하고 싶은 내용을, 코드에 명료하게 떨어뜨릴 수 있다」입니다.

구현 개요 (PHPUnit API)



공식 doc는 다음과 같습니다. 확실히 언급되었습니다.
-> 맞춤 어설션 만들기
과연, 아무래도 Constraint matcherTestCase のサブクラス 라는 것이 키워드가 될 것 같네요.
··라고는 해도, 개인적으로는 “조금 달리기 기분” 혹은 “약간 오라 붙었다” 기술로는···라고 하는 인상을 받았습니다.
구체적인 예를 원합니다. 원시 코드를 살펴 보겠습니다.

  • Constraint
  • 어설 션의 논리는 각각 독립적 인 클래스로 정의됩니다.


  • assertXxx() 엔티티
  • 이것이 역할적으로 「TestCase의 서브 클래스」의 대신을 하고 있는 이미지가 됩니다.


  • 오마케:
    나는 CakePHP를 평상시 사용하고 있기 때문에 Cake에서의 구현 예도 함께 소개해 둡니다.
  • class EventFired extends PHPUnit_Framework_Constraint
  • assertEventFired()

  • 구현해 보자.



    인터페이스 및 로직



    어떤 것이 있으면 좋을까요?
    "검사 대상 (json 문자열)"에 대해 "code 어때요?
    어리석게 쓰면 이런 느낌으로.
    $data = json_decode($json);
    if (! $json) {
      return false;
    }
    if (! (property_exists($json, 'code') && property_exists($json, 'message'))) {
      return false;
    }
    
    return ($json->code === $expected_code, && $json->message === $expected_message);
    

    이것을 「로직」으로서 어설션 메소드의 로직은 constraint에 짜넣는다고 하는 법에 따라 클래스를 정의해 보겠습니다.
    구체적으로는 ( message 와) __construct() matches() 라는 public 메소드를 구현해 두면 같다.
    class ApiResponse extends PHPUnit_Framework_Constraint
    {
        // 検査対象となる内容を格納する
        private $data;
    
        // expectedとの比較を行う前のフェーズで「失敗」させる事も可能
        public function __constuct(string $json)
        {
            $data = json_decode($json);
            if (! $data) {
                // FailedErrorを投げることで、任意のタイミングでアサーションを失敗させられる
                throw new PHPUnit_Framework_AssertionFailedError('Failed to parse json.');
            }
            if (! (is_object($json) && property_exists($json, 'code') && property_exists($json, 'message')) {
                return false;
            }
    
            $this->data = $data;
        }
    
        public function matches($expected)
        {
            // 失敗ならfalse、成功ならtrueを返すだけでOK
            return (
                $this->data->code === $expected['code']) &&
                $this->data->message = $expected['message']
            );
        }
    
        public toString()
        {
            return sprintf('%s has not valid code and message.', json_encode($this->data));
        }
    }
    

    로직은 가능했습니다. 그리고는 「호출원」입니다.
    이것이 「TestCase의 서브 클래스」라는 것이 됩니다.
    class AssertApiResponse extends  PHPUnit_Framework_TestCase
    {
        public function assertApiResponse($code, $messsage, $content, $message = '')
        {
            $this->assertThat(compact('code', 'message'), new ApiResponse($content), $message);
        }
    }
    

    그리고는, 이런 형태로 취득한 응답에 대한 어설션이 가능하게 됩니다.
    $error = $response['errors'][0];
    $this->assertApiResponse(34, 'Sorry, that page does not exist', $error, '存在しないはずのページの状態を検知できていない');
    

    좋은 웹페이지 즐겨찾기