In Pest, how do expect()->toBe() and toEqual() differ, and how do the not and and() modifiers change an expectation chain?
answer
- toBe maps to assertSame
- toEqual maps to assertEquals
- objects: same instance versus equal state
- not flips only the next expectation
- and() switches the value under test
basics
~20 stoBe() checks type and value and, for objects, the same instance; toEqual() checks equal value loosely, so '1' equals 1 and two equal objects pass. not inverts only the next expectation, and and() starts a new expectation on another value.
solid answer
~30 s`expect($value)` wraps a value; each `toX()` call asserts and returns the expectation so calls chain. `toBe()` delegates to PHPUnit's `assertSame()`: same type and value, and for objects the very same instance, so `expect('1')->toBe(1)` fails. `toEqual()` delegates to `assertEquals()`: `expect('1')->toEqual(1)` passes and two distinct objects with equal properties pass. For money totals, ids and flags I use `toBe()`; `toEqual()` for value objects compared by state. `->not` inverts only the expectation right after it, then the chain continues on the same value: `->not->toBeNull()->toBeInt()`. `->and($other)` switches the subject, so one chain can check several values: `expect($cart->total())->toBe(1998)->and($cart->count())->toBe(2)`.
code
php · 15 lines<?php
declare(strict_types=1);
use App\Shop\Cart;
use App\Shop\Money;
it('totals two items', function () {
$cart = new Cart();
$cart->add('SKU-1', 2, new Money(999));
expect($cart->total())->toBe(1998)
->not->toBe(0)
->and($cart->count())->toBe(2)
->and($cart->subtotal())->toEqual(new Money(1998));
});go deeper
Recall that toBe() is strict and toEqual() is loose, that not inverts an expectation, and that and() moves to a new value.
Explain the mapping to assertSame() and assertEquals(), identity versus state for objects, and exactly how far not and and() reach in a chain.
Show where loose equality would hide real bugs in money or id handling and set team defaults for toBe() versus toEqual().
Frame assertion strictness as a suite-wide policy that reviewers can check, rather than a per-test preference.
## The expectation object In Pest, `expect($value)` returns an **expectation** that wraps the value. Every expectation method, such as `toBe()` or `toBeInt()`, performs one assertion and returns the expectation again, so calls chain: ```php expect($cart->total()) ->toBeInt() ->toBeGreaterThan(0) ->toBe(1998); ``` Under the hood these methods call PHPUnit's assertions, so failures, assertion counts and reports work exactly as in a PHPUnit test. ## `toBe()` versus `toEqual()` | Expectation | Delegates to | Scalars | Objects | |---|---|---|---| | `toBe($expected)` | `assertSame()` | same type **and** value | the same instance | | `toEqual($expected)` | `assertEquals()` | equal value, loosely | equal class and properties | Concretely: ```php expect(1)->toBe(1); // passes expect('1')->toBe(1); // fails: string vs int expect('1')->toEqual(1); // passes expect(new Money(500))->toBe(new Money(500)); // fails: two instances expect(new Money(500))->toEqual(new Money(500)); // passes: same state ``` The practical rule: use `toBe()` for scalars where type matters - totals in cents, counts, booleans, identifiers - because a `'1998'` string slipping through a type-juggling bug should fail the test. Use `toEqual()` when comparing value objects by state or when order-sensitive array equality is what you mean. For arrays with different key order, `toEqualCanonicalizing()` compares regardless of order, and for floats `toEqualWithDelta()` accepts a tolerance. ## `not`: invert the next expectation `->not` (also callable as `->not()`) wraps the expectation so that the **next** expectation must fail for the test to pass. Only that one call is inverted; the chain then continues positively on the same value: ```php expect($cart->total()) ->not->toBeNull() // inverted ->toBeInt() // positive again ->not->toBe(0); // inverted ``` A common misreading is that `not` stays in force for the rest of the chain. It does not, and the example above would be wrong if it did. ## `and()`: switch the subject `->and($otherValue)` returns a new expectation for a different value, so one fluent chain can check several related values: ```php expect($cart->total())->toBe(1998) ->and($cart->count())->toBe(2) ->and($cart->isEmpty())->toBeFalse(); ``` After `and()`, every expectation applies to the new value; there is no way back to the previous one except starting another `expect()`. ## Related modifiers worth knowing - `->each` applies the next expectation to every item of an iterable: `expect($cart->prices())->each->toBeInt()`. - **Higher-order expectations**: accessing a property or calling a method on the expectation reads it from the value, as in `expect($cart)->count()->toBe(2)`. - `->sequence(...)` applies one closure per item, in order. ## Mapping from PHPUnit assertions Every common PHPUnit assertion has an expectation counterpart, which matters when converting tests or reading mixed code: | PHPUnit | Pest expectation | |---|---| | `assertSame($e, $a)` | `expect($a)->toBe($e)` | | `assertEquals($e, $a)` | `expect($a)->toEqual($e)` | | `assertCount(2, $a)` | `expect($a)->toHaveCount(2)` | | `assertTrue($a)` | `expect($a)->toBeTrue()` | | `assertNull($a)` | `expect($a)->toBeNull()` | | `assertInstanceOf(C::class, $a)` | `expect($a)->toBeInstanceOf(C::class)` | | `assertContains($x, $a)` | `expect($a)->toContain($x)` | Note the argument order: PHPUnit takes the expected value first, while in Pest the actual value goes into `expect()` and the expected value into the matcher. Most expectation methods also accept an optional failure message as their last argument, for example `->toBe(1998, 'total is in cents')`, which is printed when the assertion fails. ## Mistakes that pass review 1. Using `toEqual()` everywhere "because it is more forgiving". It lets a string total or a `null` that equals `0` loosely slip through. 2. Expecting `toBe()` to compare two freshly built value objects. It checks identity for objects. 3. Assuming `not` negates the rest of the chain. 4. Forgetting that after `and()` the original value is no longer under test. 5. Mixing `$this->assertSame()` and `expect()` randomly in one file. Both work, but a team usually standardises on one style for readability. ## Summary In an interview, tie the answer to a concrete bug: a cart total that comes back as the string `'1998'` after passing through a form or a JSON decode passes `toEqual(1998)` and fails `toBe(1998)`, and only the second outcome is what you want from a test guarding money. `toBe()` is strict (`assertSame`), `toEqual()` is loose (`assertEquals`). `not` flips exactly one expectation, and `and()` moves the chain to a new value.
- In Pest, why can expect($cart->total())->toEqual(1998) hide a bug that toBe(1998) would catch?`toEqual()` uses PHPUnit's loose `assertEquals()`, so a total returned as the string `'1998'` or the float `1998.0` still passes. `toBe()` uses `assertSame()` and fails on the type difference, which is exactly the kind of type-juggling bug a money calculation should never hide.
- In Pest, what does expect($x)->not->toBeNull()->toBeInt() assert?That `$x` is not null and that `$x` is an int. `not` inverts only `toBeNull()`; the chain then continues with a positive `toBeInt()` on the same value. To invert both, write `not` before each expectation.
saying these in an interview costs you the question
- Says toBe() compares objects by their property values
- Thinks not inverts every expectation after it in the chain
- Believes and() adds another check on the same value
- Uses toEqual() for money totals where the type matters
- Claims Pest expectations bypass PHPUnit's assertions