DateTimeImmutable: diff를 반환하는 함수 테스트에서 new DateInterval의 경우 복사

21859 단어 PHPPHPUnittech

DateTimeImmutable: diff의 함수를 되돌려주는 테스트에서 new DateInterval이면 assertEqual이 복사되어 곤란합니다


날짜 간의 차이를 계산할 때DateTimeImmutable::diff()(또는DateTime::diff()를 사용하려고 합니다.
PHP: DateTime::diff - Manual
https://secure.php.net/manual/ja/datetime.diff.php
이 함수의 반환값은 DateInterval 형식으로 시간의 '간격' 을 나타내는 클래스입니다.이 클래스 자체는 매우 편리하지만 DateInterval형은 특유의 행위가 있어 반환 함수를 테스트하려면 문제가 발생할 수 있다.

예: 테스트할 함수


'마감' 을 가진 대상이 존재하고, 함수가 남은 시간을 되돌려줍니다.디자인이 상당히 적합하다.
<?php

final class ItemWithDeadline
{
    /** @var DateTimeImmutable */
    private $deadline;

    ......

    /**
     * @param DateTimeImmutable $now
     * @return DateInterval
     */
    public function getRemainingInterval(DateTimeImmutable $now)
    {
        return $now->diff($this->deadline);
    }
}
이에 대응하는 PHPUnit 테스트
final class GetRemainingIntervalTest extends TestCase
{
    public function test_未来の締切に対して正しい残り時間が返る ()
    {
        $now = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2017-12-04 00:00:00');
        $item = new ItemWithDeadline([
            'deadline' => new DateTimeImmutable('2017-12-05 00:00:00'),
            ......
        ]);

        $this->assertEquals(
            DateInterval::createFromDateString('1 day'),
            $item->getRemainingInterval($now)
        ); // fails!
    }

    public function test_過ぎた締切に対して正しい残り時間が返る ()
    {
        ......
    }
}
결론적으로 상술한 테스트test_未来の締切に対して正しい残り時間が返る는 실패했다.왜 그랬을까?

DateInterval#days 정보

DateInterval류에 days속성이 존재한다.
공식 매뉴얼DateInterval의 페이지에는 다음과 같다.
days
DateTime: diff()로 작성된 DateInterval 객체의 시작일과 끝일 사이의 일수입니다.이외의 경우에는 매일 FALSE입니다.
무슨 말을 하고 있는 기분이지만, 어쨌든 그런 게 존재한다.위의 테스트를 수행하면 expectedactual의 diff에 days가 나타난다는 것을 알 수 있다.PHPUnit의assertEquals는 서로 다른 대상을 인용해도 같은 값으로 판정할 수 있지만 각 속성의 내용은 시종 같은 값이어야 한다.
이런 상황을 피하는 방법은 매우 간단하다. 예를 들어 expected 명백한 diff 을 되돌려 주는 함수를 만들 수 있다.
<?php

private static function _get1DayInterval ()
{
    $sooner = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2000-01-01 00:00:00');
    $later = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2000-01-02 00:00:00');

    return $sooner->diff($later);
}

......

$this->assertEquals(
    self::_get1DayInterval(),
    $item->getRemainingInterval($now)
); // Pass!
축하합니다!잘 됐다.나 울 것 같애.

또 다른 함정


다만, 대상자 간 비교 외에 다른 방법도 검토할 수 있다는 의견도 있다.
예를 들어 규격상 하루 단위의 diff만 가능한 경우(절대 일주일이나 한 달도 안 되고 끝수도 발생하지 않는 hour 등)는 처음부터 비교DateInterval#d만 하는 방법이 있었다.
<?php

public function test_未来の締切に対して正しい残り時間が返る ()
{
    $now = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2017-12-04 00:00:00');
    $item = new ItemWithDeadline([
        'deadline' => new DateTimeImmutable('2017-12-05 00:00:00'),
        ......
    ]);

    $this->assertEquals(
        DateInterval::createFromDateString('1 day')->d,
        $item->getRemainingInterval($now)->d
    ); // Pass!
}
통과입니다.둘 다1라서 아무 문제 없어요.
근데 이거 함정이 있어.예를 들어 마감일이 지나면 테스트 사례를 추가하자.
 <?php

 public function test_過去の締切に対して正しい残り時間が返る ()
 {
-    $now = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2017-12-04 00:00:00');
+    $now = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2017-12-06 00:00:00');
     $item = new ItemWithDeadline([
         'deadline' => new DateTimeImmutable('2017-12-05 00:00:00'),
         ......
     ]);

     $this->assertEquals(
-        DateInterval::createFromDateString('1 day')->d,
+        DateInterval::createFromDateString('-1 day')->d,
         $item->getRemainingInterval($now)->d
     ); // fail!
 }
실패합니다.뭔데?

DateImmutable#invert 정보

DateImmutable에는 또 하나의 재미있는 속성invert이 있다.이것은 공식에서 다음과 같이 설명한다.
invert
간격이 음수일 때는 1이고 그렇지 않으면 0이다.DateInterval: format()를 참조하십시오.
https://secure.php.net/manual/ja/class.dateinterval.php
즉, 다음과 같이 어느 것이 다음 날짜인지 판단할 수 있다.편하네.
<?php

$interval = $today->diff($deadline);

if ($interval->invert == 1) {
    throw new Exception('締め切りを過ぎています');
}

new DateInverval의 invert 동작


그럼 이쪽invert에서 상당히 재미있는 행동을 하세요.방금 $today->diff($deadline)를 보고 얻은 기간invert1입니다.
하지만 같은'마이너스 1일'DateInterval을 직접 하면 어떨까.
$ php -a

php > var_dump(DateInterval::createFromDateString('-1 day'));
object(DateInterval)#4 (15) {
  ["y"]=>
  int(0)
  ["m"]=>
  int(0)
  ["d"]=>
  int(-1)
  ["h"]=>
  int(0)
  ["i"]=>
  int(0)
  ["s"]=>
  int(0)
  ["weekday"]=>
  int(0)
  ["weekday_behavior"]=>
  int(0)
  ["first_last_day_of"]=>
  int(0)
  ["invert"]=>
  int(0)
  ["days"]=>
  bool(false)
  ["special_type"]=>
  int(0)
  ["special_amount"]=>
  int(0)
  ["have_weekday_relative"]=>
  int(0)
  ["have_special_relative"]=>
  int(0)
}
invert0.대신 일수d-1를 표시했다.

회피책


방금days의 회피 전략으로 정해진 날짜의 차분을 취하여 그 결과를 되돌려 주는 함수를 만드는 방법을 소개했다.내가 여기서 방법을 생각해 볼게.
 <?php

-private static function _get1DayInterval ()
+private static function _get1DayInterval ($invert = false)
 {
+    // https://github.com/beberlei/assert
+    Assersion::boolean($invert);

     $sooner = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2000-01-01 00:00:00');
     $later = DateTimeImmutable::createFromFormat('Y-m-d h:i:s', '2000-01-02 00:00:00');

-    return $sooner->diff($later);
+    return $invert ? $later->diff($sooner) : $sooner->diff($later);
 }
이렇게 하면 음방향DateInterval도 테스트에 정확하게 사용할 수 있다.잘 됐다.

또는


포기하고 이렇게 쓰는 게 안전할 수도 있어요.
<?php

$this->assertEquals(0, $actual->y);
$this->assertEquals(0, $actual->m);
$this->assertEquals(1, $actual->d);
$this->assertEquals(0, $actual->h);
$this->assertEquals(0, $actual->i);
$this->assertEquals(0, $actual->s);

총결산

DateInterval 정말 엄격하니까 기억하는 게 좋아요.

좋은 웹페이지 즐겨찾기