9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPUnitのassertEquals()は==と同じではない

Posted at

要約

  • PHPUnit の assertEquals() は、比較演算子== と完全に同じ動作をするわけではない
  • 特に理由がない場合は assertEquals() ではなく assertSame() を使うべきである

はじめに

比較演算子

PHP には、値が等しいことを比較する 比較演算子 として ===== があります。前者が型の相互変換を行ってから比較する「緩やかな比較」なのに対して、後者は相互変換を行わずに比較する「厳密な比較」であるという特徴があります。

詳しくは 型の比較表 を参照していただきたいと思いますが、例えば以下のようになります。1

// 整数値を文字列と比較した場合
var_dump("1" == 1);         // => bool(true)
var_dump("10test" == 10);   // => bool(true)

// 空文字、数値0、論理値FALSE、NULLを比較した場合
var_dump("" == 0);          // => bool(true)
var_dump("" == FALSE);      // => bool(true)
var_dump("" == NULL);       // => bool(true)
var_dump(0 == FALSE);       // => bool(true)
var_dump(0 == NULL);        // => bool(true)
var_dump(FALSE == NULL);    // => bool(true)

// 文字列を数値0と比較した場合
// 1. 空文字は数値0と等しく、論理値FALSEと等しい
var_dump("" == 0);          // => bool(true)
var_dump("" == FALSE);      // => bool(true)
// 2. 文字列"0"は数値0と等しく、論理値FALSEと等しい
var_dump("0" == 0);          // => bool(true)
var_dump("0" == FALSE);      // => bool(true)
// 3. 文字列"test"も数値0と等しいが、論理値TRUEと等しい
// (数値0自体は論理値FALSEと等しいのだが・・・)
var_dump("test" == 0);      // => bool(true)
var_dump("test" == TRUE);   // => bool(true)

assertEquals() に関する噂

PHPUnit のアサーションメソッド assertEquals() は、しばしば比較演算子 == と同じ動作をするかのように解説されています。

でも、本当にそうなのか?というのが、この記事の趣旨です2

assertEquals()

比較表

PHPマニュアルの型比較表 に掲載されている表「==による緩やかな比較」3に、assertEquals() の結果を追記しました(赤字 部分)。また、(P1) (P2) はそれぞれ後述の「パターン1」「パターン2」に該当することを示しています。

TRUE FALSE 1 0 "1" "0" NULL array() "php" ""
TRUE TRUE FALSE TRUE FALSE TRUE FALSE FALSE FALSE TRUE => FALSE (P1) FALSE
FALSE FALSE TRUE FALSE TRUE FALSE TRUE => FALSE (P1) TRUE TRUE => FALSE (P2) FALSE TRUE
1 TRUE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
0 FALSE TRUE FALSE TRUE FALSE TRUE TRUE FALSE TRUE => FALSE (P1) TRUE => FALSE (P1)
"1" TRUE FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
"0" FALSE TRUE => FALSE (P1) FALSE TRUE FALSE TRUE FALSE FALSE FALSE FALSE
NULL FALSE TRUE FALSE TRUE FALSE FALSE TRUE TRUE => FALSE (P2) FALSE TRUE
array() FALSE TRUE => FALSE (P2) FALSE FALSE FALSE FALSE TRUE => FALSE (P2) TRUE FALSE FALSE
"php" TRUE => FALSE (P1) FALSE FALSE TRUE => FALSE (P1) FALSE FALSE FALSE FALSE TRUE FALSE
"" FALSE TRUE FALSE TRUE => FALSE (P1) FALSE FALSE TRUE FALSE FALSE TRUE

差分解説

上記「比較表」を見ると、本来 == 演算子では等しい(TRUE)のに、assertEquals() では等しくならない(FALSE)パターンがいくつか存在します。

以下、どうしてそうなるのかを PHPUnit のソースコードに即して見ていきましょう。

パターン1

比較対象が スカラー型booleanintegerfloatdoublestring)または NULL の場合、PHPUnit では ScalarComparator で比較処理が行われます。

重要なのは、比較対象いずれかに string 型である場合、比較する両方の値を string にキャストしているということです。

comparator/ScalarComparator.php
// always compare as strings to avoid strange behaviour
// otherwise 0 == 'Foobar'
if (\is_string($expected) || \is_string($actual)) {
  $expectedToCompare = (string) $expectedToCompare;
  $actualToCompare = (string) $actualToCompare;

  if ($ignoreCase) {
    $expectedToCompare = \strtolower($expectedToCompare);
    $actualToCompare = \strtolower($actualToCompare);
  }
}

この結果、"php" == TRUE (== 演算子で比較した場合は TRUE になるパターン)については以下のように比較されることになります。

  1. もう一方の比較対象が string 型であるため、boolean 型の TRUEstring"TRUE" にキャストされる。
  2. 実際に比較する際は "php" == "TRUE" として比較される。
  3. 上記 2. の比較結果は FALSE になる。よって、assertEquals() では FALSE になる。

なお、boolean 型を string 型に変換する際は以下のようになるので注意しましょう(PHP マニュアル)。4

booleanTRUE は文字列の "1" に、 FALSE"" (空文字列) に変換されます。 これにより boolean と文字列の値を相互に変換することができます。

この結果、"0" == FALSE"0" == "" として比較されるようになります。

パターン2

PHPUnit の ソースコード を見ると、assertEquals() には複数の比較クラスが用意されていることが分かります。

comparator/Factory.php
$this->registerDefaultComparator(new MockObjectComparator);
$this->registerDefaultComparator(new DateTimeComparator);
$this->registerDefaultComparator(new DOMNodeComparator);
$this->registerDefaultComparator(new SplObjectStorageComparator);
$this->registerDefaultComparator(new ExceptionComparator);
$this->registerDefaultComparator(new ObjectComparator);
$this->registerDefaultComparator(new ResourceComparator);
$this->registerDefaultComparator(new ArrayComparator);
$this->registerDefaultComparator(new DoubleComparator);
$this->registerDefaultComparator(new NumericComparator);
$this->registerDefaultComparator(new ScalarComparator);
$this->registerDefaultComparator(new TypeComparator);

比較する際、assertEquals() は、これらの比較クラスを順番に見ていき、使用可能なクラスを探します。そして、どの比較クラスも使えない場合は TypeComparator を無条件で使用します。

TypeComparator は型を比較するクラスです。具体的には、gettype 関数で取得した型名が一致しているかどうかでチェックします。

この結果、例えば NULL == array()== 演算子で比較した場合は TRUE になるパターン)については、以下のように比較されることになります。

  1. 比較対象の型名を取得する。gettype 関数によって、 NULL は "NULL"、array() は "array" がそれぞれ取得される。
  2. 上記1.で取得した型名によって比較する。"NULL" == "array"FALSE である。よって、assertEquals() では NULL == array() は FALSE になる。

assertSame()

assertSame() の結果は以下のとおりです。PHPマニュアルの型比較表 に掲載されている表「===による厳密な比較」と同一であることが分かります。

TRUE FALSE 1 0 "1" "0" NULL array() "php" ""
TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
1 FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
0 FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
"1" FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
"0" FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE
NULL FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE
array() FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE
"php" FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE
"" FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE

また、PHPUnit の ソースコード を見ても、内部では === 演算子を使っていることが分かります。

まとめ

  • PHPUnit の assertEquals() は、比較演算子== と完全に同じ動作をするわけではない
    • 差分を確認すると、いずれも == 演算子で TRUE になっている箇所を FALSE に より厳密に 評価していることがわかる。
    • ただし、assertEquals()ドキュメント には、どのような場合にどういった結果になるかという表は存在しないため、毎回自分で動きを確認する必要がある。
    • そもそも PHPUnit のドキュメントでは assertEquals()== 演算子と同一の動作をするという説明は一切ない
  • 特に理由がない場合は assertEquals() ではなく assertSame() を使うべきである
    • PHPUnit の assertSame()=== 演算子と同一の動作をする5== 演算子とも微妙に違った動作をする assertEquals() に比べれば、はるかに理解しやすい。

余談

  • PHP の == 演算子について詳しく調べたことなかったけど、思った以上にカオスだった。6
  • Java の JUnit などに比べると、PHPUnit のまともな説明が少なくないですか。そもそも皆さんちゃんと使っています?7
  1. それにしてもなんで "test" == 0 としてしまったのだろうか。恐らく条件文で使うことなどを考えて "test" == TRUE としたのだろう(これはまだ理解できる)。しかし、その結果、本来、0 == FALSE であるのに "test" == 0, "test" == TRUE という矛盾した動作をするようになった。そもそも "test" == 0 とするべきではなかったのではないか?

  2. あくまでも assertEquals()== 演算子の動作の違いを示すことが趣旨であって、assertEquals() の全仕様を解説するものではありません。

  3. 数値 -1 と文字列 "-1" のパターンは割愛しました。

  4. これも私たちの常識とはやや異なるように思う。 普通、TRUE が "1" になるなら、FALSE は "0" になるべきではないか。だが、PHP は無知蒙昧なる我々の常識をはるかに超えたところで動いているのだから、我々は黙ってそれに従うほかない。

  5. これも assertEquals() と同様だが、ドキュメント=== と同一の動作をするという説明は一切ない。あくまでも、この記事では、実際に試してみた結果と PHPUnit のソースコードからそう判断しているに過ぎない。

  6. ちなみに、最近まで仕事で PHP の仕事をしていたが、既存コードは全て == 演算子になっていて、=== 演算子は一切使用されていなかった。ああいう「とりあえず動けばいいや」的に作られたプロジェクトがどういう末路を辿るかは推して知るしかない。

  7. この記事を書いた人は PHPUnit を実務で使ったことがなく、今後のために勉強していたらドツボにはまった次第。

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?