skip to content

Test Cases & Assertions

A PHPUnit test is a TestCase subclass whose methods call assertSame, assertEquals or expectException, with setUp run fresh before each. Interviewers probe identity vs equality and isolation.

on this pageshow

explore

questions

5

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.
open as a page

In PHPUnit 13, how do you assert that code throws an exception, and what are the traps of expectException?

level: middleimportance: must knowfreq 60%

basics

~10 s

Call expectException(SomeException::class) before the code that should throw. The test passes only if a matching Throwable escapes; nothing after the throwing line runs. Message checks use expectExceptionMessageIs, expectExceptionMessageIsOrContains or expectExceptionMessageMatches.

open as a page

With PHPUnit, what do assertCount and assertInstanceOf check, and why prefer them over assertTrue with count() or instanceof?

level: juniorimportance: should knowfreq 35%

basics

~10 s

assertCount checks the number of elements in a Countable or iterable, and assertInstanceOf checks a value's class or interface. Both report expected against actual on failure, where assertTrue only says false is not true.

open as a page

In PHPUnit 13, how does a TestCase subclass declare its tests, and when do setUp and tearDown run?

level: juniorimportance: should knowfreq 58%

basics

~20 s

A test class extends PHPUnit\Framework\TestCase; each public method whose name starts with test, or that carries #[Test], is a test. PHPUnit creates a fresh instance per test, calling setUp before and tearDown after each test.

open as a page

A PHPUnit test passes when run alone but fails when its whole class runs; how can setUpBeforeClass or static properties cause that?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Instance properties are rebuilt for every test, but static properties and fixtures created in setUpBeforeClass are shared by all tests in the class. An earlier test that mutates them changes what a later test sees.

open as a page