pestphp/pest · error · BadMethodCallException

Expectation value length is not countable.

Error message

Expectation value length is not countable.

What it means

toHaveLength() supports three value kinds: strings (mb_strlen comparison), iterables (delegated to toHaveCount), and objects (toArray() if present, else cast to array and counted). For anything else — int, float, bool, null — there is no meaningful length, so Pest throws BadMethodCallException instead of guessing. Note the sibling toHaveCount() has its own stricter guard for countable|iterable values.

Source

Thrown at src/Mixins/Expectation.php:240

        if (is_string($this->value)) {
            Assert::assertEquals($number, mb_strlen($this->value), $message);

            return $this;
        }

        if (is_iterable($this->value)) {
            return $this->toHaveCount($number, $message);
        }

        if (is_object($this->value)) {
            $array = method_exists($this->value, 'toArray') ? $this->value->toArray() : (array) $this->value;

            Assert::assertCount($number, $array, $message);

            return $this;
        }

        throw new BadMethodCallException('Expectation value length is not countable.');
    }

    /**
     * @return self<TValue>
     */
    public function toHaveCount(int $count, string $message = ''): self
    {
        if (! is_countable($this->value) && ! is_iterable($this->value)) {
            InvalidExpectationValue::expected('countable|iterable');
        }

        Assert::assertCount($count, $this->value, $message);

        return $this;
    }

    /**
     * @param  Countable|iterable<mixed>  $expected

View on GitHub (pinned to 1af74a215c)

Solutions

  1. Cast to string when you mean character length: expect((string) $value)->toHaveLength(5).
  2. If you mean item count, keep the value iterable/countable and use toHaveCount() instead.
  3. Handle null separately (expect($value)->not->toBeNull()) before asserting length.
  4. Fix the producer to return the string form (e.g., (string) $id) when length semantics matter.

Example fix

// before
expect($zipCode)->toHaveLength(5); // $zipCode is int 12345
// after
expect((string) $zipCode)->toHaveLength(5);
// or fix the producer to keep it a string
Defensive patterns

Strategy: type-guard

Validate before calling

$value = $api->zipCode(); // could be int|string
if (is_int($value)) {
    $value = (string) $value;
}
expect($value)->toHaveLength(5);

Type guard

function hasLength(mixed $value): bool
{
    return is_string($value) || is_iterable($value) || is_object($value);
}

// usage
if (hasLength($value)) {
    expect($value)->toHaveLength(5);
} else {
    expect($value)->toBeNull(); // or handle scalar case explicitly
}

Prevention

When it happens

Trigger: expect(42)->toHaveLength(2); expect(null)->toHaveLength(0); expect(3.14)->toHaveLength(4); expect(true)->toHaveLength(...); passing a value that a producer typed as int (strlen result, count result) when a string was intended.

Common situations: Counting digits and forgetting to cast: expect(12345)->toHaveLength(5) instead of expect((string) 12345); null returned by a lookup fed directly into the chain; strict-types codebases where an int sneaks past reviews; numbers coming from APIs as int rather than numeric strings.

Related errors


AI-assisted analysis of pestphp/pest@1af74a215c (2026-08-21). Data as JSON: /api/errors/c71ee86a80dfc8cf. Report an issue: GitHub.