skip to content

With PHPUnit, what is the difference between assertSame and assertEquals, and when does choosing the wrong one hide a bug?

level: juniorimportance: must knowfreq 75%

answer

  1. identity versus equality
  2. === with type and order
  3. 90 equals '90'
  4. two instances, same properties
  5. floats: assertEqualsWithDelta

basics

~20 s

assertSame checks identity with ===: same type and value, and for objects the same instance. assertEquals checks loose equality: 90 equals '90', and separate objects with equal properties are equal. Loose checks can pass when the type is wrong.

solid answer

~40 s

`assertSame($expected, $actual)` uses `===`. Scalars must match in type and value, arrays must hold the same keys and values in the same order with the same types, and objects must be the **same instance**. `assertEquals($expected, $actual)` uses PHPUnit's comparator, which is loose: `90` equals `'90'`, `1` equals `1.0`, associative arrays with the same pairs in a different order are equal, and two distinct objects of the same class are equal when their properties are equal. For scalars and arrays of scalars, prefer `assertSame`. With `assertEquals`, a method that returns the string `'90'` instead of the integer `90` still passes, and a `strict_types` caller breaks later. Use `assertEquals` deliberately for value objects compared by state, and `assertEqualsWithDelta` for floats.

code

php · 21 lines
php
<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class DiscountCalculatorTest extends TestCase
{
    public function testTenPercentOffHundred(): void
    {
        $calculator = new DiscountCalculator();

        // passes even if apply() wrongly returned '90'
        $this->assertEquals(90, $calculator->apply(100, 10));

        // fails unless apply() returns the int 90
        $this->assertSame(90, $calculator->apply(100, 10));

        // value objects: equal state, different instances
        $this->assertEquals(new Discount(10), $calculator->discountFor('SPRING10'));
    }
}

go deeper

for a junior

Know that assertSame is === and assertEquals is loose, with expected first and actual second, and default to assertSame for scalars.

for a middle

Explain the edge cases: numeric strings, int versus float, array key order, object identity versus equal state, and floats needing a delta.

for a senior

Choose assertions that pin the contract, strict types included, and use assertObjectEquals or assertEqualsCanonicalizing where they state intent better.

for a principal

Set team conventions on assertion strictness, so that tests catch type regressions instead of passing on anything that compares loosely.

## Two different questions PHPUnit offers two basic comparison assertions, and they answer different questions: - **`assertSame($expected, $actual)`**: *is it exactly this value?* It uses PHP's identity operator `===`. - **`assertEquals($expected, $actual)`**: *is it equal to this value?* It hands the pair to PHPUnit's comparator library, which compares loosely and recursively. Both take the **expected** value first and the **actual** value second. Swapping them still passes or fails the same way, but the failure message then labels the two sides backwards. ## What each one accepts Take a `DiscountCalculator` whose `apply(int $cents, int $percent)` should return an `int`: | Comparison | `assertSame` | `assertEquals` | |---|---|---| | `90` vs `'90'` | fails: int vs string | passes | | `1` vs `1.0` | fails: int vs float | passes | | `['a' => 1, 'b' => 2]` vs `['b' => 2, 'a' => 1]` | fails: different order | passes | | `[1]` vs `['1']` | fails | passes | | two `new Discount(10)` objects | fails: different instances | passes: same class, equal properties | | the same object twice | passes | passes | | `0.1 + 0.2` vs `0.3` | fails | fails; use `assertEqualsWithDelta` | Some consequences: 1. **Type bugs slip through `assertEquals`.** If `apply()` accidentally returns `'90'` (say it came from string formatting), `assertEquals(90, ...)` passes, while `assertSame(90, ...)` fails with a message that the string is not identical to the integer. 2. **Order matters to `assertSame` for arrays.** `===` on arrays requires the same key/value pairs **in the same order** and of the same types. If a method returns a map and the order is part of its contract, `assertSame` checks that too; if order is irrelevant, `assertEquals` expresses that. 3. **Objects:** `assertSame` asks *is this the same object?* That is right for "the repository returns the instance it cached", and wrong for "the calculator returns a discount of 10". Value objects compared by state need `assertEquals`, or a domain `equals()` method checked with `assertObjectEquals($expected, $actual)`. 4. **Floats never compare reliably with either.** Binary floating point makes `0.1 + 0.2` differ from `0.3` in the last bits, and `assertEquals` does not add a tolerance. `assertEqualsWithDelta(0.3, $actual, 0.0001)` states the tolerance explicitly. Money is better kept in integer cents in the first place. ## Objects under assertEquals For two objects, `assertEquals` does more than a shallow check: - it compares **every property**, including `private` and `protected` ones, not just the public API; - it **recurses** into nested objects and arrays, and copes with cycles (an author whose books point back at the author); - the **loose** rules apply at every level, so a `Discount` whose `$percent` holds `'10'` equals one holding `10`. That makes `assertEquals` convenient for value objects, and also a little dangerous. A lazily filled cache property or a timestamp set in the constructor makes two otherwise equal objects differ, and a type bug inside a property is hidden. When a class defines what equality means, for example an `equals(self $other): bool` method, `assertObjectEquals()` uses that definition. It requires the method to declare exactly one parameter and a `bool` return type. ## A practical rule - **Scalars, and arrays of scalars:** `assertSame`. It is the stricter check, and a strict test is what catches type regressions in a codebase that relies on `declare(strict_types=1)`. - **Value objects and nested structures compared by content:** `assertEquals`, knowing that it compares properties loosely and recursively. - **Identity itself, such as a cache or a singleton:** `assertSame` on objects. - **Order-insensitive lists:** `assertEqualsCanonicalizing`, which sorts both sides before comparing, instead of hand-sorting in the test. ## Reading the failure Both assertions print a diff of expected against actual. `assertSame` fails with *... is identical to ...*, `assertEquals` with *... is equal to ...*. When `assertSame` fails on values that look the same in the diff, the difference is almost always the **type** (`90` vs `'90'`, `1` vs `1.0`), which is exactly the information `assertEquals` would have thrown away. Assertions are static methods on `PHPUnit\Framework\Assert`, which `TestCase` extends, so `$this->assertSame(...)` and `self::assertSame(...)` call the same method. Teams pick one style and let a code-style rule enforce it.

  • Why does assertEquals(0.3, 0.1 + 0.2) fail?
    Binary floating point cannot represent 0.1, 0.2 or 0.3 exactly, so the sum differs from 0.3 in the last bits, and PHPUnit's equality comparison adds no tolerance. `assertEqualsWithDelta(0.3, 0.1 + 0.2, 0.0001)` states an explicit tolerance. For money, integer cents avoid the problem entirely.
  • How do you compare two value objects that define their own equals() method?
    Use `assertObjectEquals($expected, $actual)`. It calls `$actual->equals($expected)` (the method name can be passed as a third argument) and passes when that returns `true`. This respects the domain's own definition of equality instead of PHPUnit's property-by-property comparison.

assertSame asks whether this is the very key you lent out; assertEquals asks whether it opens the same lock.

saying these in an interview costs you the question

  • assertEquals and assertSame differ only in their failure messages.
  • assertSame compares two objects property by property.
  • assertEquals fails when an int is compared with a numeric string.
  • assertSame ignores key order when comparing arrays.
  • assertEquals compares floats with a built-in tolerance.