Introduction
Assertions are the verdict of every test. They compare an expected value against an actual value and either pass silently or fail with a descriptive message. PHPUnit ships with dozens of built-in assertion methods, and knowing which one to reach for makes your tests both clearer and more maintainable.
Key Concepts
- assertEquals: Compares two values with loose (
==) comparison, allowing type juggling. - assertSame: Compares two values with strict (
===) comparison, requiring both value and type to match. - assertTrue / assertFalse: Verify a value is exactly
trueorfalse. - assertNull: Confirms a value is
null. - assertCount: Checks the number of elements in a countable (array,
Countableobject). - assertInstanceOf: Verifies that an object is an instance of a given class or interface.
Real World Context
Choosing the wrong assertion leads to tests that pass when they should fail. For example, assertEquals(0, false) passes because 0 == false in PHP. Using assertSame(0, false) correctly catches the type mismatch. In payment or security code, this distinction can mean the difference between a passing test suite and a production bug.
Deep Dive
assertEquals vs assertSame
The most important distinction in PHPUnit assertions is loose vs strict comparison. Consider the following test:
php<?php $this->assertEquals(0, ''); // PASSES — 0 == '' is true in PHP $this->assertSame(0, ''); // FAILS — 0 !== '' $this->assertEquals(1, true); // PASSES — 1 == true $this->assertSame(1, true); // FAILS — int !== bool
Because PHP's type juggling can produce surprising equalities, assertSame is almost always the safer choice when you know the exact type of the return value.
Boolean and Null Assertions
Use dedicated methods instead of generic equality checks. They produce clearer failure messages:
php<?php $userService = new UserService(); $isActive = $userService->isActive($userId); // Preferred — failure says "Failed asserting that false is true" $this->assertTrue($isActive); // Avoid — failure says "Failed asserting that false matches expected true" $this->assertEquals(true, $isActive); // Null check $deletedUser = $userService->findById($unknownId); $this->assertNull($deletedUser);
Dedicated assertions also make the test's intent obvious at a glance.
Collection Assertions
assertCount is clearer than manually calling count() inside assertSame:
php<?php $orderRepository = new OrderRepository(); $recentOrders = $orderRepository->findRecentByUser($userId); // Preferred $this->assertCount(3, $recentOrders); // Less readable alternative $this->assertSame(3, count($recentOrders));
The first form produces a better failure message: "Failed asserting that actual size 5 matches expected size 3."
Type Assertions
assertInstanceOf verifies that a factory or container returns the correct type:
php<?php $logger = LoggerFactory::create('file'); $this->assertInstanceOf(FileLogger::class, $logger); $cache = CacheFactory::create('redis'); $this->assertInstanceOf(CacheInterface::class, $cache);
This is especially useful when testing factories, service containers, or any code that returns interfaces.
Common Pitfalls
- Using assertEquals when you need assertSame — Loose comparison hides type bugs. Default to
assertSamefor scalars and switch toassertEqualsonly when you intentionally want loose comparison (e.g., comparing objects by value). - Asserting too many things in one test — If a test has ten assertions, the first failure masks the rest. Split the test or use a data provider to test each case independently.
Best Practices
- Put expected value first — PHPUnit's assertion signature is
assertSame($expected, $actual). Reversing the arguments produces confusing failure messages like "expected 5 but got 10" when you meant the opposite. - Use the most specific assertion available — Prefer
assertCountoverassertSame(3, count(...)),assertStringContainsStringover a manualstrposcheck. Specific assertions yield better failure diagnostics.
Summary
assertSameuses strict===comparison;assertEqualsuses loose==.- Prefer
assertSamefor scalar values to avoid PHP type-juggling surprises. - Use dedicated assertions (
assertTrue,assertNull,assertCount,assertInstanceOf) for clearer intent and better failure messages. - Always place the expected value as the first argument.
Code Examples
<?php
declare(strict_types=1);
namespace Tests\Unit;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use App\InvoiceGenerator;
use App\Invoice;
class InvoiceGeneratorTest extends TestCase
{
#[Test]
public function generatesInvoiceWithCorrectTotal(): void
{
$generator = new InvoiceGenerator();
$invoice = $generator->create(
customerId: 42,
lineItems: [
['description' => 'Widget', 'amount' => 19.99],
['description' => 'Gadget', 'amount' => 49.99],
]
);
$this->assertInstanceOf(Invoice::class, $invoice);
$this->assertSame(42, $invoice->customerId);
$this->assertSame(69.98, $invoice->total);
$this->assertCount(2, $invoice->lineItems);
$this->assertFalse($invoice->isPaid());
$this->assertNull($invoice->paidAt);
}
}