skip to content

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

level: juniorimportance: should knowfreq 35%

answer

  1. failure message says what was expected
  2. Countable or iterable
  3. a Generator is rejected
  4. class-string first, then the value
  5. unknown class throws, never passes

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.

solid answer

~40 s

`assertCount(int $expectedCount, Countable|iterable $haystack)` counts arrays and `Countable` objects with `count()` and walks other iterators, restoring their position afterwards. It rejects a `Generator` with a `GeneratorNotSupportedException`, because counting would consume it. `assertInstanceOf(string $expected, mixed $actual)` takes a class or interface name first and checks `instanceof`. If the name does not exist, for example a typo or a missing `use`, it throws `UnknownClassOrInterfaceException` instead of quietly failing. The reason to prefer both over `assertTrue(count($codes) === 3)` or `assertTrue($x instanceof Discount)` is the failure message: they say *actual size 2 matches expected size 3* or name the actual type, where `assertTrue` only reports that false is not true. Static analysers also narrow the type after `assertInstanceOf`.

code

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

use PHPUnit\Framework\TestCase;

final class DiscountCalculatorTest extends TestCase
{
    public function testStackedCodesProduceTwoDiscounts(): void
    {
        $calculator = new DiscountCalculator();
        $calculator->addCode('SPRING10');
        $calculator->addCode('LOYAL5');

        $discounts = $calculator->discounts();

        $this->assertCount(2, $discounts);
        $this->assertInstanceOf(PercentageDiscount::class, $discounts[0]);

        // on failure only says: false is true
        // $this->assertTrue(count($discounts) === 2);
    }
}

go deeper

for a junior

Know assertCount(expected, collection) and assertInstanceOf(Class::class, value), and why they beat assertTrue in a failure report.

for a middle

Explain what assertCount accepts, why Generators are rejected, instanceof semantics, and the unknown-class exception.

for a senior

Choose the assertion that states the intent and gives the best diff, such as assertSame on a whole collection instead of a count plus element checks.

for a principal

Make readable failures a review criterion, since test reports are read far more often than tests are written.

## Two assertions that say what went wrong A test is only as useful as its failure message. Writing every check as `assertTrue(<expression>)` works, but when it fails the report reads *Failed asserting that false is true*, and the reader has to open the test to learn what was expected. PHPUnit's specific assertions carry the expected and actual values into the message. ## assertCount ```php final public static function assertCount(int $expectedCount, Countable|iterable $haystack, string $message = ''): void ``` - **Arrays and `Countable` objects** are counted with `count()`. - **Other `Traversable` objects** are unwrapped through `IteratorAggregate::getIterator()` and counted by iterating. For an `Iterator`, PHPUnit remembers the current key and moves back to it afterwards, so the assertion does not leave the iterator exhausted. - **A `Generator` is rejected** with `GeneratorNotSupportedException`: a generator cannot be rewound once iterated, so counting it would consume the values the test may still need. Convert it with `iterator_to_array()` first, deliberately. - The expected count comes **first**. `assertCount(count($codes), 3)` has the arguments swapped and fails with a type error, because the second parameter must be countable. For the discount scenario, `assertCount(2, $calculator->appliedCodes())` fails with a message comparing the actual size to 2, instead of *false is true*. Related assertions: `assertEmpty()`/`assertNotEmpty()` for zero elements, and `assertSameSize($expected, $actual)` to compare two collections' sizes. ## assertInstanceOf ```php final public static function assertInstanceOf(string $expected, mixed $actual, string $message = ''): void ``` - `$expected` is a **class or interface name**, usually written `Discount::class`. - The check is `instanceof`, so subclasses and implementations match. `assertInstanceOf(DiscountInterface::class, $d)` passes for any implementation. - If the name does not exist, PHPUnit throws **`UnknownClassOrInterfaceException`**. That catches the classic bug where a missing `use` statement turns `Discount::class` into a string naming a class in the test's own namespace, which `instanceof` would never match. - The assertion carries a PHPStan-style assertion annotation, so static analysers treat `$actual` as that type afterwards and allow method calls on it without further checks. ## When to prefer something else | Intent | Better assertion | |---|---| | the exact type of a scalar | `assertIsInt`, `assertIsString`, `assertIsList` | | the exact object | `assertSame($expected, $actual)` | | equal contents of a collection | `assertSame` or `assertEquals` on the whole array, which also checks the count | | a collection is empty | `assertEmpty` or `assertCount(0, ...)` | A frequent over-test is `assertCount(3, $codes)` followed by three separate element checks, when `assertSame(['A', 'B', 'C'], $codes)` states all of it in one line with a full diff on failure. ## Other targeted assertions in the same spirit | Instead of | Prefer | |---|---| | `assertTrue(isset($a['code']))` | `assertArrayHasKey('code', $a)` | | `assertTrue(in_array($d, $list, true))` | `assertContains($d, $list)` | | `assertTrue(str_contains($msg, 'off'))` | `assertStringContainsString('off', $msg)` | | a loop of `assertInstanceOf` calls | `assertContainsOnlyInstancesOf(Discount::class, $list)` | | `assertTrue(array_is_list($a))` | `assertIsList($a)` | Each of these names its subject in the failure message, which a bare boolean cannot do. `assertContains()` uses identity (`===`) for each element. `assertContainsEquals()` is the loose variant, with the same trade-off as `assertSame` against `assertEquals`. ## Common mistakes 1. **Swapped arguments** (`assertInstanceOf($obj, Discount::class)`): the first parameter must be a class-string, so this fails loudly rather than passing. 2. **Counting a generator** straight from a method that `yield`s: an exception, by design. 3. **`assertTrue(count($x) == 3)`**: correct, but useless when it fails. 4. **Checking type where behaviour matters**: `assertInstanceOf` proves the class, not that the object is in the right state. Follow it with assertions on the values.

  • Why does assertCount refuse a Generator?
    Counting a generator means iterating it to the end, and a generator cannot be rewound afterwards, so the assertion would consume the values the test might still need. PHPUnit throws `GeneratorNotSupportedException` instead. If counting it is really what you want, convert it explicitly with `iterator_to_array()` and assert on the array.
  • What happens with assertInstanceOf when a use statement is missing?
    `Discount::class` then resolves to a name in the test's own namespace, which does not exist. `assertInstanceOf` checks that the class or interface exists and throws `UnknownClassOrInterfaceException`, so the mistake surfaces immediately instead of looking like a failed type check.

saying these in an interview costs you the question

  • assertTrue(count($x) === 3) gives the same failure message.
  • assertCount accepts any iterable, generators included.
  • assertInstanceOf takes the object first, then the class name.
  • assertInstanceOf with a misspelled class name just fails normally.
  • assertInstanceOf matches only the exact class, not subclasses.