With PHPUnit, what is the difference between assertSame and assertEquals, and when does choosing the wrong one hide a bug?
answer
- identity versus equality
- === with type and order
- 90 equals '90'
- two instances, same properties
- floats: assertEqualsWithDelta
basics
~20 sassertSame 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
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
Know that assertSame is === and assertEquals is loose, with expected first and actual second, and default to assertSame for scalars.
Explain the edge cases: numeric strings, int versus float, array key order, object identity versus equal state, and floats needing a delta.
Choose assertions that pin the contract, strict types included, and use assertObjectEquals or assertEqualsCanonicalizing where they state intent better.
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.